---
title: "Issue a portable verification credential for an approved customer"
method: GET
path: "/customers/{customer_id}/credential"
tags: ["Customers"]
---

# Issue a portable verification credential for an approved customer

`GET /customers/{customer_id}/credential`

## Request

- Base URL: `https://developers.ozav.io/v2`
- URL: `https://developers.ozav.io/v2/customers/{customer_id}/credential`
- Auth: one of:
  - API key in header `X-API-Key`
  - HTTP bearer

Returns a **portable credential** signed by OZAV that attests what OZAV
verified about this customer, when, and how - as an IETF SD-JWT
(`vc+sd-jwt`, ES256). Hand it to the end user: they can present it to
any third party and choose which attributes to disclose (for example
`over_18` without `cpf`). Any verifier checks it offline against the
public key at `jwks_url` and confirms it is still in force at
`status_url`.

**Selectively disclosable claims:** `cpf`, `full_name`, `birth_date`,
`over_18`, `kyc_level` (`3` = hosted session with document, liveness and
face match; `2` = document-based identification), `method`,
`verified_at`, `screening`. The JWT body itself carries no personal
data - only salted digests - so the credential leaks nothing the holder
does not disclose.

Each call issues a **new** credential (new `id`). Earlier ones stay in
force until they expire or the customer's status changes: a credential
is reported `revoked` at `status_url` as soon as the customer is no
longer approved, is frozen, or is re-verified. There is no revocation
step for you to call. Issuance is capped per customer per day
(`429 CREDENTIAL_ISSUANCE_LIMIT_REACHED` past the cap): keep the
credential you received rather than requesting one per use.

Individual customers identified by a Brazilian CPF only. Not issued in
the sandbox environment. Availability is enabled per organization by
OZAV; until then the call returns `422 FEATURE_NOT_AVAILABLE`.

## Response `200`

A freshly issued credential.

- CustomerCredential: A portable verification credential (IETF SD-JWT, `vc+sd-jwt`) signed by OZAV. Give `credential` to the end user; keep `id` to look the credential up at `status_url`.
  - `id` string, required: Opaque credential id (the JWT `jti`).
  - `object` 'credential', required: Resource type discriminator; always the literal `credential`.
  - `customer_id` string, required: The customer this credential attests.
  - `format` 'vc+sd-jwt', required: Always `vc+sd-jwt` - an IETF SD-JWT with selectively disclosable claims.
  - `credential` string, required: The compact SD-JWT, `<jwt>~<disclosure>~...~`. Hand it to the end user; do not store it as personal data - the disclosures carry the CPF, name and birth date in clear.
  - `kyc_level` 2 | 3, required: `3` = hosted session (document, liveness, face match); `2` = document-based identification (reliance or manual review).
  - `method` 'hosted_session' | 'reliance' | 'manual_review', required: How OZAV verified the customer.
  - `issued_at` string, date-time, required: When this credential was issued (RFC 3339, UTC).
  - `expires_at` string, date-time, required: When this credential stops being valid regardless of the customer's status.
  - `jwks_url` string, uri, required: Where OZAV publishes the signing keys (JWKS, `kid`-addressed). Public; no API key.
  - `status_url` string, uri, required: Public status of this credential, `valid` | `revoked` | `expired` | `unknown`. No API key; no personal data.

## Other responses

- `401`: Missing or invalid API key.
- `403`: The organization lacks the entitlement (code ENTITLEMENT_REQUIRED) or the key lacks the scope (code INSUFFICIENT_SCOPE). On money-moving requests (POST /transfers, POST /wallets/{wallet_id}/transfers) a compliance hold on the customer answers with code COMPLIANCE_HOLD: the hold is reviewed and lifted on OZAV's side; retrying does not clear it. When the only hold is a pause until pending onboarding documents are received, a withdrawal answers CUSTOMER_DOCUMENTS_PENDING instead: send what `requirements` on GET /customers/{id} lists; OZAV lifts the pause after reviewing it. On OZAV Services (live without the platform's KYB), any API outside decisions, screenings, lists and self-custody wallets answers with code PRODUCTION_NOT_ACTIVATED.
- `404`: The resource does not exist, is not visible to the caller, or the id was malformed for this position (see "Resource ids" under Errors). These share one status; `code` and `message` tell them apart.
- `409`: Conflict (e.g. IDEMPOTENCY_KEY_REUSED).
- `422`: The request is well-formed but cannot be fulfilled - e.g. a `rail` that is not served (see `Rail.x-ozav-rail-availability`).
- `429`: Too many requests. Read `code` first. `RATE_LIMIT_EXCEEDED` is the per-minute limit: wait the number of seconds in the `Retry-After` response header before retrying; retrying sooner just consumes budget without being served. A cap with its own code (for example `CREDENTIAL_ISSUANCE_LIMIT_REACHED`, which resets the next day) carries no `Retry-After`, and retrying sooner than its stated reset cannot succeed.
- `503`: `CREDENTIAL_SIGNING_UNAVAILABLE` - transient; retry with backoff.

---

[API](https://skmtc.dev/ozav/apis/ozav-api.md) - [All operations](https://skmtc.dev/ozav/apis/ozav-api/llms.txt) - [OpenAPI document](https://skmtc.dev/ozav/apis/ozav-api/revisions/4133dc2109d7?raw)
