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.
  • 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.
{
  "session_id": "11111111-1111-1111-1111-111111111111",
  "status": "Approved",
  "phone_verifications": [
    { "status": "Approved", "node_id": "feature_phone_1", "...": "..." }
  ]
}
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.
interface PhoneVerification {
  status: "Not Finished" | "Approved" | "Declined" | "In Review" | "Expired";
  phone_number_prefix: string;       // E.g. "+34"
  phone_number: string;              // National number, no prefix
  full_number: string;               // Full E.164, e.g. "+34600600600"
  country_code: string;              // ISO 3166-1 alpha-2, e.g. "ES"
  country_name: string;
  carrier: {
    name: string;
    type:                              // From LineTypeChoices
      | "mobile" | "fixed_line" | "voip" | "isp" | "vpn"
      | "toll_free" | "premium_rate" | "shared_cost" | "local_rate"
      | "satellite" | "pager" | "payphone" | "voice_mail"
      | "calling_cards" | "service" | "short_codes_commercial"
      | "universal_access" | "other" | "unknown";
  };
  is_disposable: boolean;            // Temporary / burner number
  is_virtual: boolean;               // true when carrier.type is voip, isp or vpn
  verification_method:               // Actual channel that delivered the OTP
    | "sms" | "whatsapp" | "telegram" | "voice" | "rcs" | "viber" | "zalo";
  verification_attempts: number;     // OTP send attempts (initial send + resends)
  verified_at: string | null;        // ISO 8601, set when a valid OTP was entered
  warnings: Warning[];               // Risk events emitted during verification
  lifecycle: PhoneLifecycleEvent[];  // OTP send/retry/delivery/check timeline
  matches: PhoneMatch[];             // Cross-session / blocklist matches (max 5)
  node_id: string;                   // Workflow graph node that produced this report
}

Status values

StatusMeaning
Not FinishedThe OTP send has been initiated but the verification has not been finalized yet.
ApprovedThe user entered a valid OTP and no declining risk matched.
DeclinedAn auto-decline warning fired (blocklist, high-risk, attempts exceeded) or a risk action configured to DECLINE matched.
In ReviewA risk action configured to REVIEW routed the step to manual review.
ExpiredThe 5-minute OTP window elapsed with no valid code — applies to verifications created via the standalone phone API.

Lifecycle event types

lifecycle[] is sorted chronologically. Each event has type, timestamp, details, and a billable fee (when applicable):
Event typeEmitted when
PHONE_VERIFICATION_MESSAGE_SENTFirst OTP send attempt over the requested channel.
PHONE_VERIFICATION_RETRY_MESSAGE_SENTSubsequent OTP send after the user requested a resend.
PHONE_VERIFICATION_BLOCKEDThe send was blocked (spam, suspicious, repeated, invalid number).
PHONE_DELIVERY_DELIVEREDCarrier confirmed delivery on the actual channel used.
PHONE_DELIVERY_UNDELIVERABLECarrier reported the message could not be delivered.
VALID_CODE_ENTEREDThe user submitted the correct OTP code.
INVALID_CODE_ENTEREDThe user submitted an incorrect OTP code (counts toward the code-entry cap).
PHONE_VERIFICATION_APPROVEDFeature-level status was set to Approved.
PHONE_VERIFICATION_DECLINEDFeature-level status was set to Declined.
PHONE_VERIFICATION_IN_REVIEWFeature-level status was set to In Review.
PHONE_VERIFICATION_EXPIREDThe OTP window elapsed without a valid code.
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 }. 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).
  • Delivery events (PHONE_DELIVERY_DELIVERED, PHONE_DELIVERY_UNDELIVERABLE) — { channel, status } with status delivered or undeliverable.
  • Check events (VALID_CODE_ENTERED, INVALID_CODE_ENTERED) — { code_tried, status } with status Approved, Declined, Failed or Expired or Not Found.
  • Final status eventsnull, except PHONE_VERIFICATION_DECLINED / PHONE_VERIFICATION_IN_REVIEW, which carry { "reason": "<risk code>" } (e.g. PHONE_NUMBER_IN_BLOCKLIST).
fee on a send event is the estimated price until delivery is confirmed, then the actual billed amount — delivery is billed per delivered message when the verification finalizes, and only Blocked sends are guaranteed free. 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.
interface PhoneMatch {
  session_id: string | null;         // null when source = "list_entry"
  session_number: number | null;
  vendor_data: string | null;
  verification_date: string | null;  // ISO 8601, creation date of the matched session
  phone_number: string;
  status: string | null;             // Status of the matched session
  is_blocklisted: boolean;
  api_service: string | null;        // Set when the matched session came from a standalone API
  source: "session" | "list_entry";  // "list_entry" = manual blocklist hit
}

Examples

Approved — WhatsApp OTP, mobile carrier

{
  "status": "Approved",
  "phone_number_prefix": "+34",
  "phone_number": "600600600",
  "full_number": "+34600600600",
  "country_code": "ES",
  "country_name": "Spain",
  "carrier": { "name": "Orange", "type": "mobile" },
  "is_disposable": false,
  "is_virtual": false,
  "verification_method": "whatsapp",
  "verification_attempts": 1,
  "verified_at": "2025-08-24T09:12:39.684292Z",
  "warnings": [],
  "lifecycle": [
    {
      "type": "PHONE_VERIFICATION_MESSAGE_SENT",
      "timestamp": "2025-08-24T09:12:30.580554Z",
      "details": { "status": "Success", "reason": null, "channel": "whatsapp", "actual_channel": "whatsapp" },
      "fee": 0.04
    },
    {
      "type": "PHONE_DELIVERY_DELIVERED",
      "timestamp": "2025-08-24T09:12:31.000000Z",
      "details": { "channel": "whatsapp", "status": "delivered" },
      "fee": 0
    },
    {
      "type": "VALID_CODE_ENTERED",
      "timestamp": "2025-08-24T09:12:39.662157Z",
      "details": { "code_tried": "123456", "status": "Approved" },
      "fee": 0
    },
    {
      "type": "PHONE_VERIFICATION_APPROVED",
      "timestamp": "2025-08-24T09:12:39.684292Z",
      "details": null,
      "fee": 0
    }
  ],
  "matches": [],
  "node_id": "feature_phone_1"
}

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.
{
  "status": "Declined",
  "phone_number_prefix": "+1",
  "phone_number": "5551234567",
  "full_number": "+15551234567",
  "country_code": "US",
  "country_name": "United States",
  "carrier": { "name": "VoIP Carrier", "type": "voip" },
  "is_disposable": false,
  "is_virtual": true,
  "verification_method": "sms",
  "verification_attempts": 1,
  "verified_at": "2025-08-24T09:11:10.221340Z",
  "warnings": [
    {
      "feature": "PHONE",
      "risk": "PHONE_NUMBER_IN_BLOCKLIST",
      "additional_data": { "blocklisted_session_id": null, "blocklisted_session_number": null, "api_service": null },
      "log_type": "error",
      "short_description": "Phone number in blocklist",
      "long_description": "The system detected that the phone number is in the blocklist, which is not allowed.",
      "node_id": "feature_phone_1"
    },
    {
      "feature": "PHONE",
      "risk": "VOIP_NUMBER_DETECTED",
      "additional_data": null,
      "log_type": "information",
      "short_description": "VoIP number detected",
      "long_description": "The system detected that the phone number is a VoIP number, which is not allowed.",
      "node_id": "feature_phone_1"
    }
  ],
  "lifecycle": [
    {
      "type": "PHONE_VERIFICATION_MESSAGE_SENT",
      "timestamp": "2025-08-24T09:11:00.000000Z",
      "details": { "status": "Success", "reason": null, "channel": "sms", "actual_channel": "sms" },
      "fee": 0.04
    },
    {
      "type": "VALID_CODE_ENTERED",
      "timestamp": "2025-08-24T09:11:10.221340Z",
      "details": { "code_tried": "482917", "status": "Approved" },
      "fee": 0
    },
    {
      "type": "PHONE_VERIFICATION_DECLINED",
      "timestamp": "2025-08-24T09:11:10.350000Z",
      "details": { "reason": "PHONE_NUMBER_IN_BLOCKLIST" },
      "fee": 0
    }
  ],
  "matches": [
    {
      "session_id": null,
      "session_number": null,
      "vendor_data": null,
      "verification_date": null,
      "phone_number": "+15551234567",
      "status": null,
      "is_blocklisted": true,
      "api_service": null,
      "source": "list_entry"
    }
  ],
  "node_id": "feature_phone_1"
}