> ## Documentation Index
> Fetch the complete documentation index at: https://docs.beltic.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Verifying a Credential

> Validate a signed credential JWT against the issuer's keys, schema, status list, and policy.

## Overview

Verification answers a simple question: **is this credential currently valid, and what does it say?** The Beltic Credentials API runs a deterministic seven-step pipeline for every verify call, returning a flat response that's either a success (with the full subject + claims) or a structured rejection (with a machine-readable reason code).

## Prerequisites

* A Beltic API key with `credentials:verify` permission
* A signed credential JWT — the `signed_payload` field from a previous issue response

## The Seven-Step Pipeline

Every verify call runs through these checks, in order. The first failure short-circuits the rest.

<Steps>
  <Step title="Parse JWS">
    Decode the JWT header and payload. Extract `alg`, `kid`, and `iss`. Reject if malformed or if `alg` isn't ES256.
  </Step>

  <Step title="Resolve the issuer key">
    Look up `iss` in the trusted-issuer list (V1 is just `did:web:beltic.com`), then fetch the public JWK by `kid` from `/.well-known/jwks.json`.
  </Step>

  <Step title="Verify the signature">
    Cryptographically verify the JWT against the resolved key using `jose.jwtVerify`. Reject on signature mismatch.
  </Step>

  <Step title="Check standard claims">
    Validate `iat`, `exp`, `nbf`. Optionally validate `aud` if `context.audience` was provided.
  </Step>

  <Step title="Validate the schema">
    Match the JWT claims against the registered credential-type schema. Catches forged credentials whose signature is valid but whose claim shape is wrong.
  </Step>

  <Step title="Check status">
    Look up the credential in Beltic's registry, then check the Status List 2021 bit. Reject if revoked, expired, or suspended.
  </Step>

  <Step title="Apply policy">
    For `agent_authorization` credentials with a verify context, evaluate the credential's `permissions[]` against the request context — fields like `amount`, `currency`, `resource`. Reject on no matching permission or condition failure.
  </Step>
</Steps>

## Basic Verify

The minimum request: just the JWT.

```bash theme={null}
curl -X POST https://api.beltic.com/v1/credentials/verify \
  -H "X-Api-Key: $BELTIC_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"credential\": \"$CREDENTIAL_JWT\"}"
```

<Note>
  The issue response returns the JWT as `signed_payload`; the verify endpoint accepts it as `credential`. Same value, different field names — `signed_payload` is the persisted resource field, `credential` follows the JWT-VC presentation convention. Pass the JWT string directly either way.
</Note>

### Success Response

```json theme={null}
{
  "valid": true,
  "credential_id": "cred_01HQ7P4M6...",
  "credential_type": "business",
  "issuer_did": "did:web:beltic.com",
  "subject": {
    "id": "biz_widgetcorp",
    "type": "organisation",
    "name": "WidgetCorp Ltd"
  },
  "issued_at": "2026-05-21T10:30:00Z",
  "expires_at": "2026-08-19T10:30:00Z",
  "verified_at": "2026-05-21T14:22:11Z",
  "status": "active",
  "evidence_refs": [],
  "verification_id": "ver_01HQ8R..."
}
```

* **`valid: true`** is the headline.
* **`status`** is the persisted lifecycle (`active`, `revoked`, `expired`, `suspended`) — different from `valid`, which is the protocol outcome.
* **`verification_id`** is unique per call. Use it as a correlation handle for audit and support.

### Rejection Response

```json theme={null}
{
  "valid": false,
  "reason": "revoked",
  "credential_id": "cred_01HQ7P4M6...",
  "status": "revoked",
  "verified_at": "2026-05-21T14:30:00Z",
  "verification_id": "ver_01HQ8S...",
  "details": {
    "revoked_at": "2026-05-21T14:25:00Z",
    "reason": "rotated_key"
  }
}
```

## Verify with Policy Context

For `agent_authorization` credentials, you can pass a `context` object to evaluate the agent's permissions against a specific request. This is how downstream services (e.g. a payment gateway) check that the agent is authorized for *this particular call*.

```bash theme={null}
curl -X POST https://api.beltic.com/v1/credentials/verify \
  -H "X-Api-Key: $BELTIC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "credential": "'"$AGENT_JWT"'",
    "context": {
      "resource_type": "wallet",
      "resource_id": "*",
      "action": "payment_authorize",
      "transaction_amount": 4999,
      "transaction_currency": "usd",
      "audience": "stripe.com"
    }
  }'
```

If the credential's permissions allow the action, the response includes a `policy_match`:

```json theme={null}
{
  "valid": true,
  "credential_id": "cred_...",
  "credential_type": "agent_authorization",
  "policy_match": {
    "matched": true,
    "permission_index": 0,
    "conditions_evaluated": [
      { "field": "amount", "op": "lte", "result": "pass" },
      { "field": "currency", "op": "eq", "result": "pass" }
    ]
  },
  "verification_id": "ver_..."
}
```

If a condition fails, you get a structured rejection:

```json theme={null}
{
  "valid": false,
  "reason": "condition_failed",
  "credential_id": "cred_...",
  "details": {
    "deny_reason": "condition_failed:amount",
    "conditions_evaluated": [
      { "field": "amount", "op": "lte", "result": "fail" }
    ]
  },
  "verification_id": "ver_..."
}
```

## Reject Reason Codes

| Reason                   | Pipeline step | Meaning                                                |
| ------------------------ | ------------- | ------------------------------------------------------ |
| `malformed_jwt`          | 1             | JWT couldn't be parsed                                 |
| `alg_not_allowed`        | 1             | Algorithm isn't ES256                                  |
| `issuer_not_trusted`     | 2             | `iss` isn't in the trust list                          |
| `kid_not_found`          | 2             | Key ID doesn't resolve in the issuer's JWKS            |
| `signature_mismatch`     | 3             | Signature failed verification                          |
| `expired`                | 4 or 6        | Credential past its `exp`, or status list says expired |
| `not_yet_valid`          | 4             | `nbf` is in the future                                 |
| `audience_mismatch`      | 4             | `aud` doesn't match `context.audience`                 |
| `schema_mismatch`        | 5             | JWT claim shape doesn't match the credential type      |
| `revoked`                | 6             | Status List 2021 bit is flipped                        |
| `policy_deny`            | 7             | Generic policy denial                                  |
| `no_matching_permission` | 7             | No permission in the credential matched the request    |
| `condition_failed`       | 7             | A permission matched but its conditions failed         |

## Verifying Offline (Without the API)

The full pipeline runs server-side, but the cryptographic checks (steps 1-5) can be replicated offline:

1. Fetch `https://api.beltic.com/.well-known/jwks.json` and cache it. Honour the response's `Cache-Control` header (typically 1 hour).
2. Use any JWT library to verify the JWS against the JWKs.
3. Validate `iat`, `exp`, `nbf` yourself.
4. To check revocation, fetch `https://api.beltic.com/.well-known/status-lists/v1` and decode the bitstring at the credential's `status_list_index`.

The Beltic verify endpoint is recommended for most callers — it handles trust-list management, JWKS caching, and policy evaluation. Offline verification is appropriate when you need to verify in air-gapped environments or want to minimize calls to Beltic.

## Public Verify (No Auth)

There's also a public verify endpoint at `POST /v1/credentials/_public/verify` that doesn't require an API key. It returns the same shape but doesn't expose org-internal fields. Use it for verifier integrations where you can't ship an API key (e.g. browser-side checks).

## Next Steps

<CardGroup cols={2}>
  <Card title="Revoke a Credential" icon="ban" href="/guides/credentials/revoking-a-credential">
    Mark a credential revoked and understand how verifiers see the change.
  </Card>

  <Card title="Issue a Credential" icon="plus" href="/guides/credentials/issuing-a-credential">
    Mint a new credential to verify.
  </Card>
</CardGroup>
