> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Verify — Get Verification Detail

> Consolidated detail endpoint for the verification request drawer — channel attempts, fallback chain, code-attempt log

# 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**

<Note>
  `GET /api/v1/verify/{id}/detail`
</Note>

**Authentication:** Clerk session (`Authorization: Bearer <token>`) or API key (`X-API-Key`). Any authenticated caller for the tenant may read this endpoint — no additional scope is required.

**Rate limit:** 60 requests/minute per API key.

***

## Path parameter

<ParamField path="id" type="string" required>
  Verification id (prefixed, e.g. `vrf_3f1c0b2a8e4d4f7a9c2b1e6d5a4c3b2a`). Cross-tenant ids return `404` (the tenant boundary is enforced by schema isolation; the controller cannot leak existence of another tenant's verification).
</ParamField>

***

## Response

**200 OK**

```json theme={null}
{
  "data": {
    "id": "vrf_3f1c0b2a8e4d4f7a9c2b1e6d5a4c3b2a",
    "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": 3,
    "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": {
      "channel_timeout_seconds": 60,
      "max_attempts_per_channel": 1,
      "channels": ["sms", "voice", "email"]
    },
    "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. `vrf_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 (1–10). Default 3.                                                                                                       |
| `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). 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).

| Field                      | Type      | Notes                                                                                                                         |
| -------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `channel_timeout_seconds`  | integer   | How long the scheduler waited before advancing to the next channel (10–600s, default 60). From the `fallback_config` body.    |
| `max_attempts_per_channel` | integer   | Retries per chain slot before advancing (1–3, default 1). From the `fallback_config` body.                                    |
| `channels`                 | string\[] | Ordered fallback chain as sent (1–4 entries) — primary first, then each fallback. Frozen from the request's `channels` array. |

### `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.                                                                                                                                       |

<Warning>
  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.
</Warning>

***

## 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.                              |
| `429`  | `RATE_LIMITED`    | 60 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

* [Verify API overview](/api-reference/verify)
* [Verify endpoints reference](/api-reference/endpoints/verify)
* [Webhook events — verify](/webhooks/events)
