Tutorial: complete your first credential lifecycle
You will configure an issuer, define a credential type, issue a credential, verify one, and revoke it — using the Management API for configuration and the Product API for the transactions.
Time: about 45 minutes.
You need: a Sandbox API key (see sandbox) and curl.
Everything here happens in Sandbox. Nothing you create is real, and nothing costs anything.
What you will and will not automate
Two steps in a credential's life belong to the holder: accepting an offer, and presenting a credential. They happen in a wallet, on a person's device, and they cannot be driven from your server. That is the point of the model — a credential nobody accepted is not held by anyone.
So this tutorial takes you to the boundary and stops honestly at it. You will see an issuance transaction reach
awaiting_wallet and a verification reach pending. Those are correct terminal states for an integration
without a wallet in the loop, not failures.
Set up
export DILIGENCE_BASE_URL="https://api.example.diligence.id"
export DILIGENCE_API_KEY="YOUR_API_KEY"
export MGMT="$DILIGENCE_BASE_URL/management"
export API_VERSION="2026-08-30"
A Sandbox key starts did_test.. If yours starts did_live. stop — you are pointed at production.
Step 1 — Create an organisation
Credentials are issued on behalf of a legal entity.
curl -sS -X POST "$MGMT/organisations?api-version=$API_VERSION" \
-H "Authorization: ApiKey $DILIGENCE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalReference": "tutorial-org",
"displayName": "Tutorial Organisation",
"legalName": "Tutorial Organisation Limited",
"countryCode": "NZ"
}'
You get back a Management resource. Note the shape — every Management response looks like this:
{
"id": "/management/organisations/tutorial-org",
"name": "tutorial-org",
"type": "DiligenceID/organisations",
"etag": "\"AAAAAAAAB9E=\"",
"properties": { "reference": "tutorial-org", "legalName": "Tutorial Organisation Limited" },
"systemData": { "createdAt": "2026-08-30T04:11:02Z", "lastModifiedAt": "2026-08-30T04:11:02Z" }
}
Save the reference:
export ORG="tutorial-org"
Step 2 — Create an issuer
An issuer is a signing identity: its own keys, its own published metadata, its own credentials.
curl -sS -X POST "$MGMT/issuers?api-version=$API_VERSION" \
-H "Authorization: ApiKey $DILIGENCE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"displayName": "Tutorial Issuer",
"organisationReference": "'"$ORG"'",
"issuerType": "Root",
"signingKey": { "algorithm": "ES256", "provider": "Software" }
}'
The issuer's environment comes from your key, never from this body. A Sandbox key cannot create a Production issuer, whatever it asks for.
export ISSUER="the-name-from-the-response"
The issuer is created in Draft. It cannot issue yet.
Step 3 — Define a credential type
A credential configuration says what a credential of this type contains.
curl -sS -X POST "$MGMT/credential-configurations?api-version=$API_VERSION" \
-H "Authorization: ApiKey $DILIGENCE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"issuerId": "'"$ISSUER"'",
"identifier": "tutorial-membership",
"displayName": "Tutorial Membership",
"credentialType": "TutorialMembership",
"format": "dc+sd-jwt",
"claims": [
{ "name": "member_number", "displayName": "Member number", "required": true },
{ "name": "member_since", "displayName": "Member since", "required": false }
]
}'
Claims are a contract. Issuance rejects any claim not declared here — a typo in a claim name fails at issuance rather than producing a credential missing a field nobody notices for six months.
Step 4 — Check readiness, then activate
Readiness tells you what is outstanding, so activation is never a guess:
curl -sS "$MGMT/issuers/$ISSUER/readiness?api-version=$API_VERSION" \
-H "Authorization: ApiKey $DILIGENCE_API_KEY"
When readyForActivation is true:
curl -sS -X POST "$MGMT/issuers/$ISSUER/activate?api-version=$API_VERSION" \
-H "Authorization: ApiKey $DILIGENCE_API_KEY"
If activation returns 400, the body lists the unmet requirements. Fix those rather than retrying.
Configuration is done. Everything below is the Product API.
Step 5 — Issue a credential
curl -sS -X POST "$DILIGENCE_BASE_URL/v1.0/credentials/issuance" \
-H "Authorization: ApiKey $DILIGENCE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: tutorial-issue-001" \
-d '{
"issuerId": "'"$ISSUER"'",
"credentialConfiguration": "tutorial-membership",
"externalSubjectReference": "tutorial-subject-1",
"claims": { "member_number": "M-000123", "member_since": "2021-04-01" }
}'
You get 201 and a transaction with "status": "awaiting_wallet" and an offer for the holder.
This is the boundary. The credential is not yet held by anyone. The offer is waiting for a wallet to accept
it, and until that happens awaiting_wallet is the correct state, not a stuck one.
Send that same request again with the same Idempotency-Key: you get the same transaction back, not a second
one. Change one claim value and keep the key: you get 409. That is what makes retrying a timed-out request
safe. See idempotency.
Step 6 — Read the transaction
curl -sS "$DILIGENCE_BASE_URL/v1.0/credentials/issuance/$TRANSACTION_ID" \
-H "Authorization: ApiKey $DILIGENCE_API_KEY"
In a real integration this is what your system polls, or what your webhook handler confirms, before telling a user their credential is ready.
Step 7 — Define what a verifier will accept
A verification policy is written once by whoever understands the risk, and referenced by name at runtime.
curl -sS -X POST "$MGMT/verification-policies?api-version=$API_VERSION" \
-H "Authorization: ApiKey $DILIGENCE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"policyIdentifier": "tutorial-check",
"displayName": "Tutorial membership check",
"organisationReference": "'"$ORG"'",
"credentialConfigurations": ["tutorial-membership"],
"requireActiveCredentialStatus": true
}'
Note what a verifier does not do at runtime: name claims, choose which issuers to trust, or construct OpenID4VP parameters. Those are policy decisions, frozen here. An endpoint that accepted raw claim names from the caller would let a compromised front end ask for anything.
Step 8 — Run a verification
curl -sS -X POST "$DILIGENCE_BASE_URL/v1.0/verifications" \
-H "Authorization: ApiKey $DILIGENCE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: tutorial-verify-001" \
-d '{ "policyIdentifier": "tutorial-check" }'
You get a verification in pending with a request for the holder to respond to — typically rendered as a QR
code. It stays pending until a wallet presents something, which is again the honest boundary.
Read it back the same way you read the issuance.
Step 9 — Revoke
curl -sS -X POST "$DILIGENCE_BASE_URL/v1.0/credentials/$CREDENTIAL_ID/revoke" \
-H "Authorization: ApiKey $DILIGENCE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "reason": "Tutorial cleanup" }'
Revocation is published in a status list that verifiers read directly. It is not a message sent to anyone: a verifier checking a revoked credential finds out because it looked, not because it was told.
Revocation is permanent. There is no un-revoke — reissue instead.
What you built
Management Product
organisation issuance transaction
issuer (activated) verification
credential configuration revocation
verification policy
Configuration through one plane, transactions through the other, one key for both.
Next
- Build an issuer integration — production concerns
- Build a verifier integration
- Handle API errors
- Handle concurrency
- SDKs — the same flow in typed code
- Production integration