Skip to main content
The Phone Verification report captures the full outcome of a phone OTP challenge: who we sent the code to, which carrier and channel handled it, how many attempts it took, whether the number is disposable or VoIP, and any cross-session matches against your blocklist or other users’ sessions. This page documents the JSON shape returned by the decision endpoint so you can parse OTP outcomes, carrier metadata, and risk flags for each verified number.
Didit phone verification report showing OTP outcome, carrier metadata and risk flags

Overview

A phone report is produced every time a workflow node runs the Phone Verification feature. Each report represents one OTP challenge against one phone number and contains:
  • The phone number broken into prefix, national number and full E.164 form, plus ISO country code and country name.
  • Carrier metadata (name, type) resolved from the destination network.
  • Boolean risk flags (is_disposable, is_virtual) derived from line-type analysis.
  • The actual delivery channel used (verification_method) and the count of OTP send attempts.
  • A chronological lifecycle[] log of every send, retry, delivery event, and code-check attempt, each delivery event naming the send it belongs to and, when the message was not delivered, why.
  • A matches[] array surfacing the same number on other sessions — any status, across KYC, KYB and standalone API verifications — or your blocklist.
  • A warnings[] array — risk events emitted during the verification (see Phone Verification warnings).
  • A node_id that identifies which workflow graph node produced the report (V3 sessions only).
In hosted verification flows Didit runs the OTP exchange for you. For direct API integrations the same flow is two calls — POST /v3/phone/send/ to deliver the code and POST /v3/phone/check/ to validate the user’s entry. A pending OTP is valid for 5 minutes from the first send (retries do not extend the window). In workflow sessions users get 2 OTP send attempts (phone_max_retries) and 2 code-entry attempts (phone_max_check_attempts) by default — both tunable per workflow node; exhausting either cap finalizes the step as Declined with VERIFICATION_CODE_ATTEMPTS_EXCEEDED. The standalone phone API allows 3 code-entry attempts per verification.

Where it appears in API responses

The decision endpoint (GET /v3/session/{sessionId}/decision/) returns phone reports under the plural array key phone_verifications. The array contains one entry per Phone Verification node in the workflow graph — typically one, but step-up flows may produce several.
A null value means no Phone Verification step has run yet. Iterate the array (rather than reading phone_verifications[0]) when your workflow can collect more than one phone number.

Schema

The canonical field-by-field schema lives on the Data models page.

Status values

Lifecycle event types

lifecycle[] is sorted chronologically. Each event has type, timestamp, details, and a billable fee (when applicable): The details payload depends on the event family:
  • Send events (PHONE_VERIFICATION_MESSAGE_SENT, PHONE_VERIFICATION_RETRY_MESSAGE_SENT, PHONE_VERIFICATION_BLOCKED) — { status, reason, channel, actual_channel, attempt, delivery_status }. status is Success, Retry or Blocked; reason is null unless the send was blocked (invalid_phone_number, repeated_attempts, suspicious, spam, unknown); channel is the requested channel and actual_channel the channel that actually delivered (e.g. a WhatsApp send that fell back to SMS); attempt is the 0-based index of this send, in the same order as the send events. delivery_status is how the send ended — delivered, undeliverable, or unknown when the verification ended and no delivery report had arrived — and is absent while a report can still arrive.
  • Delivery events (PHONE_DELIVERY_DELIVERED, PHONE_DELIVERY_UNDELIVERABLE) — { channel, status, attempt, reason } with status delivered or undeliverable. attempt is the index of the send the report belongs to, so a report that arrives after the code was entered (an earlier send expiring, a fallback attempt failing) is not mistaken for the code itself not arriving. reason appears on PHONE_DELIVERY_UNDELIVERABLE only, and is one word from the table below; a call’s channel is voice and its reason is one of the four call words.
  • Check events (VALID_CODE_ENTERED, INVALID_CODE_ENTERED) — { code_tried, status } with status Approved, Declined, Failed or Expired or Not Found.
  • Final status events — null, except PHONE_VERIFICATION_DECLINED / PHONE_VERIFICATION_IN_REVIEW, which carry { "reason": "<risk code>" } (e.g. PHONE_NUMBER_IN_BLOCKLIST).
reason on a PHONE_DELIVERY_UNDELIVERABLE event is a fixed, customer-safe word. It is never a provider name, a provider status or a raw error code, so it is safe to show to support teams and end users as it is. A call answers one of the four call words and never a message word, so the reason on the event is the same word the phone step shows the user while they wait for the code. fee on a send event is the total price of that send: the carrier or WhatsApp price plus the $0.02 Didit fee, or only the $0.02 Didit fee for a WhatsApp code sent from your own WhatsApp Business number. It is the estimated price until delivery is confirmed, then the actual billed amount. Only Blocked sends are guaranteed free: any other send is charged when the verification finalizes or expires, even if delivery was never confirmed or the message was undeliverable. Delivery, check and status events always carry fee: 0.

Cross-session matches

matches[] records other sessions in the same application where the same number was seen — across KYC, KYB and standalone API verifications, regardless of their status — plus blocklist hits configured in the management-api lists. Sessions sharing the current session’s vendor_data are excluded (the same end-user does not match themselves), and the array is capped at 5 entries. When the number is on your blocklist but none of the matched sessions is blocklisted, a synthetic entry with source: "list_entry" (all session fields null) is prepended to the array.

Examples

Approved — WhatsApp OTP, mobile carrier

Declined — blocklisted VoIP number

The user entered a valid OTP, but finalization found the number on the application’s blocklist (auto-decline) and flagged its VoIP line type (voip_number_action left at NO_ACTION, so it logs as information). Because no matched session is blocklisted, the blocklist hit appears as a synthetic list_entry match.
Last modified on November 10, 2026