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
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
| Field | Type | Notes |
|---|
id | string | Prefixed verification id (e.g. verification_xxx). |
recipient | string | E.164 phone, email, or channel-specific handle. |
channel | string | Primary channel as recorded at send time. |
status | enum | One of pending, in_progress, approved, failed, expired, cancelled. |
profileId | string | null | Verify-profile id used at send time. |
profileName | string | null | Best-effort lookup against organizations.settings. Null when the profile was deleted between send and detail-read. |
createdAt | string | null | ISO-8601. |
updatedAt | string | null | ISO-8601. |
expiresAt | string | null | ISO-8601. |
verifiedAt | string | null | ISO-8601 — populated only when status: 'approved'. |
attempts | integer | /check attempts consumed so far. |
maxAttempts | integer | Configured ceiling. Default 5. |
resendsCount | integer | Explicit /resend calls so far. |
currentChannelIndex | integer | Cursor into the fallback chain (0 = primary, 1 = first fallback, …). |
nextAttemptAt | string | null | Scheduler tick target for the next fallback advance. |
expirationCountdownSec | integer | Seconds 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).
| Field | Type | Notes |
|---|
channel | string | sms, voice, email, whatsapp, etc. |
provider | string | null | Devotel wholesale (default), or a per-channel BYO provider id. |
status | string | queued, sent, delivered, failed, bounced. |
sentAt | string | null | ISO-8601. |
errorCode | string | null | Provider-side error code (e.g. carrier 30001). |
errorMessage | string | null | Human-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.
| Field | Type | Notes |
|---|
channel | string | Channel attempted at this tick. |
outcome | string | delivered, failed, timeout, advanced. |
reason | string | null | Why the chain advanced (e.g. no_dlr_within_window). |
errorCode | string | null | Provider error code if outcome: 'failed'. |
attemptedAt | string | null | ISO-8601. |
providerMessageId | string | null | Stable 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.
| Field | Type | Notes |
|---|
attemptedAt | string | ISO-8601. |
submittedDigits | string | null | Always 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. |
result | enum | match, mismatch, expired, unknown. |
ipAddress | string | undefined | Caller 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
| Status | error.code | Cause |
|---|
404 | NOT_FOUND | Verification id not found, or belongs to a different tenant. |
401 | UNAUTHENTICATED | Auth header missing or invalid. |
403 | FORBIDDEN | API key lacks verify:read scope. |
429 | RATE_LIMITED | 60 reads/min ceiling tripped. |
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