Assure verification API
Assure verifies a person over the channel they already use — interactive email, Apple Messages for Business, RCS or a passkey approval — and hands your backend one single-use result. Every call is plain HTTPS with a workspace API key; there is no vendor SDK to install.
- Your backend starts a verification request.
- Assure reaches the person and records what actually happened.
- Your backend spends the bound result once, then completes its own action once.
Sandbox: https://app.assure.chat/api/public/v1
Production: https://app.assure.chat/api/public/v1
Passkey step-up — The hosted authentication domain is not live yet.
Text-message delivery — The Twilio route is not activated in this environment.
Signed result webhooks — Signed customer callbacks arrive with the webhook stage.
Sandbox connectors, sends and events are simulations until real services are connected.
Recent changes
5 October 2026
- Phone notifications accept any identity provider (auth0, okta, entra_id, ping, other); only Auth0 is end-to-end tested.
- Errors identify the call with trace_id only; the old request_id copy has been removed.
- One error vocabulary for every endpoint, including the phone-notification codes.
- A rejected code (422) is one flat error object; remaining_attempts sits inside it.
- Spending an already-used result by request returns 409 with the original receipt.
- Action completions spell the outcome canceled; cancelled is no longer accepted.
- Each webhook event type now has its own data schema; the fields below come from it.
- Result claims, method evidence and receipt fields are typed; see Results.
Authentication and safety rules
Send your workspace key as Authorization: Bearer … from your own server. A key must never reach a browser. Each key carries only the permissions you grant it.
challenges:write — Create, verify, resend and cancel verification requests
results:read — Read request state and whether a result is outstanding
results:consume — Spend a verification result once
connections:manage — Manage provider connections for this workspace
notifications:send — Deliver externally generated phone notifications from an identity provider
notifications:read — Read masked delivery status of external notifications
- Calls that create, send or spend take an `Idempotency-Key` header: creating a request or code, resend, the three consume calls, action completions and phone notifications. Verify, cancel and report do not. A repeat with the same key returns the first stored response.
- Reusing a key with a different request body is refused with 409 `idempotency_conflict`; nothing new is created.
- A result is single-use and bound to the application, subject, session, transaction and, for action approvals, the intent it was issued for. A call that does not match is refused.
- Spending is atomic: the first consume wins. A later attempt without the original key returns 409 `conflict` with the original receipt; a repeat with the same key returns the receipt with `replayed: true`. Neither authorises anything again.
- Responses never return a code, result token or other secret value a second time.
- Sign-in token signing keys (this JWKS) have no scheduled rotation today: one key per environment is published and stays until replaced.
- Always resolve the key by the token's `kid`. Never pin a single key by value, so a future rotation needs no change on your side.
- Refetch the JWKS when you see an unknown `kid` before rejecting a token.
- Webhook signing secrets are a separate key system: after a rotation the previous secret stays valid for 24 hours.
Start a verification
Ask Assure to reach the person. A code is never in the response.
/api/public/v1/otp/emailExample request body
{
"email": "person@example.com",
"subject_ref": "user_1042",
"purpose": "email_confirmation",
"application_ref": "checkout",
"transaction_ref": "order_5567"
}Response fields
request_id · status · expires_at · delivery_status · channel · destination_hint · simulated
- Email confirmation proves control of an address. It is not multi-factor authentication.
/api/public/v1/otp/messagingExample request body
{
"phone": "+15555550123",
"subject_ref": "user_1042",
"purpose": "phone_verification",
"method_preference": "apple_first"
}Response fields
request_id · status · expires_at · delivery_status · channel · destination_hint · simulated
- rcs_only skips Apple Messages and exists for testing and demos.
- With purpose step_up the destination comes from the subject's confirmed contact on the server; a phone in the body is ignored.
- method_preference passkey is accepted and refused with not_available_yet.
/api/public/v1/verification-requestsExample request body
{
"channel": "email",
"purpose": "email_confirmation",
"subject_ref": "user_1042",
"email": "person@example.com"
}Response fields
request_id · status · expires_at · delivery_status · channel · destination_hint · simulated
- channel sms and webhook_url are accepted and refused with not_available_yet.
Follow and control a request
Read state, submit an answer, resend, cancel, or record a report.
/api/public/v1/verification-requests/{requestId}Response fields
status · purpose · expires_at · verified_at · consumed_at · channel · delivery_status · destination_hint · result_available · simulated · flagged_for_review · invitation_surface · invitation_path
/api/public/v1/verification-requests/{requestId}/verifyExample request body
{
"code": "123456"
}Response fields
request_id · status · result_token · result_expires_at · already_verified · idempotent (deprecated)
- A wrong code returns the same neutral refusal whatever the reason, with the remaining attempts.
- Call this from your backend. Never place a server key in a browser or in email markup.
/api/public/v1/verification-requests/{requestId}/resendResponse fields
request_id · status · expires_at · delivery_status · channel
/api/public/v1/verification-requests/{requestId}/cancelResponse fields
request_id · status
/api/public/v1/verification-requests/{requestId}/reportResponse fields
request_id · report_kind · canceled · duplicate
- Never locks an account, removes a passkey or reverses an action you already took.
Results
Read the outcome, then spend the single grant exactly once from your backend.
/api/public/v1/verification-requests/{requestId}/resultResponse fields
status · purpose · verified_at · consumed_at · result_available · simulated
/api/public/v1/results/{token}/consumePuts the token in the URL. Use POST /results/consume, which takes it in the body.
Example request body
{
"subject_ref": "user_1042",
"transaction_ref": "order_5567"
}Response fields
request_id · subject_ref · status · claims · receipt · replayed
- The claims describe the evidence collected. There is no unconditional MFA flag.
- claims always has purpose, method and simulated. Passkey results may also have user_verified and auth_time. Other keys may appear.
- receipt always has all ten keys (receipt_ref, step_kind, method_evidence, policy_id, policy_version, requested_assurance, evaluated_assurance, assurance_reason_codes, verified_at, consumed_at); some values may be null.
- method_evidence is {} when nothing was recorded. Otherwise it holds observed_method, user_verified, user_present and proof_provenance, and may hold issuer. For user_verified and user_present, null means not observed and false means observed false.
/api/public/v1/verification-requests/{requestId}/result/consumeExample request body
{
"subject_ref": "user_1042",
"transaction_ref": "order_5567"
}Response fields
request_id · subject_ref · status · claims · receipt · replayed
- The browser never holds this authorisation; your backend calls it with its own key.
- A second attempt is refused and returns the first receipt, so nothing is ever authorised twice.
- A passkey that has since been removed is refused, replay included.
- A second attempt returns 409 conflict with the original receipt next to the error.
/api/public/v1/results/consumeExample request body
{
"result_token": "RESULT_TOKEN",
"subject_ref": "user_1042",
"transaction_ref": "order_5567"
}Response fields
request_id · subject_ref · status · claims · receipt · replayed
- Repeating the same Idempotency-Key returns the first receipt again with replayed: true; nothing is spent twice.
Completed actions
Tell Assure that your own business action finished, linked to the result you spent.
/api/public/v1/actions/completionsExample request body
{
"receipt_ref": "rcp_8f2a91c04d",
"action_ref": "order_5567",
"outcome": "succeeded",
"customer_event_id": "evt_10442",
"occurred_at": "2026-10-05T02:41:00Z"
}Response fields
report_id · recorded · duplicate · notice_queued · provenance
- Send exactly one of result_token or receipt_ref. A receipt_ref looks like rcp_ followed by 8–64 letters, digits, - or _.
- outcome is succeeded, failed, canceled or pending.
- Only succeeded may cause a completion notice. The same Idempotency-Key with a different body is refused with 409.
Phone notifications (identity providers)
Delivery only. Your identity provider (for example Auth0) owns the code and the sign-in result; Assure only delivers the message.
/api/public/v1/notifications/phoneResponse fields
notification_id · profile_ref · environment · status · simulated · replayed · created_at · updated_at · delivery_deadline · destination_hint · delivery_channel
- Text only: voice returns 422 unsupported_delivery_method.
- Production sending is still simulated, so a production live-only request returns 503 live_delivery_unavailable.
/api/public/v1/notifications/phone/{notificationId}Response fields
notification_id · profile_ref · environment · status · simulated · replayed · created_at · updated_at · delivery_deadline · destination_hint · delivery_channel · delivered_at · failure
Sign-in routes
Used by Apple Messages sign-in and passkey approval, not called from your backend directly. Listed for completeness; see the full specification for details.
/oauth/authorizeOpen the Apple Messages sign-in session/oauth/authorizeContinue a sign-in session from the refusal or start page/oauth/cancelShow the stop page for one sign-in session/oauth/cancelStop one sign-in session/oauth/v1/authorizeStart an authorization for a registered application/oauth/v1/tokenExchange an authorization code, or renew access/oauth/v1/revokeRevoke a renewal token/oauth/v1/introspectInspect a renewal token/oauth/v1/jwksPublic signing keys/oauth/v1/discoveryDiscovery metadata for Assure's own OAuth serviceErrors
Errors are neutral by design: nothing in a response reveals whether an account, address or number exists. Every endpoint uses the same body.
{
"error": {
"code": "verification_failed",
"message": "That code is not valid. Request a new one if you need to.",
"trace_id": "req_9f2c1a77d0e34b8a5c61",
"retryable": false,
"remaining_attempts": 2
}
}trace_ididentifies this call. Quote it to support.retryableis true only for rate_limited, unavailable and destination_paused.- A rejected code (422 verification_failed) may include
remaining_attemptsin the same object. - Spending an already-used result by request returns 409 conflict with the original
receiptnext toerror. It never authorises anything again. - New codes may be added. Treat an unknown code as a non-retryable failure.
| Code | Status | Meaning |
|---|---|---|
| invalid_request | 400 | The body or a field is not acceptable. |
| unauthorized | 401 | Missing, unknown or revoked API key. |
| forbidden | 403 | Scope denied, or the record belongs to another caller. |
| destination_paused | 429 | New codes to this number are paused for 15 minutes after its owner tapped Not you?. Retry after the Retry-After header. |
| not_found | 404 | No such request, or no confirmed contact for the subject. |
| conflict | 409 | Idempotency or state conflict, or an attempt limit. |
| expired | 410 | The request, code or result has expired. |
| verification_failed | 422 | The code was not accepted. The wording is neutral. |
| rate_limited | 429 | Too many requests. Honour Retry-After. |
| unavailable | 503 | Temporarily unavailable. Retryable. |
| not_available_yet | 503 | Accepted in shape, but this environment cannot serve it. |
| intent_mismatch | 409 | The action you are spending the result for is not the one the person approved. |
| summary_missing | 400 | An action approval needs its masked summary. |
| idempotency_conflict notifications | 409 | The same Idempotency-Key was sent with a different body. |
| unsupported_delivery_method notifications | 422 | Only text delivery is supported; voice is refused. |
| no_eligible_channel notifications | 422 | No channel in the profile can reach this number. |
| content_not_supported notifications | 422 | The message content cannot be sent on the available channels. |
| live_delivery_unavailable notifications | 503 | Live sending is not enabled here. Not retryable. |
| undeliverable notifications | — | Appears only in a notification's failure field: the provider reported it could not deliver the message. |
Webhooks into Assure
Assure receives provider callbacks on its own endpoints. Each signature is verified over the exact bytes received before anything is parsed, and delivery, seen, click and redirect events never establish a successful authentication.
Signed event payload, verified over the raw bytes before anything is parsed.
Delivery, open and click events describe the message, never the person.
Per-connection signed callback, verified over the raw bytes.
Conversation control proves phone control only with documented number binding.
HMAC-SHA256 over the raw bytes using the per-connection secret, on the configured header (X-Hub-Signature by default).
A missing, invalid or ambiguous signature is refused. There is no alternate-header fallback.
X-Hub-Signature-256 over the raw bytes, plus the subscription challenge on setup.
Events from an unknown business account are quarantined, not processed.
Webhooks Assure sends to you
An event only announces that something happened. It never carries a code, a result token or a full destination, and it never authorises anything: your backend still spends the bound result through the API. Schema version 1, timestamps accepted within 300 seconds.
Assure-Webhook-Id — unique delivery identifier, for deduplication
Assure-Webhook-Timestamp — when it was signed
Assure-Webhook-Signature — signature over the raw body
Assure-Webhook-Key-Id — which signing secret was used
- Verify the signature over the exact bytes you received, before parsing.
- Refuse a timestamp more than 300 seconds old, so an old delivery cannot be replayed.
- Store Assure-Webhook-Id and ignore an identifier you have already handled: a retry is normal.
- Treat the event as an announcement only. Spend the result through the API before you act.
- Every event is saved in the same step as the change it announces, so a change never happens without its event.
- If an event never arrives, ask Assure for the request's state instead of waiting. Nothing is lost.
- During a secret rotation, accept either key until the old one is retired.
verification.createdData fields (VerificationEventData)
Data for verification.* and consent.revoked. Every emitter of these types attaches a request, so the request fields are always present and set. Stored examples exist for created, verified, expired, consumed, canceled and denied; delivered, failed, security_reported and consent.revoked are derived from the emitter code only (no stored examples).
request_id: stringsubject_ref: string — Your own reference for the person.transaction_ref: string | nullpurpose: string (known values, may grow: phone_verification, email_confirmation, email_phone_enrollment, step_up, password_recovery, account_password_reset, shipping_address_change)status: string — The request's state when the event was published, not necessarily the state named by the event (for example a late-published verification.created can carry verified). Use the event type to know what happened. A future version will make status match the event; until then, do not branch on status alone.channel: string | nulldelivery_channel: string | nullauthentication_method: string | nullsignals: object (EventSignals) — Indicators about the exchange, copied onto an event when present. An event authorises nothing, so these are signals, not claims; only a consumed result (ResultClaims) is a statement you act on. Today the only signal is reason_code, so expect {} or a reason_code alone. New keys may be added.assurance: object (EventAssurance) — The request's assurance decision as stored. Both keys are always emitted; with no request, decision is null and reason_codes is empty.simulated: booleanreason(optional): string | null (known values, may grow: amb_declined_by_subject, attempts_exhausted) — Why a verification.failed request ended. Null on every other verification and consent event.
Ordering: Rises per verification request. Arrival order is not guaranteed.
What to do: Nothing yet.
verification.deliveredData fields (VerificationEventData)
Data for verification.* and consent.revoked. Every emitter of these types attaches a request, so the request fields are always present and set. Stored examples exist for created, verified, expired, consumed, canceled and denied; delivered, failed, security_reported and consent.revoked are derived from the emitter code only (no stored examples).
request_id: stringsubject_ref: string — Your own reference for the person.transaction_ref: string | nullpurpose: string (known values, may grow: phone_verification, email_confirmation, email_phone_enrollment, step_up, password_recovery, account_password_reset, shipping_address_change)status: string — The request's state when the event was published, not necessarily the state named by the event (for example a late-published verification.created can carry verified). Use the event type to know what happened. A future version will make status match the event; until then, do not branch on status alone.channel: string | nulldelivery_channel: string | nullauthentication_method: string | nullsignals: object (EventSignals) — Indicators about the exchange, copied onto an event when present. An event authorises nothing, so these are signals, not claims; only a consumed result (ResultClaims) is a statement you act on. Today the only signal is reason_code, so expect {} or a reason_code alone. New keys may be added.assurance: object (EventAssurance) — The request's assurance decision as stored. Both keys are always emitted; with no request, decision is null and reason_codes is empty.simulated: booleanreason(optional): string | null (known values, may grow: amb_declined_by_subject, attempts_exhausted) — Why a verification.failed request ended. Null on every other verification and consent event.
Ordering: Rises per verification request. Arrival order is not guaranteed.
What to do: Nothing. Delivery, opening and clicks never mean the person answered.
verification.verifiedData fields (VerificationEventData)
Data for verification.* and consent.revoked. Every emitter of these types attaches a request, so the request fields are always present and set. Stored examples exist for created, verified, expired, consumed, canceled and denied; delivered, failed, security_reported and consent.revoked are derived from the emitter code only (no stored examples).
request_id: stringsubject_ref: string — Your own reference for the person.transaction_ref: string | nullpurpose: string (known values, may grow: phone_verification, email_confirmation, email_phone_enrollment, step_up, password_recovery, account_password_reset, shipping_address_change)status: string — The request's state when the event was published, not necessarily the state named by the event (for example a late-published verification.created can carry verified). Use the event type to know what happened. A future version will make status match the event; until then, do not branch on status alone.channel: string | nulldelivery_channel: string | nullauthentication_method: string | nullsignals: object (EventSignals) — Indicators about the exchange, copied onto an event when present. An event authorises nothing, so these are signals, not claims; only a consumed result (ResultClaims) is a statement you act on. Today the only signal is reason_code, so expect {} or a reason_code alone. New keys may be added.assurance: object (EventAssurance) — The request's assurance decision as stored. Both keys are always emitted; with no request, decision is null and reason_codes is empty.simulated: booleanreason(optional): string | null (known values, may grow: amb_declined_by_subject, attempts_exhausted) — Why a verification.failed request ended. Null on every other verification and consent event.
Ordering: Rises per verification request. Arrival order is not guaranteed.
What to do: Spend the bound result through the API, then complete your own action once.
verification.failedData fields (VerificationEventData)
Data for verification.* and consent.revoked. Every emitter of these types attaches a request, so the request fields are always present and set. Stored examples exist for created, verified, expired, consumed, canceled and denied; delivered, failed, security_reported and consent.revoked are derived from the emitter code only (no stored examples).
request_id: stringsubject_ref: string — Your own reference for the person.transaction_ref: string | nullpurpose: string (known values, may grow: phone_verification, email_confirmation, email_phone_enrollment, step_up, password_recovery, account_password_reset, shipping_address_change)status: string — The request's state when the event was published, not necessarily the state named by the event (for example a late-published verification.created can carry verified). Use the event type to know what happened. A future version will make status match the event; until then, do not branch on status alone.channel: string | nulldelivery_channel: string | nullauthentication_method: string | nullsignals: object (EventSignals) — Indicators about the exchange, copied onto an event when present. An event authorises nothing, so these are signals, not claims; only a consumed result (ResultClaims) is a statement you act on. Today the only signal is reason_code, so expect {} or a reason_code alone. New keys may be added.assurance: object (EventAssurance) — The request's assurance decision as stored. Both keys are always emitted; with no request, decision is null and reason_codes is empty.simulated: booleanreason(optional): string | null (known values, may grow: amb_declined_by_subject, attempts_exhausted) — Why a verification.failed request ended. Null on every other verification and consent event.
Ordering: Rises per verification request. Arrival order is not guaranteed.
What to do: Nothing.
verification.expiredData fields (VerificationEventData)
Data for verification.* and consent.revoked. Every emitter of these types attaches a request, so the request fields are always present and set. Stored examples exist for created, verified, expired, consumed, canceled and denied; delivered, failed, security_reported and consent.revoked are derived from the emitter code only (no stored examples).
request_id: stringsubject_ref: string — Your own reference for the person.transaction_ref: string | nullpurpose: string (known values, may grow: phone_verification, email_confirmation, email_phone_enrollment, step_up, password_recovery, account_password_reset, shipping_address_change)status: string — The request's state when the event was published, not necessarily the state named by the event (for example a late-published verification.created can carry verified). Use the event type to know what happened. A future version will make status match the event; until then, do not branch on status alone.channel: string | nulldelivery_channel: string | nullauthentication_method: string | nullsignals: object (EventSignals) — Indicators about the exchange, copied onto an event when present. An event authorises nothing, so these are signals, not claims; only a consumed result (ResultClaims) is a statement you act on. Today the only signal is reason_code, so expect {} or a reason_code alone. New keys may be added.assurance: object (EventAssurance) — The request's assurance decision as stored. Both keys are always emitted; with no request, decision is null and reason_codes is empty.simulated: booleanreason(optional): string | null (known values, may grow: amb_declined_by_subject, attempts_exhausted) — Why a verification.failed request ended. Null on every other verification and consent event.
Ordering: Rises per verification request. Arrival order is not guaranteed.
What to do: Nothing.
verification.canceledData fields (VerificationEventData)
Data for verification.* and consent.revoked. Every emitter of these types attaches a request, so the request fields are always present and set. Stored examples exist for created, verified, expired, consumed, canceled and denied; delivered, failed, security_reported and consent.revoked are derived from the emitter code only (no stored examples).
request_id: stringsubject_ref: string — Your own reference for the person.transaction_ref: string | nullpurpose: string (known values, may grow: phone_verification, email_confirmation, email_phone_enrollment, step_up, password_recovery, account_password_reset, shipping_address_change)status: string — The request's state when the event was published, not necessarily the state named by the event (for example a late-published verification.created can carry verified). Use the event type to know what happened. A future version will make status match the event; until then, do not branch on status alone.channel: string | nulldelivery_channel: string | nullauthentication_method: string | nullsignals: object (EventSignals) — Indicators about the exchange, copied onto an event when present. An event authorises nothing, so these are signals, not claims; only a consumed result (ResultClaims) is a statement you act on. Today the only signal is reason_code, so expect {} or a reason_code alone. New keys may be added.assurance: object (EventAssurance) — The request's assurance decision as stored. Both keys are always emitted; with no request, decision is null and reason_codes is empty.simulated: booleanreason(optional): string | null (known values, may grow: amb_declined_by_subject, attempts_exhausted) — Why a verification.failed request ended. Null on every other verification and consent event.
Ordering: Rises per verification request. Arrival order is not guaranteed.
What to do: Stop waiting.
verification.deniedData fields (VerificationEventData)
Data for verification.* and consent.revoked. Every emitter of these types attaches a request, so the request fields are always present and set. Stored examples exist for created, verified, expired, consumed, canceled and denied; delivered, failed, security_reported and consent.revoked are derived from the emitter code only (no stored examples).
request_id: stringsubject_ref: string — Your own reference for the person.transaction_ref: string | nullpurpose: string (known values, may grow: phone_verification, email_confirmation, email_phone_enrollment, step_up, password_recovery, account_password_reset, shipping_address_change)status: string — The request's state when the event was published, not necessarily the state named by the event (for example a late-published verification.created can carry verified). Use the event type to know what happened. A future version will make status match the event; until then, do not branch on status alone.channel: string | nulldelivery_channel: string | nullauthentication_method: string | nullsignals: object (EventSignals) — Indicators about the exchange, copied onto an event when present. An event authorises nothing, so these are signals, not claims; only a consumed result (ResultClaims) is a statement you act on. Today the only signal is reason_code, so expect {} or a reason_code alone. New keys may be added.assurance: object (EventAssurance) — The request's assurance decision as stored. Both keys are always emitted; with no request, decision is null and reason_codes is empty.simulated: booleanreason(optional): string | null (known values, may grow: amb_declined_by_subject, attempts_exhausted) — Why a verification.failed request ended. Null on every other verification and consent event.
Ordering: Rises per verification request. Arrival order is not guaranteed.
What to do: Stop waiting. The person said it was not theirs; it identifies nobody and locks nothing.
verification.security_reportedData fields (VerificationEventData)
Data for verification.* and consent.revoked. Every emitter of these types attaches a request, so the request fields are always present and set. Stored examples exist for created, verified, expired, consumed, canceled and denied; delivered, failed, security_reported and consent.revoked are derived from the emitter code only (no stored examples).
request_id: stringsubject_ref: string — Your own reference for the person.transaction_ref: string | nullpurpose: string (known values, may grow: phone_verification, email_confirmation, email_phone_enrollment, step_up, password_recovery, account_password_reset, shipping_address_change)status: string — The request's state when the event was published, not necessarily the state named by the event (for example a late-published verification.created can carry verified). Use the event type to know what happened. A future version will make status match the event; until then, do not branch on status alone.channel: string | nulldelivery_channel: string | nullauthentication_method: string | nullsignals: object (EventSignals) — Indicators about the exchange, copied onto an event when present. An event authorises nothing, so these are signals, not claims; only a consumed result (ResultClaims) is a statement you act on. Today the only signal is reason_code, so expect {} or a reason_code alone. New keys may be added.assurance: object (EventAssurance) — The request's assurance decision as stored. Both keys are always emitted; with no request, decision is null and reason_codes is empty.simulated: booleanreason(optional): string | null (known values, may grow: amb_declined_by_subject, attempts_exhausted) — Why a verification.failed request ended. Null on every other verification and consent event.
Ordering: Rises per verification request. Arrival order is not guaranteed.
What to do: Review. The earlier result still stands and nothing was reversed for you.
verification.consumedData fields (VerificationEventData)
Data for verification.* and consent.revoked. Every emitter of these types attaches a request, so the request fields are always present and set. Stored examples exist for created, verified, expired, consumed, canceled and denied; delivered, failed, security_reported and consent.revoked are derived from the emitter code only (no stored examples).
request_id: stringsubject_ref: string — Your own reference for the person.transaction_ref: string | nullpurpose: string (known values, may grow: phone_verification, email_confirmation, email_phone_enrollment, step_up, password_recovery, account_password_reset, shipping_address_change)status: string — The request's state when the event was published, not necessarily the state named by the event (for example a late-published verification.created can carry verified). Use the event type to know what happened. A future version will make status match the event; until then, do not branch on status alone.channel: string | nulldelivery_channel: string | nullauthentication_method: string | nullsignals: object (EventSignals) — Indicators about the exchange, copied onto an event when present. An event authorises nothing, so these are signals, not claims; only a consumed result (ResultClaims) is a statement you act on. Today the only signal is reason_code, so expect {} or a reason_code alone. New keys may be added.assurance: object (EventAssurance) — The request's assurance decision as stored. Both keys are always emitted; with no request, decision is null and reason_codes is empty.simulated: booleanreason(optional): string | null (known values, may grow: amb_declined_by_subject, attempts_exhausted) — Why a verification.failed request ended. Null on every other verification and consent event.
Ordering: Rises per verification request. Arrival order is not guaranteed.
What to do: Reconcile. The result cannot be spent again.
credential.enrolledData fields (CredentialEventData)
Data for credential.enrolled, credential.revoked and passkey.enrolled_during_sign_in. These events carry no request, so the request fields are null; the account and passkey are identified from the event itself. Derived from the emitter code only (no stored examples).
request_id: nullsubject_ref: string | null — Your reference for the account. Set on credential.enrolled and credential.revoked; null on passkey.enrolled_during_sign_in.subject_hint(optional): string | null — Masked account reference. passkey.enrolled_during_sign_in only.credential_reference(optional): string | null — Last six characters of the passkey's credential ID. passkey.enrolled_during_sign_in only.operation(optional): string | null (known values, may grow: first_credential, add_credential, recovery_reenrollment) — credential.enrolled only. What kind of enrollment this was.bootstrap_strength(optional): string | null (known values, may grow: phishing_resistant, possession_of_channel, operator_authorized, unverified) — credential.enrolled only. The evidence that authorized the enrollment.transaction_ref: nullpurpose: nullstatus(optional): string | nullchannel(optional): string | nulldelivery_channel(optional): string | nullauthentication_method(optional): string | nullsignals: object (EventSignals) — Indicators about the exchange, copied onto an event when present. An event authorises nothing, so these are signals, not claims; only a consumed result (ResultClaims) is a statement you act on. Today the only signal is reason_code, so expect {} or a reason_code alone. New keys may be added.assurance: object (EventAssurance) — The request's assurance decision as stored. Both keys are always emitted; with no request, decision is null and reason_codes is empty.simulated: boolean
Ordering: Rises per subject. It is not tied to any one verification request.
What to do: Record that a passkey exists. It claims nothing about the device or a biometric.
passkey.enrolled_during_sign_inData fields (CredentialEventData)
Data for credential.enrolled, credential.revoked and passkey.enrolled_during_sign_in. These events carry no request, so the request fields are null; the account and passkey are identified from the event itself. Derived from the emitter code only (no stored examples).
request_id: nullsubject_ref: string | null — Your reference for the account. Set on credential.enrolled and credential.revoked; null on passkey.enrolled_during_sign_in.subject_hint(optional): string | null — Masked account reference. passkey.enrolled_during_sign_in only.credential_reference(optional): string | null — Last six characters of the passkey's credential ID. passkey.enrolled_during_sign_in only.operation(optional): string | null (known values, may grow: first_credential, add_credential, recovery_reenrollment) — credential.enrolled only. What kind of enrollment this was.bootstrap_strength(optional): string | null (known values, may grow: phishing_resistant, possession_of_channel, operator_authorized, unverified) — credential.enrolled only. The evidence that authorized the enrollment.transaction_ref: nullpurpose: nullstatus(optional): string | nullchannel(optional): string | nulldelivery_channel(optional): string | nullauthentication_method(optional): string | nullsignals: object (EventSignals) — Indicators about the exchange, copied onto an event when present. An event authorises nothing, so these are signals, not claims; only a consumed result (ResultClaims) is a statement you act on. Today the only signal is reason_code, so expect {} or a reason_code alone. New keys may be added.assurance: object (EventAssurance) — The request's assurance decision as stored. Both keys are always emitted; with no request, decision is null and reason_codes is empty.simulated: boolean
Ordering: Rises per subject. It is not tied to any one verification request.
What to do: Record that a passkey now exists. It is not an approval: the request still has to be answered with that passkey.
credential.revokedData fields (CredentialEventData)
Data for credential.enrolled, credential.revoked and passkey.enrolled_during_sign_in. These events carry no request, so the request fields are null; the account and passkey are identified from the event itself. Derived from the emitter code only (no stored examples).
request_id: nullsubject_ref: string | null — Your reference for the account. Set on credential.enrolled and credential.revoked; null on passkey.enrolled_during_sign_in.subject_hint(optional): string | null — Masked account reference. passkey.enrolled_during_sign_in only.credential_reference(optional): string | null — Last six characters of the passkey's credential ID. passkey.enrolled_during_sign_in only.operation(optional): string | null (known values, may grow: first_credential, add_credential, recovery_reenrollment) — credential.enrolled only. What kind of enrollment this was.bootstrap_strength(optional): string | null (known values, may grow: phishing_resistant, possession_of_channel, operator_authorized, unverified) — credential.enrolled only. The evidence that authorized the enrollment.transaction_ref: nullpurpose: nullstatus(optional): string | nullchannel(optional): string | nulldelivery_channel(optional): string | nullauthentication_method(optional): string | nullsignals: object (EventSignals) — Indicators about the exchange, copied onto an event when present. An event authorises nothing, so these are signals, not claims; only a consumed result (ResultClaims) is a statement you act on. Today the only signal is reason_code, so expect {} or a reason_code alone. New keys may be added.assurance: object (EventAssurance) — The request's assurance decision as stored. Both keys are always emitted; with no request, decision is null and reason_codes is empty.simulated: boolean
Ordering: Rises per subject. It is not tied to any one verification request.
What to do: Stop treating that passkey as usable. Nothing it already proved changes.
consent.revokedData fields (VerificationEventData)
Data for verification.* and consent.revoked. Every emitter of these types attaches a request, so the request fields are always present and set. Stored examples exist for created, verified, expired, consumed, canceled and denied; delivered, failed, security_reported and consent.revoked are derived from the emitter code only (no stored examples).
request_id: stringsubject_ref: string — Your own reference for the person.transaction_ref: string | nullpurpose: string (known values, may grow: phone_verification, email_confirmation, email_phone_enrollment, step_up, password_recovery, account_password_reset, shipping_address_change)status: string — The request's state when the event was published, not necessarily the state named by the event (for example a late-published verification.created can carry verified). Use the event type to know what happened. A future version will make status match the event; until then, do not branch on status alone.channel: string | nulldelivery_channel: string | nullauthentication_method: string | nullsignals: object (EventSignals) — Indicators about the exchange, copied onto an event when present. An event authorises nothing, so these are signals, not claims; only a consumed result (ResultClaims) is a statement you act on. Today the only signal is reason_code, so expect {} or a reason_code alone. New keys may be added.assurance: object (EventAssurance) — The request's assurance decision as stored. Both keys are always emitted; with no request, decision is null and reason_codes is empty.simulated: booleanreason(optional): string | null (known values, may grow: amb_declined_by_subject, attempts_exhausted) — Why a verification.failed request ended. Null on every other verification and consent event.
Ordering: Rises per destination, not per request. A consent event may belong to no verification at all, so never order it against one.
What to do: Stop sending to that destination.
notification.acceptedData fields (NotificationEventData)
Data for notification.* events. These have no verification request by design. The request fields are still emitted, always null; do not read them.
request_id: null — Always null on notification events and will be removed in a future version.subject_ref: null — Always null on notification events and will be removed in a future version.transaction_ref: null — Always null on notification events and will be removed in a future version.purpose: null — Always null on notification events and will be removed in a future version.status: string (known values, may grow: accepted, sent, delivered, failed)channel(optional): string | nulldelivery_channel(optional): string | nullauthentication_method(optional): string | nullnotification: object (NotificationDetail) — Present only on notification.* events that carry a payload, so it may be absent even on those. Delivery facts only; never an authentication result.notification.notification_id(optional): string — The notification's id. An empty string when the source omitted it.notification.destination_hint(optional): string | null — Masked destination. Never the full number.notification.delivery_channel(optional): string | null (known values, may grow: amb, rcs) — Known values, not exhaustive: amb, rcs. These are short provider names and differ from NotificationReceipt.delivery_channel's spellings (e.g. apple_messages_for_business), so do not compare the two directly. Null on accepted events.notification.status(optional): string (known values, may grow: accepted, sent, delivered, failed) — Known values, not exhaustive: accepted, sent, delivered, failed. Falls back to the event-name suffix, so it is not NotificationReceipt.status.notification.failure_code(optional): string | null (known values, may grow: expired, no_eligible_channel, unavailable, ambiguous_provider_outcome, undeliverable) — Known values, not exhaustive: expired, no_eligible_channel, unavailable, ambiguous_provider_outcome, undeliverable.notification.occurred_at(optional): string | nullsignals: object (EventSignals) — Indicators about the exchange, copied onto an event when present. An event authorises nothing, so these are signals, not claims; only a consumed result (ResultClaims) is a statement you act on. Today the only signal is reason_code, so expect {} or a reason_code alone. New keys may be added.assurance: object (EventAssurance) — The request's assurance decision as stored. Both keys are always emitted; with no request, decision is null and reason_codes is empty.simulated: boolean
Ordering: 1 accepted, 2 sent, 3 delivered or failed, per notification. Arrival order is not guaranteed.
What to do: Nothing. Auth0 (or your IdP) still owns the code and the sign-in result.
notification.sentData fields (NotificationEventData)
Data for notification.* events. These have no verification request by design. The request fields are still emitted, always null; do not read them.
request_id: null — Always null on notification events and will be removed in a future version.subject_ref: null — Always null on notification events and will be removed in a future version.transaction_ref: null — Always null on notification events and will be removed in a future version.purpose: null — Always null on notification events and will be removed in a future version.status: string (known values, may grow: accepted, sent, delivered, failed)channel(optional): string | nulldelivery_channel(optional): string | nullauthentication_method(optional): string | nullnotification: object (NotificationDetail) — Present only on notification.* events that carry a payload, so it may be absent even on those. Delivery facts only; never an authentication result.notification.notification_id(optional): string — The notification's id. An empty string when the source omitted it.notification.destination_hint(optional): string | null — Masked destination. Never the full number.notification.delivery_channel(optional): string | null (known values, may grow: amb, rcs) — Known values, not exhaustive: amb, rcs. These are short provider names and differ from NotificationReceipt.delivery_channel's spellings (e.g. apple_messages_for_business), so do not compare the two directly. Null on accepted events.notification.status(optional): string (known values, may grow: accepted, sent, delivered, failed) — Known values, not exhaustive: accepted, sent, delivered, failed. Falls back to the event-name suffix, so it is not NotificationReceipt.status.notification.failure_code(optional): string | null (known values, may grow: expired, no_eligible_channel, unavailable, ambiguous_provider_outcome, undeliverable) — Known values, not exhaustive: expired, no_eligible_channel, unavailable, ambiguous_provider_outcome, undeliverable.notification.occurred_at(optional): string | nullsignals: object (EventSignals) — Indicators about the exchange, copied onto an event when present. An event authorises nothing, so these are signals, not claims; only a consumed result (ResultClaims) is a statement you act on. Today the only signal is reason_code, so expect {} or a reason_code alone. New keys may be added.assurance: object (EventAssurance) — The request's assurance decision as stored. Both keys are always emitted; with no request, decision is null and reason_codes is empty.simulated: boolean
Ordering: 1 accepted, 2 sent, 3 delivered or failed, per notification. Arrival order is not guaranteed.
What to do: Nothing. A provider accepting the message is not delivery.
notification.deliveredData fields (NotificationEventData)
Data for notification.* events. These have no verification request by design. The request fields are still emitted, always null; do not read them.
request_id: null — Always null on notification events and will be removed in a future version.subject_ref: null — Always null on notification events and will be removed in a future version.transaction_ref: null — Always null on notification events and will be removed in a future version.purpose: null — Always null on notification events and will be removed in a future version.status: string (known values, may grow: accepted, sent, delivered, failed)channel(optional): string | nulldelivery_channel(optional): string | nullauthentication_method(optional): string | nullnotification: object (NotificationDetail) — Present only on notification.* events that carry a payload, so it may be absent even on those. Delivery facts only; never an authentication result.notification.notification_id(optional): string — The notification's id. An empty string when the source omitted it.notification.destination_hint(optional): string | null — Masked destination. Never the full number.notification.delivery_channel(optional): string | null (known values, may grow: amb, rcs) — Known values, not exhaustive: amb, rcs. These are short provider names and differ from NotificationReceipt.delivery_channel's spellings (e.g. apple_messages_for_business), so do not compare the two directly. Null on accepted events.notification.status(optional): string (known values, may grow: accepted, sent, delivered, failed) — Known values, not exhaustive: accepted, sent, delivered, failed. Falls back to the event-name suffix, so it is not NotificationReceipt.status.notification.failure_code(optional): string | null (known values, may grow: expired, no_eligible_channel, unavailable, ambiguous_provider_outcome, undeliverable) — Known values, not exhaustive: expired, no_eligible_channel, unavailable, ambiguous_provider_outcome, undeliverable.notification.occurred_at(optional): string | nullsignals: object (EventSignals) — Indicators about the exchange, copied onto an event when present. An event authorises nothing, so these are signals, not claims; only a consumed result (ResultClaims) is a statement you act on. Today the only signal is reason_code, so expect {} or a reason_code alone. New keys may be added.assurance: object (EventAssurance) — The request's assurance decision as stored. Both keys are always emitted; with no request, decision is null and reason_codes is empty.simulated: boolean
Ordering: 1 accepted, 2 sent, 3 delivered or failed, per notification. Arrival order is not guaranteed.
What to do: Nothing. Delivery never means the person read the message or signed in.
notification.failedData fields (NotificationEventData)
Data for notification.* events. These have no verification request by design. The request fields are still emitted, always null; do not read them.
request_id: null — Always null on notification events and will be removed in a future version.subject_ref: null — Always null on notification events and will be removed in a future version.transaction_ref: null — Always null on notification events and will be removed in a future version.purpose: null — Always null on notification events and will be removed in a future version.status: string (known values, may grow: accepted, sent, delivered, failed)channel(optional): string | nulldelivery_channel(optional): string | nullauthentication_method(optional): string | nullnotification: object (NotificationDetail) — Present only on notification.* events that carry a payload, so it may be absent even on those. Delivery facts only; never an authentication result.notification.notification_id(optional): string — The notification's id. An empty string when the source omitted it.notification.destination_hint(optional): string | null — Masked destination. Never the full number.notification.delivery_channel(optional): string | null (known values, may grow: amb, rcs) — Known values, not exhaustive: amb, rcs. These are short provider names and differ from NotificationReceipt.delivery_channel's spellings (e.g. apple_messages_for_business), so do not compare the two directly. Null on accepted events.notification.status(optional): string (known values, may grow: accepted, sent, delivered, failed) — Known values, not exhaustive: accepted, sent, delivered, failed. Falls back to the event-name suffix, so it is not NotificationReceipt.status.notification.failure_code(optional): string | null (known values, may grow: expired, no_eligible_channel, unavailable, ambiguous_provider_outcome, undeliverable) — Known values, not exhaustive: expired, no_eligible_channel, unavailable, ambiguous_provider_outcome, undeliverable.notification.occurred_at(optional): string | nullsignals: object (EventSignals) — Indicators about the exchange, copied onto an event when present. An event authorises nothing, so these are signals, not claims; only a consumed result (ResultClaims) is a statement you act on. Today the only signal is reason_code, so expect {} or a reason_code alone. New keys may be added.assurance: object (EventAssurance) — The request's assurance decision as stored. Both keys are always emitted; with no request, decision is null and reason_codes is empty.simulated: boolean
Ordering: 1 accepted, 2 sent, 3 delivered or failed, per notification. Arrival order is not guaranteed.
What to do: Optional: let your IdP offer another method. Never treat it as a sign-in failure by itself.
Full specification
The JSON is converted from the document below as you download it, so the two can never disagree.