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
curl -X POST https://api.orbit.devotel.io/api/v1/verify/send \
-H "X-API-Key: dv_live_sk_..." \
-H "Content-Type: application/json" \
-d '{
"to": "+14155552671",
"channel": "sms",
"code_length": 6
}'
Response
{
"data": {
"verification_id": "vrf_3f1c0b2a8e4d4f7a9c2b1e6d5a4c3b2a",
"status": "pending",
"channel": "sms",
"expires_at": "2026-03-08T00:10:00Z"
}
}
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
curl -X POST https://api.orbit.devotel.io/api/v1/verify/check \
-H "X-API-Key: dv_live_sk_..." \
-H "Content-Type: application/json" \
-d '{
"verification_id": "vrf_3f1c0b2a8e4d4f7a9c2b1e6d5a4c3b2a",
"code": "482901"
}'
Response
{
"data": {
"verification_id": "vrf_3f1c0b2a8e4d4f7a9c2b1e6d5a4c3b2a",
"status": "approved",
"channel": "sms"
}
}
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
| Channel | Delivery Method |
|---|
sms | SMS text message with the code |
whatsapp | WhatsApp message with the code |
voice | Automated voice call that reads the code aloud |
email | Email with the code and branded template |
viber | Viber message with the code |
telegram | Telegram message with the code |
flashcall | Missed-call OTP — the trailing caller-ID digits are the code |
Non-delivery / factor channels
| Channel | Behaviour |
|---|
silent | Network-based silent verification (no code shown to the user) |
sna | Silent Network Authentication — possession-proof MNO verification via the CAMARA / GSMA Open Gateway broker. Pass a device-bound device_token; returns status: "verified" on a confirmed match (no OTP). Without a token it fails closed with 503 PROVIDER_NOT_WIRED |
magic_link | Email-delivered single-use HTTPS link instead of a typed code (see Magic Link) |
totp | Authenticator-app (RFC 6238) factor — managed via /verify/factors/totp |
push | Push-factor MFA — managed via /verify/push |
backup_code | Single-use recovery codes — managed via /verify/factors/backup-codes |
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.
All factor endpoints are tenant-scoped and require the verify:write scope for create/verify/
delete operations (reads use verify:read).
TOTP (authenticator app)
RFC 6238 authenticator-app factors (Google Authenticator, Authy, 1Password, etc.). Creating a
factor returns the otpauth:// 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.
| Method | Path | Purpose |
|---|
GET | /verify/factors/totp | List TOTP factors |
POST | /verify/factors/totp | Create a factor (returns otpauth:// URI + secret once) |
POST | /verify/factors/totp/{id}/verify | Validate a 6-digit code (30s step, ±1 step skew) |
DELETE | /verify/factors/totp/{id} | Delete a factor |
POST | /verify/factors/totp/{id}/recovery-codes/regenerate | Invalidate and re-mint 10 recovery codes |
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 an UPDATE ... WHERE consumed_at IS NULL
gate), and verify responses return remaining_count so your UI can prompt regeneration.
| Method | Path | Purpose |
|---|
GET | /verify/factors/backup-codes | List backup-code factors |
POST | /verify/factors/backup-codes | Mint 10 single-use codes (returned once) |
POST | /verify/factors/backup-codes/{id}/verify | Consume one code (returns remaining_count) |
DELETE | /verify/factors/backup-codes/{id} | Delete a factor (idempotent) |
Verify Push
Phishing-resistant MFA via a device public-key + signed-challenge flow (mirrors Twilio Verify
factor.type="push"). Register a device public key, issue a challenge, then verify the signed
nonce returned by the device.
| Method | Path | Purpose |
|---|
POST | /verify/push/factors | Register a device public key |
POST | /verify/push/factors/{factorId}/challenges | Issue a new challenge |
POST | /verify/push/challenges/{challengeId}/verify | Submit a signed nonce |
POST | /verify/push/factors/{factorId}/revoke | Revoke a paired 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.
| Method | Path | Purpose |
|---|
POST | /verify/passkey/registration/options | Issue a WebAuthn registration ceremony |
POST | /verify/passkey/registration/verify | Verify attestation, create the factor |
POST | /verify/passkey/authentication/options | Issue a WebAuthn authentication ceremony |
POST | /verify/passkey/authentication/verify | Verify the assertion, bump the counter |
POST | /verify/passkey/factors/{factorId}/revoke | Revoke a paired passkey |
Magic Link
Email-delivered single-use HTTPS link (Stytch / WorkOS / Auth0 parity) instead of a typed code.
Send with channel: "magic_link" and an email to; Orbit emails a signed link. The user’s
click is consumed by the public, unauthenticated endpoint:
| Method | Path | Purpose |
|---|
POST | /verify/send (channel: "magic_link") | Email a signed single-use link |
GET | /public/verify/magic-link/consume?token={token} | Consume the link and approve the verification |
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 30+ languages
- Fraud detection — blocks known VoIP numbers and high-risk destinations
Configuration Options
These are the body parameters accepted by POST /api/v1/verify/send (sendVerificationSchema):
| Parameter | Type | Default | Description |
|---|
to | string | — | Required. Recipient phone number (E.164) or email address. |
channel | string | sms | Delivery channel: sms, whatsapp, email, voice, viber, telegram, silent. |
code_length | integer | 6 | Number of digits (4–8). |
max_attempts | integer | 3 | Maximum verification attempts allowed for this code (1–10). |
country | string | — | ISO 3166-1 alpha-2 hint used to parse national-format numbers into E.164 (e.g. TR). |
profile_id | string | — | Verify profile to apply (templates, expirySeconds, fallback config, branding). |
channels | string[] | — | Ordered fallback chain (min 1, max 4). Index 0 is the primary channel; the rest are tried in order. |
fallback_config | object | — | Async fallback engine config — { channel_timeout_seconds: 10–600 (default 60), max_attempts_per_channel: 1–3 (default 1) }. Pairs with channels. |
locale | string | — | 2-letter language code for the localized fallback message/TTS (e.g. en, de, es). Region tags (es-MX) are rejected. |
custom_code | string | — | Sandbox/test-mode only — fixed OTP digits (must equal code_length). Rejected with live keys. |
Code expiry is not a /send parameter. Direct sends expire after the default 600s (10 minutes). To change the TTL, set expirySeconds on a verify profile and pass its profile_id — there is no expiry field on the send body.
Webhook Events
| Event | Description |
|---|
verification.sent | The code was successfully handed to the provider |
verification.approved | A /check call matched the code — fires exactly once per verification (CAS-guarded) |
verification.failed | max_attempts exhausted with no match, or a SIM-swap pre-flight blocked the send |
verification.checked | Any /check attempt landed — successful or not (useful for audit / fraud scoring) |
verification.fallback_triggered | The async fallback engine advanced to the next channel in the chain |
verification.fallback_exhausted | Every channel in the fallback chain was tried without an approved status |