Skip to content
Skip the search — install the Self skill Let your AI agent integrate Self for you.

Start typing to search the documentation.

Event catalog

Self delivers a single webhook event: verification.completed. Every registered endpoint receives it; there’s no per-event subscription.

verification.completed

Fires when off-chain verification finishes, either successfully or with a definitive failure. This is the event integrations care about.

Payload

{
  "type": "verification.completed",
  "verification_id": "7f3b2a1e-9c4d-4b2a-8e1f-2c6d5a4b3c2d",
  "external_uuid": "a1b2c3d4-5678-4e9a-b012-3456789abcde",
  "flow_id": "9c0b4f1c-1d6c-4f1b-a8c4-9f0fa0a8d9e2",
  "flow_version_id": "b1e2c3d4-5678-4abc-9def-0123456789ab",
  "environment": "live",
  "status": "valid",
  "proof_attributes": {
    "minimumAge": 18,
    "excludedCountries": ["PRK", "IRN"],
    "ofac": true,
    "nationality": "USA",
    "name": "JANE DOE"
  },
  "proof": { /* raw proof envelope for re-verification (see below) */ },
  "nullifier": "0x9b1c…",
  "verified_at": "2026-05-29T17:33:21.412Z",
  "storage_state": "pending",
  "storage_uri": null
}

Fields

FieldTypeNotes
verification_idstringStable, unique. Use as your dedup key.
external_uuidstringThe identifier you passed when creating the session.
flow_id / flow_version_idstringWhich flow + frozen version this verification ran against.
environment'test' | 'live'Matches the API key environment that created the session.
status'valid' | 'invalid' | 'error' | 'expired'See statuses below.
proof_attributesobjectThe flow’s resolved predicate config that was enforced, echoed back — the same keys you set in the dashboard: minimumAge, excludedCountries, includedCountries, ofac (only the ones your flow uses). These are not disclosed booleans; a valid status is what tells you every predicate passed. On a valid event this object also carries the disclosed reveal values for any Additional-data fields the flow requested — keyed name, idNumber, dateOfBirth, gender, nationality, expiryDate, issuingState — included only when the holder returned a non-empty value (predicate and reveal keys never collide). Non-valid statuses carry the predicate config only. Empty object if the flow had no predicates and requested no reveals.
proofobject | nullThe raw submitted proof envelope (attestationId, proof, publicSignals, userContextData) — the shape the SDK’s verifyProof(...) takes if you want to independently re-verify. Currently populated on every status, including invalid/error/expired (it’s whatever the holder submitted). Most integrations ignore it — Self already verified the proof.
nullifierstring | nullPrivacy-preserving uniqueness identifier derived from the document, stable per person within your organization (the same across all your products and flows, unlinkable across organizations). Use it for Sybil resistance / dedup. Present when status === 'valid', null otherwise. See nullifiers.
verified_atISO-8601When verification completed.
storage_state'pending' | 'committed' | 'failed'Always pending on this event — the completed event fires before async storage runs. For the final state, read the session with sessions.get(...).
storage_uristring | nullAlways null on this event. The committed URI, once available, is returned by sessions.get(...), not the webhook.

Statuses

  • valid: the proof verified and all flow predicates passed. This is the green-light case.
  • invalid: the proof verified but at least one predicate failed (e.g. user is 16, flow requires 18).
  • error: verification failed for a technical reason (unsupported document, malformed proof, signature mismatch). Not the user’s fault necessarily.
  • expired: session expired before completion.

The event carries the status category but no machine-readable failure reason — an invalid or error event tells you that it failed, not which predicate or which technical cause. If you need to distinguish cases (e.g. “underage” vs. “excluded country”), branch on the proof_attributes your flow enforced, or read the session with sessions.get(...).

Event ID (for deduplication)

The deduplication key is <verification_id>-completed (the same for every status, so a retry of this event reuses it).

Handling in TypeScript

The payload is typed as WebhookEvent; branch on event.type to narrow it. verification.completed is the event delivered:

import type { WebhookEvent } from '@selfxyz/enterprise-sdk';

function handle(event: WebhookEvent) {
  if (event.type === 'verification.completed') {
    // narrowed to VerificationCompletedPayload
    // event.verification_id, event.external_uuid, event.proof_attributes, event.status
  }
}

Forward compatibility

We may add fields to existing events without bumping major versions. The SDK’s webhookEvent schema uses .passthrough(), so unknown fields are preserved (and ignored by your existing code). We may also add new event types over time, so branch on event.type and let your handler ignore any type it doesn’t recognize.

Was this page helpful?