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.
- 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.
- Select your organization.
- Navigate to Settings > Webhooks.
- Enter your Webhook URL (must be HTTPS).
- Click 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 first-time setups receive the same firehose as before.
- 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.
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. | 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 end-user's wallet accepted the credential — terminal success. | { request_id } | GET /b2b/v1/self-service/requests/{requestId} |
self_service.request.offer_expired | The live credential offer created from an approved request expired before the wallet picked it up. Terminal. | { request_id } | GET /b2b/v1/self-service/requests/{requestId} |
self_service.request.failed | A self-service request failed at the wallet-exchange step (typically wallet rejection). Terminal. | { request_id } | GET /b2b/v1/self-service/requests/{requestId} |
swiyu.configured | Your organization's swiyu integration became active 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 } | — |
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 locked or revoked credential from a clean one before the follow-up GET. 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.
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, captured credential_subject_data, 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}' \
-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 | Present during 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) → hex-encoded.
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");
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.
- During the grace period, the gateway 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.
Webhook delivery currently makes one synchronous HTTP attempt. 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 or get the verification session and credential offer 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.