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",
  "revocation_uri": "https://issuer.example.org/credential-status/revoke/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.
  • revocation_uri is where to POST to revoke this credential later, and is present only when the issuance succeeded. See Revocation.
  • 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, which is what a verifier reads to decide whether the credential is still valid. Revoking goes through revocation_uri above, so you do not need to record the index yourself.
  • 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

The callback hands you a full copy of the credential, claims and all, in readable form. Recording the session_id from the callback body is enough to reconcile the issuance and to revoke it later — keep the revocation_uri alongside it if you would rather not rebuild the address. Nothing in the decoded payload needs storing, so discard the rest rather than keeping 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