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
- An installed credential issuance service.
- At least one credential configuration, so there is something to issue.
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:
{
"port": "...",
"baseUrl": "...",
"database": {
"..."
},
"issuer": {
"..."
},
"signing": {
"..."
},
"sessionCallback": { // (1)!
"targetUrl": "https://example-application.com/issued-credential", // (2)!
"bearerToken": "81952788-6321-412c-91b9-8f61ee2a1e52" // (3)!
}
}
- Where the copy of each issued credential is pushed, and how the request authenticates itself.
- 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.
- Optional, but configure it. Sent as
Authorization: Bearer 81952788-6321-412c-91b9-8f61ee2a1e52on 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_idmatches what you got back when you started the issuance. Use it to tie the result to the right holder.issuance_successfulis whether the credential reached the wallet.revocation_uriis where toPOSTto revoke this credential later, and is present only when the issuance succeeded. See Revocation.credential_response.credentialsholds 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.idxis the credential's index in your status list, which is what a verifier reads to decide whether the credential is still valid. Revoking goes throughrevocation_uriabove, so you do not need to record the index yourself.subis the holder'sdid: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:
targetUrlis reachable from inside the container — not just from your machine.- Your endpoint returns a success status.
- The session actually completed:
GET /issuance/oid4vci/status/{session_id}and readerror_message.
More in Troubleshooting.
What's Next
- Revocation: What to do with the
revocation_uriyou just recorded. - Configuration reference: Every issuer setting.
- Going to production: Authenticating the callback endpoint, and what else has to change.