Verify
Orbit Verify is a turnkey OTP (one-time password) verification service. Send verification codes via SMS, WhatsApp, Voice, or Email — Orbit handles code generation, delivery, expiration, and validation so you don’t have to.Send a Verification Code
Response
Send in Bulk
Need to fan an OTP out to many recipients at once (login surges, password-reset campaigns)?POST /api/v1/verify/bulk accepts an array of up to 1000 recipients that share one
channel and optional verify profile. Every recipient runs through the same per-recipient
send pipeline as /verify/send, and the call returns one result row per recipient plus batch
totals. The HTTP status reflects the batch outcome: 201 when every recipient was sent,
207 (Multi-Status) on partial success, and 400 when every recipient failed. See the
Verify API reference → Bulk send for the full request/response
shape.
Check a Verification Code
Response
Supported Channels
POST /verify/send accepts the channel values below. The first group delivers an OTP
to the recipient; the second group are non-delivery / factor channels that don’t carry
a code over a carrier and are handled by dedicated factor endpoints (see
MFA Factors below).
Delivery channels
Telegram delivery configuration
Thetelegram channel delivers codes through Telegram’s
Gateway API (sendVerificationMessage) — the
verification gateway, which is separate from the Telegram Bot API that powers the
Telegram messaging channel. To enable it, set these on the API
service:
Until
DEVOTEL_TELEGRAM_GATEWAY_API_KEY is set, POST /verify/send with
channel: "telegram" fails closed with 503 SERVICE_UNAVAILABLE.
Non-delivery / factor channels
Sending
channel: "totp", "push", or "backup_code" to POST /verify/send does not
issue an OTP — these are possession-of-secret factors with their own lifecycle. The send
endpoint short-circuits (422 / 400) and steers you to the matching factor endpoint below.MFA Factors
Beyond OTP send/check, Verify ships phishing-resistant and possession-of-secret MFA factor surfaces for end-tenant users. These factors carry no carrier delivery and no wallet deduct — they validate proof-of-possession against a server-side secret or device key pair. Every factor endpoint requires an authenticated request and is isolated to your organization — a factor created by one tenant is never visible to another. The scope requirement depends on the factor type:- TOTP and backup-code write operations (create, verify, delete, and recovery-code
regeneration) require the
verify:writescope. Dashboard (session) callers must also hold an owner, admin, or developer role. - TOTP and backup-code list (read) endpoints, along with every Verify Push and
Passkey endpoint, are gated by authentication and per-tenant rate limiting only. They do
not require an additional
verify:writeorverify:readscope, so any authenticated key scoped to the tenant can call them — restrict which keys reach these flows accordingly.
The factor suite is API-only by design — it has no dashboard screen. TOTP, backup codes,
passkeys (WebAuthn / FIDO2), and push are enrolled, verified, and revoked from inside your own
application, where the end user is present to scan a QR code, run a WebAuthn ceremony, or approve
a push prompt on their device. Drive them straight from the endpoints below or the SDK. (The
Verify dashboard covers OTP send/check, profiles, and analytics; the 2FA control under
Settings → Security protects your Orbit login and is separate from these end-user factors.)
TOTP (authenticator app)
RFC 6238 authenticator-app factors (Google Authenticator, Authy, 1Password, etc.). Creating a factor returns theotpauth:// URI for QR rendering plus the base32 secret once —
subsequent reads return metadata only. Ten single-use recovery codes are minted on creation
and surfaced once; only their SHA-256 hashes are persisted.
Backup codes
Single-use, hashed-at-rest recovery codes for users who have lost their device + SMS access. Creating a factor mints 10 plaintext codes returned once; the server stores SHA-256 hashes only. Each code can succeed at most once (enforced by anUPDATE ... WHERE consumed_at IS NULL
gate), and verify responses return remaining_count so your UI can prompt regeneration.
Verify Push
Phishing-resistant MFA via a device public-key + signed-challenge flow (mirrors Twilio Verifyfactor.type="push"). Register a device public key, issue a challenge, then verify the signed
nonce returned by the device.
Passkeys (WebAuthn / FIDO2)
Phishing-resistant passkey factors. The registration and authentication ceremonies follow the WebAuthn spec; attestation/assertion verification (CBOR / COSE / signature) runs server-side.Magic Link
Email-delivered single-use HTTPS link (Stytch / WorkOS / Auth0 parity) instead of a typed code. Send withchannel: "magic_link" and an email to; Orbit emails a signed link. The user’s
click is consumed by the public, unauthenticated endpoint:
The token is a constant-time HMAC-signed payload; the consume endpoint approves the underlying
verification on success and rejects tampered, expired, or already-used tokens.
Features
- Auto-generated codes — secure random codes (4–8 digits)
- Multi-channel delivery — SMS, WhatsApp, Voice, Email
- Channel fallback — automatic retry on a different channel if the first fails
- Rate limiting — built-in protection against brute-force attacks
- Expiration — configurable TTL (default 10 minutes / 600s, max 60 minutes / 3600s)
- Attempt limits — configurable max verification attempts per code (1–10, default 3)
- Locale support — localized message templates in 25 languages
- Fraud detection — optional VoIP/high-risk blocking, configurable per tenant (off by default)
Configuration Options
These are the body parameters accepted byPOST /api/v1/verify/send (sendVerificationSchema):
Code expiry is not a/sendparameter. Direct sends expire after the default 600s (10 minutes). To change the TTL, setexpirySecondson a verify profile and pass itsprofile_id— there is noexpiryfield on the send body.
Webhook Events
A SIM-swap pre-flight block does not emit a webhook. When the pre-flight check rejects a send,/sendreturns a synchronous403 SIM_SWAP_DETECTEDwithlast_swap_dateandblock_window_hoursin the responsedetails. No verification row is created and noverification.failedevent fires, so wire failure/step-up triggers to the403response rather than the webhook.