Receive signed notifications when events occur in DeepCredentials, such as a verification session completing or an invitation reaching a terminal state.
- A publicly accessible HTTPS endpoint to receive webhook events (plain
httpis accepted only outside production). - An API credential whose scope matches the resources you fetch (
deepcredentials.verifyfor verification,deepcredentials.issuefor issuance,deepcredentials.self-servicefor self-service).
- Log in to the DeepCredentials portal.
- Select your organization and the environment (
betaorproduction) the webhook is for. Each environment has its own webhook URL, signing secret, and event selection. - Navigate to B2B API.
- Enter your Webhook URL (must be HTTPS in production).
- Click Save.
On first save the portal generates a signing secret — copy it immediately, it is shown only once. Use it to verify webhook signatures (recommended — see Verifying signatures below).
If you plan to verify signatures, store the signing secret securely. If you lose it, use Rotate Secret to generate a new one.
Below the URL the portal renders a checkbox grid grouped by family (Verification / Credential issuance / Self-service). Tick only the events your integration cares about — anything you leave unchecked is silently dropped at our edge with no delivery, no retry.
- New webhook configs default to all events checked, so a first-time setup receives every event type.
- Existing webhook configs keep their current event set when we add new event types — you have to opt in explicitly. In particular, existing integrations must enable
credential_invitation.status_changedin the Credential issuance group before invitation callbacks are sent. - Group-level toggles let you flip an entire family on or off in one click; the header shows
All on/Partial/All offfor quick scanning. - Save replaces the full allowlist. There is no separate "subscribe to event X" call.
Once configured, the page can also send a signed sample of any event type to your endpoint (resource IDs are the nil UUID) and shows the response status, the signature, and the exact payload sent. Test deliveries ignore the event selection.
Every webhook event uses the same envelope. data is deliberately slim — only the resource id (and a status hint where the trigger is a status transition). To read claims, captured data, identification details, or any other field, fetch the resource via the linked GET endpoint with your API credential. Webhooks intentionally do not carry captured data or PII over the wire.
Event type (same as the X-Webhook-Event header).
Your organization ID.
When the event occurred (RFC 3339 UTC).
Slim per-event payload — see Events below.
| Event | Trigger | data payload | Fetch full snapshot |
|---|---|---|---|
verification.completed | A verification session reached a terminal status. | { session_id, status, credential_status } where status ∈ success, failed, expired, and credential_status ∈ valid, suspended, revoked (present only when status is success). | GET /b2b/v1/verification-sessions/{sessionId} |
issuing.status_changed | A credential offer reached a terminal status. | { offer_id, status } where status ∈ issued, failed, expired, revoked. | GET /b2b/v1/credential-offers/{offerId} |
credential_invitation.status_changed | An invitation was redeemed, expired, or revoked. | { invitation_id, status, offer_id? }; offer_id is present only for redeemed. | GET /b2b/v1/credential-invitations/{invitationId} |
credential.status_changed | An issued credential's status changed (revoked / suspended / reactivated). | { credential_id, status } where status ∈ revoked, suspended, active. | GET /b2b/v1/issued-credentials/{credentialId} |
self_service.request.submitted | End-user finished a self-service flow; the request needs your decision. Only fires for configs with verification_mode: webhook. See the Self-Service Detailed Reference. | { request_id } | GET /b2b/v1/self-service/requests/{requestId} |
self_service.request.approved | A self-service request was approved (any verification mode). Fires regardless of who made the decision, so you have an audit trail. | { request_id } | GET /b2b/v1/self-service/requests/{requestId} |
self_service.request.rejected | A self-service request was rejected (manual or webhook mode). The rejection reason is on the fetched record. | { request_id } | GET /b2b/v1/self-service/requests/{requestId} |
self_service.request.credential_issued | The credential offer created from the request reached issued (credential delivered to the wallet) — terminal success. | { request_id } | GET /b2b/v1/self-service/requests/{requestId} |
self_service.request.offer_expired | The request expired: either the credential offer created from an approved request expired before the wallet picked it up, or the request timed out before an offer was created (for example while awaiting identification or your decision). Terminal. | { request_id } | GET /b2b/v1/self-service/requests/{requestId} |
self_service.request.failed | The credential offer created from the request reached failed. Terminal. A failed identification step does not emit this event; read status and failure_code on the request instead. | { request_id } | GET /b2b/v1/self-service/requests/{requestId} |
self_service.request.revoked | The invitation or credential offer created from the request was revoked before the wallet accepted it — by an operator, or because the environment was disabled on the credential definition. The request's status is revoked. Terminal. | { request_id } | GET /b2b/v1/self-service/requests/{requestId} |
swiyu.configured | swiyu connection credentials were saved for an environment. System event — delivered to every configured webhook endpoint regardless of the subscription grid. | { tenant_id, environment, action } | — |
swiyu.deconfigured | Your organization's swiyu integration for an environment was removed. System event — always delivered. | { tenant_id, environment, action } | — |
swiyu.reconfigured | The stored swiyu connection credentials were replaced. System event — always delivered. | { tenant_id, environment, action } | — |
swiyu.refresh_token_replaced | Only the swiyu refresh token was replaced (recovery of a lapsed connection). System event — always delivered. | { tenant_id, environment, action } | — |
swiyu.did_registered | A DID was registered for the environment, which enables verification. System event — always delivered. | { tenant_id, environment, action } | — |
swiyu.issuing_enabled | A status list was created and issuance was enabled for the environment. System event — always delivered. | { tenant_id, environment, action } | — |
The status hint on the *.status_changed and verification.completed events lets you decide which resource to reconcile. credential_status on verification.completed lets you distinguish a suspended or revoked credential from a valid one before the follow-up GET. A failed session carries no reason in the callback; error_code and error_description are on the fetched session. A redeemed invitation includes the resulting offer_id, which is the handoff to the normal offer lifecycle. Self-service event names already encode the lifecycle stage, so they only carry request_id. For swiyu.* events, action repeats the event name without the swiyu. prefix.
Invitation callbacks deliberately omit the invitation code, email address, claims, credential values, custom message, and redemption URL. Creation and code rotation do not emit credential_invitation.status_changed; their synchronous B2B responses are authoritative.
When you need the full payload — claims, a verification session's disclosed attributes, identification result, admin-claims schema, error fields — call the linked GET endpoint with the resource id from data. Same access token (and X-Org-Id / X-Environment headers) you'd use for any B2B call.
- Developmenthttps://api.dev.deepcredentials.swiss/b2b/v1/verification-sessions/{sessionId}
- Integrationhttps://api.int.deepcredentials.swiss/b2b/v1/verification-sessions/{sessionId}
curl -i -X GET \
'https://api.dev.deepcredentials.swiss/b2b/v1/verification-sessions/{sessionId}?response_code=string' \
-H 'Authorization: Bearer <YOUR_JWT_HERE>' \
-H 'X-Environment: beta' \
-H 'X-Org-Id: 497f6eca-6276-4993-bfeb-53cbbbba6f08'- Developmenthttps://api.dev.deepcredentials.swiss/b2b/v1/credential-offers/{offerId}
- Integrationhttps://api.int.deepcredentials.swiss/b2b/v1/credential-offers/{offerId}
curl -i -X GET \
'https://api.dev.deepcredentials.swiss/b2b/v1/credential-offers/{offerId}' \
-H 'Authorization: Bearer <YOUR_JWT_HERE>' \
-H 'X-Environment: beta' \
-H 'X-Org-Id: 497f6eca-6276-4993-bfeb-53cbbbba6f08'- Developmenthttps://api.dev.deepcredentials.swiss/b2b/v1/credential-invitations/{invitationId}
- Integrationhttps://api.int.deepcredentials.swiss/b2b/v1/credential-invitations/{invitationId}
curl -i -X GET \
'https://api.dev.deepcredentials.swiss/b2b/v1/credential-invitations/{invitationId}' \
-H 'Authorization: Bearer <YOUR_JWT_HERE>' \
-H 'X-Environment: beta' \
-H 'X-Org-Id: 497f6eca-6276-4993-bfeb-53cbbbba6f08'- Developmenthttps://api.dev.deepcredentials.swiss/b2b/v1/issued-credentials/{credentialId}
- Integrationhttps://api.int.deepcredentials.swiss/b2b/v1/issued-credentials/{credentialId}
curl -i -X GET \
'https://api.dev.deepcredentials.swiss/b2b/v1/issued-credentials/{credentialId}' \
-H 'Authorization: Bearer <YOUR_JWT_HERE>' \
-H 'X-Environment: beta' \
-H 'X-Org-Id: 497f6eca-6276-4993-bfeb-53cbbbba6f08'- Developmenthttps://api.dev.deepcredentials.swiss/b2b/v1/self-service/requests/{requestId}
- Integrationhttps://api.int.deepcredentials.swiss/b2b/v1/self-service/requests/{requestId}
curl -i -X GET \
'https://api.dev.deepcredentials.swiss/b2b/v1/self-service/requests/{requestId}' \
-H 'Authorization: Bearer <YOUR_JWT_HERE>' \
-H 'X-Environment: beta' \
-H 'X-Org-Id: 497f6eca-6276-4993-bfeb-53cbbbba6f08'| Header | Description |
|---|---|
X-Webhook-Event | Event type (same as event field in body). |
X-Webhook-Signature | HMAC-SHA256 hex digest of the raw request body. |
X-Webhook-Timestamp | Unix timestamp (seconds) of the delivery. |
X-Webhook-Signature-Previous | HMAC-SHA256 hex digest of the raw body under the previous secret. Present only during the 24-hour secret rotation grace period. |
Every webhook request includes an HMAC-SHA256 signature of the raw request body in the X-Webhook-Signature header. Verifying it is strongly recommended but is your decision — your endpoint will receive the delivery either way. Skipping verification means you have no way to confirm the request actually came from DeepCredentials: HTTPS protects the channel, but anyone who learns your webhook URL (through logs, error trackers, leaked configs, etc.) can hit it directly.
Algorithm: HMAC-SHA256(signing_secret, raw_request_body) → lowercase hex-encoded.
Only the raw body is signed. X-Webhook-Timestamp is not part of the signed input, and DeepCredentials enforces no timestamp tolerance. To reject replayed requests, compare the body's signed timestamp field with your clock and choose your own tolerance.
EXPECTED=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SIGNING_SECRET" | awk '{print $2}')
if [ "$SIGNATURE_HEADER" = "$EXPECTED" ]; then
echo "Valid"
fiimport hashlib
import hmac
def verify_signature(body: bytes, secret: str, signature_header: str) -> bool:
expected = hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature_header)
# In your webhook handler:
is_valid = verify_signature(
body=request.body,
secret="YOUR_SIGNING_SECRET",
signature_header=request.headers["X-Webhook-Signature"],
)const crypto = require("crypto");
function verifySignature(body, secret, signatureHeader) {
const expected = crypto
.createHmac("sha256", secret)
.update(body)
.digest("hex");
if (typeof signatureHeader !== "string" || signatureHeader.length !== expected.length) {
return false;
}
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader)
);
}
// In your webhook handler:
const isValid = verifySignature(
req.rawBody,
"YOUR_SIGNING_SECRET",
req.headers["x-webhook-signature"]
);Always use constant-time comparison to prevent timing attacks.
To rotate the signing secret without dropping events:
- Click Rotate Secret in the portal webhook settings.
- Copy the new secret.
- For 24 hours after rotation (the grace period), DeepCredentials sends two signature headers:
X-Webhook-Signature— signed with the new secret.X-Webhook-Signature-Previous— signed with the old secret.
- Update your verification logic to check both headers.
- Once your code uses the new secret, remove the old secret from your configuration. After the grace period,
X-Webhook-Signature-Previousis no longer sent.
Webhook delivery currently makes one HTTP attempt per event, with a 10-second timeout. A timeout, connection error, or non-2xx response is logged and then discarded; there is no delivery persistence, retry queue, or replay API. Callbacks can therefore be missed.
Treat every callback as a slim progress hint, not as the authoritative resource or a guaranteed delivery stream. Reconcile through authenticated B2B reads:
- Poll the verification session or credential offer (
GETby ID) while your workflow is active. - Use invitation list filters and
GET /b2b/v1/credential-invitations/{invitationId}to reconcile invitation state. - Do not depend on callbacks arriving in a particular order. The redeemed invitation hint carries
offer_idso you can follow the correct offer independently.
- Respond quickly — Return a
2xxstatus within 10 seconds (the delivery timeout). Process the event asynchronously if the work takes longer. - Verify signatures (recommended) — Validating the HMAC is the only way to confirm the request originated from DeepCredentials. Skipping it is supported but leaves your endpoint trusting any caller who knows the URL.
- Reconcile from the API — Fetch the resource through its authenticated GET endpoint before making a final business decision, and use polling or scheduled reconciliation to cover missed callbacks.
- Protect captured payloads — If you retain raw request bodies for signature verification or audit, apply your normal access controls and retention policy even though lifecycle callbacks are slim.
- Verification Flow — Understand the full session lifecycle.
- Error Handling — Handle error events in webhooks.