Skip to main content

Documentation Index

Fetch the complete documentation index at: https://orbit-docs.devotel.io/llms.txt

Use this file to discover all available pages before exploring further.

Get Verification Detail

A single endpoint that returns the full lifecycle of a verification request — channel-by-channel send attempts, the fallback chain’s append-only history, the frozen config snapshot that drove the chain, the live expiration countdown, and a masked code-attempt log. The endpoint powers the verification-request drawer in the dashboard. It is also the canonical surface for self-serve dispute-resolution: when an end user claims they “never received the OTP”, an operator can reconstruct exactly what was tried, when, on which channel, with which provider, and what error came back. Endpoint
GET /api/v1/verify/verifications/{id}/detail
Authentication: Clerk session (Authorization: Bearer <token>) or API key (X-API-Key) carrying verify:read scope. Rate limit: 60 requests/minute per API key.

Path parameter

id
string
required
Verification id (prefixed, e.g. verification_3f1c8b2a9d4e5f7a8b6c). Cross-tenant ids return 404 (the tenant boundary is enforced by schema isolation; the controller cannot leak existence of another tenant’s verification).

Response

200 OK
{
  "data": {
    "id": "verification_3f1c8b2a9d4e5f7a8b6c",
    "recipient": "+14155552671",
    "channel": "sms",
    "status": "approved",
    "profileId": "vfp_us_default",
    "profileName": "US default — 6-digit, 10min TTL",
    "createdAt": "2026-05-24T11:00:00Z",
    "updatedAt": "2026-05-24T11:01:42Z",
    "expiresAt": "2026-05-24T11:10:00Z",
    "verifiedAt": "2026-05-24T11:01:42Z",
    "attempts": 1,
    "maxAttempts": 5,
    "resendsCount": 0,
    "currentChannelIndex": 0,
    "nextAttemptAt": null,
    "expirationCountdownSec": 0,
    "channelAttempts": [
      {
        "channel": "sms",
        "provider": "devotel-wholesale",
        "status": "delivered",
        "sentAt": "2026-05-24T11:00:01Z",
        "errorCode": null,
        "errorMessage": null
      }
    ],
    "fallbackChain": [
      {
        "channel": "sms",
        "outcome": "delivered",
        "reason": null,
        "errorCode": null,
        "attemptedAt": "2026-05-24T11:00:01Z",
        "providerMessageId": "msg_abc123"
      }
    ],
    "fallbackConfigSnapshot": {
      "primary": "sms",
      "fallbacks": ["voice", "email"],
      "advance_after_seconds": 60
    },
    "codeAttempts": [
      {
        "attemptedAt": "2026-05-24T11:01:42Z",
        "submittedDigits": "****1234",
        "result": "match",
        "ipAddress": "203.0.113.42"
      }
    ]
  },
  "meta": { "request_id": "req_abc123", "timestamp": "2026-05-24T12:00:00Z" }
}

Field reference

Top-level

FieldTypeNotes
idstringPrefixed verification id (e.g. verification_xxx).
recipientstringE.164 phone, email, or channel-specific handle.
channelstringPrimary channel as recorded at send time.
statusenumOne of pending, in_progress, approved, failed, expired, cancelled.
profileIdstring | nullVerify-profile id used at send time.
profileNamestring | nullBest-effort lookup against organizations.settings. Null when the profile was deleted between send and detail-read.
createdAtstring | nullISO-8601.
updatedAtstring | nullISO-8601.
expiresAtstring | nullISO-8601.
verifiedAtstring | nullISO-8601 — populated only when status: 'approved'.
attemptsinteger/check attempts consumed so far.
maxAttemptsintegerConfigured ceiling. Default 5.
resendsCountintegerExplicit /resend calls so far.
currentChannelIndexintegerCursor into the fallback chain (0 = primary, 1 = first fallback, …).
nextAttemptAtstring | nullScheduler tick target for the next fallback advance.
expirationCountdownSecintegerSeconds remaining until expiresAt. Computed at read time, clamped to 0 once expired. The dashboard renders a live countdown off this.

channelAttempts[] — per-channel summary

One row per channel touched. Latest outcome wins when the same channel was used twice (e.g. an explicit /resend).
FieldTypeNotes
channelstringsms, voice, email, whatsapp, etc.
providerstring | nullDevotel wholesale (default), or a per-channel BYO provider id.
statusstringqueued, sent, delivered, failed, bounced.
sentAtstring | nullISO-8601.
errorCodestring | nullProvider-side error code (e.g. carrier 30001).
errorMessagestring | nullHuman-readable provider-side error message.

fallbackChain[] — append-only history

One row per fallback engine tick. This is the full timeline, not just the latest. Useful for support flows that need to reconstruct exactly what happened.
FieldTypeNotes
channelstringChannel attempted at this tick.
outcomestringdelivered, failed, timeout, advanced.
reasonstring | nullWhy the chain advanced (e.g. no_dlr_within_window).
errorCodestring | nullProvider error code if outcome: 'failed'.
attemptedAtstring | nullISO-8601.
providerMessageIdstring | nullStable upstream message id for cross-reference with the carrier’s DLR.

fallbackConfigSnapshot

Frozen JSON snapshot of the fallback config that was active at send time. Null when the verification didn’t use a profile (direct send). Schema mirrors the verify-profile fallback_config body — reading the snapshot is the only reliable way to know what config drove this specific verification (the underlying profile may have been edited or deleted since).

codeAttempts[] — masked code-attempt log

Projected from audit_logs where action='verification.checked' AND resource='verification' AND resource_id=:id AND organization_id=:orgId, limited to the most recent 50 entries.
FieldTypeNotes
attemptedAtstringISO-8601.
submittedDigitsstring | nullAlways masked to ****<last4> (e.g. ****1234). The raw OTP is never returned even if a future audit-writer accidentally leaks it into the audit-log details payload.
resultenummatch, mismatch, expired, unknown.
ipAddressstring | undefinedCaller IP at /check time. Optional.
The OTP code column (a SHA-256 hash on disk) is never returned by this endpoint. The submittedDigits mask is a defense-in-depth control — defending the operator-facing detail view even if upstream code paths drift.

Errors

Statuserror.codeCause
404NOT_FOUNDVerification id not found, or belongs to a different tenant.
401UNAUTHENTICATEDAuth header missing or invalid.
403FORBIDDENAPI key lacks verify:read scope.
429RATE_LIMITED60 reads/min ceiling tripped.

Performance

P95 ≤ 200ms. The row read is a single PK lookup on the tenant schema; the audit_logs join is bounded to 50 rows and uses the (organization_id, resource, resource_id) composite index.

Edge-case handling

  • Empty history. Just-sent verifications return channelAttempts: [] and fallbackChain: []. The drawer renders an empty-state copy.
  • Deleted profile. profileName: null with profileId still populated. The snapshot still hydrates the dashboard rendering.
  • Expired row. expirationCountdownSec: 0 — the FE renders the “expired” badge unconditionally.
  • No audit rows. codeAttempts: []. Older verifications recorded before the verification.checked audit action shipped may legitimately have no entries.

See also