Skip to main content

The issuer-challenge model

Before Orbit trusts certain registrations, it asks the counterparty to prove something only the real counterparty can prove: that a human is at the keyboard, that the party controls the phone number or email address being registered, or that the business is the entity it claims to be. Each such proof request is an issuer challenge. A sender ID does not go live on a form submission alone; a marketplace publisher listing does not appear on a self-declaration alone. The registrar issues a challenge, the counterparty solves it, and only then does the registration bind. This page is the canonical explainer for the model every issuing surface shares. It names the challenge classes, the surfaces that fire them, the state machine each challenge moves through, the payload a solver must return, and what happens when a challenge expires or fails. The per-surface procedures (which documents a DLT submission needs, how the 10DLC wizard walks a brand) live in their own guides; this page tells you how to reason about any challenge any surface issues. If you are looking for the proof a session is live before a destructive operation, that is a different mechanism: see Destructive-operation re-auth challenge. Issuer challenges prove a counterparty at registration time; re-auth challenges prove a person at the keyboard at operation time.

The challenge definition

The registrar issues four challenge classes. Each asks for one specific proof and nothing else: A challenge always names its class, its issuer (the surface that fired it), and its subject (the sender ID, the WhatsApp Business account, the publisher submission, the key being created). A solve is only accepted for the exact challenge it answers; a code delivered for one subject cannot verify a different one.

When a challenge fires

The following surfaces issue challenges as part of their normal flow. The class column is what the surface typically issues; a surface may stack two classes when its risk warrants it (for example, a sender-ID registration from a fresh account). The rule across all surfaces is the same: the challenge fires at the moment state would become trusted, and the trusted state is withheld until the challenge resolves. Nothing downstream (traffic on the sender ID, a public listing, an active key) proceeds on an open challenge.

The state enum and transitions

Every challenge moves through one state machine, regardless of class:
  • pending — issued and waiting for a solve. The TTL clock starts here.
  • challenged — a solve payload has been submitted and is being checked (code comparison, captcha verification, or compliance review for kyc-doc-upload).
  • resolved — terminal success. The issuing surface binds the proof to the registration and proceeds.
  • expired — terminal failure. The TTL elapsed before a verified solve; nothing further can be submitted against this challenge.
TTLs are set per class to match how long a genuine counterparty needs: A resolved challenge is then bound to the request that asked for the registration: the surface records which challenge id cleared it, and the registration carries that binding forward. A challenge resolved last month does not satisfy a registration attempted today.

Payload contract per class

Every challenge, whichever class, carries the same envelope when issued:
A solver answers with the class-specific payload plus two binding fields:
  • request_id — copied verbatim from the challenge. The consume step rejects any solve whose request_id does not match the challenge it targets; this is what stops a code issued for one subject from being replayed against another.
  • challenge_id — the id of the challenge being solved.
Per-class solver fields: The authentication on the solve call is the tenant’s own credential (session or API key). The challenge payload is not itself a credential; it is a claim that a proof was produced, checked against the stored challenge.

Expiry and retry semantics

Each challenge carries a retry budget, reported as attempts_remaining and decremented on every rejected attempt: Terminal failure modes, in order of how you will meet them:
  • expired — the TTL elapsed. Mint a fresh challenge; nothing carries over except whatever documents you already uploaded.
  • Budget exhausted — too many rejected attempts. The challenge closes and the surface imposes a cooldown before a new one can be issued; this is what makes brute-forcing a six-digit code uneconomic.
  • Review denied (kyc-doc-upload) — the reviewer rejected the documents. The response carries the reason; correct the submission and start a new challenge.
A terminal challenge never revives. Every retry path is “issue a new challenge and solve that one.”

Tenant-side handling

If you build on the dashboard, most of this is handled for you: the wizard for each surface renders the right challenge UI (puzzle, code entry, or upload form), shows the remaining time and attempts, and re-issues on expiry. You interact with it as a user, not as an integrator. If you integrate over the API, two patterns work, and the right one depends on the class:
  • Poll for short classes. After submitting a solve for captcha, phone-verify, or email-verify, re-read the registration status; verification completes in seconds, so a short poll interval against the registration endpoint is sufficient.
  • Subscribe for kyc-doc-upload. Review takes days, so polling is the wrong shape. The resolution arrives as a webhook event on your tenant’s event stream: a challenge.resolved (or terminal challenge.expired / denial) event names the challenge id and the registration it unblocked. Register a webhook endpoint for the surface’s events and drive your state machine off it.
In both patterns, treat the challenge envelope as the source of truth for expires_at and attempts_remaining rather than assuming your own clock or counter agrees with the registrar’s.

Failure-mode troubleshooting

Worked walkthroughs

Dashboard: WhatsApp Business verification (phone-verify)

  1. In Channels → WhatsApp, start the Business verification for the phone number you are binding.
  2. The wizard issues a phone-verify challenge and sends a one-time code to that number over WhatsApp itself; the page shows the countdown and the attempts remaining.
  3. Enter the code. The wizard submits the solve with the correct request_id and shows the challenge move to resolved.
  4. The wizard proceeds to bind the Business account. If the code expired, the wizard re-issues the challenge in place; you never need to restart the whole verification.

API: sender-ID registration (captcha + solve)

Register a sender ID from a fresh account; the registration comes back with an open challenge instead of an approval:
Solve the puzzle at the issuing surface, then submit the solve against the same request_id:
On a verified solve the challenge transitions to resolved, the registration proceeds to approval, and a re-read of the sender ID shows it without any open challenges. On rejection, attempts_remaining decrements and the same sequence runs again with a fresh token; on expiry, the whole challenge is re-issued.

See also