Skip to content

Receive the Issued Credential

When an issuance completes, the issuance service pushes a copy of the issued credential to your application. This is step 9 of the issuance flow, and configuring it is how you learn that a credential was delivered without polling.

Requirements

1. Expose an endpoint

Your application needs an endpoint that accepts POST requests with a JSON body. The issuance service calls it once per completed session.

Give it a secret and check it. The configuration below sends a bearer token with every call; verify it on your side before processing the body. Without that check, the endpoint is an unauthenticated way into your application for anyone who learns the URL.

2. Configure the callback

Set sessionCallback in the issuance service's server.json:

server.json (truncated)
{
  "port": "...",
  "baseUrl": "...",
  "database": {
    "..."
  },
  "issuer": {
    "..."
  },
  "signing": {
    "..."
  },
  "sessionCallback": { // (1)!
    "targetUrl": "https://example-application.com/issued-credential", // (2)!
    "bearerToken": "81952788-6321-412c-91b9-8f61ee2a1e52" // (3)!
  }
}
  1. Where the copy of each issued credential is pushed, and how the request authenticates itself.
  2. Required within this block. The URL of the endpoint from step 1. It must be reachable from inside the issuance service's container — not merely from your desktop.
  3. Optional, but configure it. Sent as Authorization: Bearer 81952788-6321-412c-91b9-8f61ee2a1e52 on every callback.

3. Restart the service

Configuration is read at startup. Stop the container and start it again with the updated file:

docker run -d \
  -p 8079:8079 \
  -w /app \
  -v "$(pwd)/issuer/conf:/app/conf:ro" \
  registry.gitlab.com/secata/platform/did/release/issuer-backend:latest

What you receive

curl -X POST 'https://example-application.com/issued-credential' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer 81952788-6321-412c-91b9-8f61ee2a1e52' \
  -d '{
  "issuance_successful": true,
  "session_id": "7d4633b273f000ca2243e258863975907756b3afc29783c7777cf47839264722",
  "credential_response": {
    "credentials": [
      { "credential": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6andrOi…~WyJ4cjRnako0N3FUeXJNbDBnaVNKa1dnIiwiZmlyc3RfbmFtZSIsIkplbnMiXQ~" }
    ]
  }
}'
  • session_id matches what you got back when you started the issuance. Use it to tie the result to the right holder.
  • issuance_successful is whether the credential reached the wallet.
  • credential_response.credentials holds the issued credentials themselves.

What is inside the credential

The credential value is the SD-JWT that now lives in the holder's wallet. Decoding its payload is worth doing once, because two of its fields matter to you operationally:

{
    "iss": "issuer.example.org",
    "sub": "did:jwk:eyJjcnYiOiJQLTI1NiIs…",
    "nbf": 1704067200,
    "exp": 4102444800,
    "vc": {
        "type": ["VerifiableCredential", "IdCard"],
        "credentialSubject": {
            "id": "did:jwk:…",
            "_sd": ["PgFAKDxd_8r9IhSlZhHHkf2n7CzGYZSp1YyeOnFWMEc", "…"]
        },
        "status": {
            "status_list": {
                "idx": 3,
                "uri": "https://issuer.example.org/credential-status/status-list"
            }
        }
    }
}
  • status.status_list.idx is the credential's index in your status list, and it is the only way to revoke this specific credential later. Store it alongside your own record of who the credential was issued to. See Revocation.
  • sub is the holder's did:jwk: identifier, derived from the key in their wallet. It is what binds the credential to that wallet.

The claim values do not appear in the payload directly — _sd holds hashes of them, and the values themselves are the ~-separated disclosures appended after the signature. That is what makes selective disclosure possible: the holder can send some disclosures and withhold others while the signature stays valid.

Keep only what you need from this payload

The copy contains the claims you issued, in readable form. Recording the session_id and the status list idx is enough to reconcile the issuance and to revoke it later, so discard the rest rather than storing a second copy of data the holder now carries.

If results do not arrive

Check, in order:

  1. targetUrl is reachable from inside the container — not just from your machine.
  2. Your endpoint returns a success status.
  3. The session actually completed: GET /issuance/oid4vci/status/{session_id} and read error_message.

More in Troubleshooting.

What's Next