Skip to main content
The Email Verification report captures the full outcome of an email OTP challenge: who the message was sent to, whether the address is disposable or undeliverable, whether it appears in known data breaches, how many attempts the user took, and any cross-session matches against your blocklist or other approved users. This page documents the JSON shape returned by the decision endpoint so you can parse OTP outcomes, breach exposure, and risk flags for each verified address.
Didit email verification report showing OTP outcome, breach data and risk flags

Overview

An email report is produced every time a workflow node runs the Email Verification feature. Each report represents one OTP challenge against one address and contains:
  • The verified email address.
  • Boolean risk flags (is_breached, is_disposable, is_undeliverable) and the supporting breaches[] array sourced from a breach-intelligence database.
  • The count of OTP send attempts (verification_attempts) and the approval timestamp.
  • A chronological lifecycle[] log of every send, retry and code-check attempt.
  • A matches[] array surfacing the same address on other approved email verifications or your blocklist.
  • A warnings[] array — risk events emitted during the verification (see Email Verification warnings).
  • A node_id that identifies which workflow graph node produced the report (V3 sessions only).
In hosted workflow sessions the OTP challenge runs inside the Didit verification UI — you only read the result from the decision payload. For server-to-server OTP without a hosted flow, use the standalone Email API: POST /v3/email/send/ delivers the code and POST /v3/email/check/ validates the user’s entry; finalized standalone verifications surface through the same report shape. Hosted sessions cap the user at 2 wrong code entries (email_max_check_attempts) and 2 OTP sends — the initial send plus one resend (email_max_retries) by default, tunable per workflow node. Exceeding either cap finalizes the step as Declined with EMAIL_CODE_ATTEMPTS_EXCEEDED.

Where it appears in API responses

The decision endpoint (GET /v3/session/{sessionId}/decision/) returns email reports under the plural array key email_verifications. The array contains one entry per Email Verification node in the workflow graph — typically one, but step-up flows may produce several.
{
  "session_id": "11111111-1111-1111-1111-111111111111",
  "status": "Approved",
  "email_verifications": [
    { "node_id": "feature_email_1", "status": "Approved", "...": "..." }
  ]
}
A null value means no Email Verification step has run yet. Iterate the array (rather than reading email_verifications[0]) when your workflow can collect more than one email address.

Schema

The canonical field-by-field schema lives on the Data models page.
interface EmailVerification {
  node_id: string | null;
  status: "Not Finished" | "Approved" | "Declined" | "In Review" | "Expired";
  email: string;
  is_breached: boolean;
  breaches: Breach[];                // Up to the 5 most recent known breaches
  is_disposable: boolean;            // Disposable / throwaway provider
  is_undeliverable: boolean;         // Failed syntax / DNS (MX) validation or OTP delivery
  verification_attempts: number;     // OTP send attempts
  verified_at: string | null;        // ISO 8601, null until a valid code is entered
  lifecycle: EmailLifecycleEvent[];  // OTP send/retry/check timeline
  warnings: Warning[];               // Risk events (see Data models → Warning object)
  matches: EmailMatch[];             // Cross-session / blocklist matches
}

interface Breach {
  name: string;                      // Breached service name
  domain: string;                    // Affected domain
  breach_date: string;               // YYYY-MM-DD
  breach_emails_count: number;       // Total affected accounts
  description: string;               // HTML description of the breach
  logo_path: string;                 // Breached-service logo URL
  data_classes: string[];            // Exposed data categories (snake_case)
  is_verified: boolean;              // true when the breach is a verified incident
}
An undeliverable address — invalid syntax, a non-existent domain, missing MX records, or a failed OTP delivery — finalizes the step as Declined with the auto-decline warning UNDELIVERABLE_EMAIL_DETECTED. The only exception: when the user typed the address themselves (it was not pre-filled at session creation), an undeliverable send returns an inline error so they can retry with a different address instead of being declined immediately.

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, undeliverable address, 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 email API.

Lifecycle event types

lifecycle[] is sorted chronologically. Each event has type, timestamp, details and a billable fee (when applicable):
Event typeEmitted when
EMAIL_VERIFICATION_MESSAGE_SENTFirst OTP send attempt — including sends that come back undeliverable.
EMAIL_VERIFICATION_RETRY_MESSAGE_SENTSubsequent OTP send after the user requested a resend.
VALID_CODE_ENTEREDThe user submitted the correct OTP code.
INVALID_CODE_ENTEREDThe user submitted a wrong or expired OTP code (counts toward the cap).
EMAIL_VERIFICATION_APPROVEDFeature-level status was set to Approved.
EMAIL_VERIFICATION_DECLINEDFeature-level status was set to Declined.
EMAIL_VERIFICATION_IN_REVIEWFeature-level status was set to In Review.
EMAIL_VERIFICATION_EXPIREDThe OTP window elapsed without a valid code (standalone email API).
The details payload depends on the event family:
  • Send events (EMAIL_VERIFICATION_MESSAGE_SENT, EMAIL_VERIFICATION_RETRY_MESSAGE_SENT) — { status, reason }. status is Success, Retry or Undeliverable; reason is null unless the send failed (email_can_not_be_delivered, or unknown for unrecognized legacy values).
  • Check events (VALID_CODE_ENTERED, INVALID_CODE_ENTERED) — { code_tried, status } with status Approved, Failed, Expired or Not Found or Declined.
  • Final status eventsnull, except EMAIL_VERIFICATION_DECLINED / EMAIL_VERIFICATION_IN_REVIEW, which carry { "reason": "<risk code>" } (e.g. UNDELIVERABLE_EMAIL_DETECTED).
Sends with status Success or Undeliverable record the $0.03 Email Verification fee; Retry sends, check events and status events always carry fee: 0.

Cross-session matches

matches[] records the same address on previously approved email verifications in the same application — across KYC, KYB and standalone API sessions — plus blocklist hits configured in the management-api lists. Verifications sharing the current session’s vendor_data are excluded (the same end-user does not match themselves), the array is capped at 5 entries, ordered oldest-first. When the address 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 EmailMatch {
  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
  email: 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 — clean OTP with one informational breach

{
  "node_id": "feature_email_1",
  "status": "Approved",
  "email": "alex.sample@example.com",
  "is_breached": true,
  "breaches": [
    {
      "name": "ExampleAir",
      "domain": "example-air.com",
      "breach_date": "2022-08-25",
      "breach_emails_count": 6083479,
      "description": "In August 2022, the airline ExampleAir suffered a data breach that exposed customers' personal information.",
      "logo_path": "https://<media-host>/logos/ExampleAir.png",
      "data_classes": [
        "dates_of_birth", "email_addresses", "genders", "names",
        "nationalities", "phone_numbers", "physical_addresses",
        "salutations", "spoken_languages"
      ],
      "is_verified": true
    }
  ],
  "is_disposable": false,
  "is_undeliverable": false,
  "verification_attempts": 1,
  "verified_at": "2025-09-15T17:36:19.963451Z",
  "lifecycle": [
    {
      "type": "EMAIL_VERIFICATION_MESSAGE_SENT",
      "timestamp": "2025-09-15T17:35:50.000000+00:00",
      "details": { "status": "Success", "reason": null },
      "fee": 0.03
    },
    {
      "type": "VALID_CODE_ENTERED",
      "timestamp": "2025-09-15T17:36:19.000000+00:00",
      "details": { "code_tried": "123456", "status": "Approved" },
      "fee": 0
    },
    {
      "type": "EMAIL_VERIFICATION_APPROVED",
      "timestamp": "2025-09-15T17:36:19.963451+00:00",
      "details": null,
      "fee": 0
    }
  ],
  "warnings": [
    {
      "feature": "EMAIL",
      "risk": "BREACHED_EMAIL_DETECTED",
      "additional_data": null,
      "log_type": "information",
      "short_description": "Breached email detected",
      "long_description": "This email address was found in one or more known data breaches.",
      "node_id": "feature_email_1"
    }
  ],
  "matches": []
}

Declined — undeliverable address

A pre-filled address that fails syntax or DNS (MX) validation — or whose OTP email cannot be delivered — finalizes immediately as Declined. No code-check events appear because no code ever reached the user.
{
  "node_id": "feature_email_1",
  "status": "Declined",
  "email": "user@nonexistent-domain.example",
  "is_breached": false,
  "breaches": [],
  "is_disposable": false,
  "is_undeliverable": true,
  "verification_attempts": 1,
  "verified_at": null,
  "lifecycle": [
    {
      "type": "EMAIL_VERIFICATION_MESSAGE_SENT",
      "timestamp": "2025-09-15T18:00:00.000000+00:00",
      "details": { "status": "Undeliverable", "reason": "email_can_not_be_delivered" },
      "fee": 0.03
    },
    {
      "type": "EMAIL_VERIFICATION_DECLINED",
      "timestamp": "2025-09-15T18:00:01.500000+00:00",
      "details": { "reason": "UNDELIVERABLE_EMAIL_DETECTED" },
      "fee": 0
    }
  ],
  "warnings": [
    {
      "feature": "EMAIL",
      "risk": "UNDELIVERABLE_EMAIL_DETECTED",
      "additional_data": null,
      "log_type": "error",
      "short_description": "Undeliverable email detected",
      "long_description": "The system detected that the email is undeliverable, which is not allowed.",
      "node_id": "feature_email_1"
    }
  ],
  "matches": []
}

Declined — blocklisted disposable mailbox

The user completed the OTP, but the address matched your email blocklist (auto-decline). The disposable flag is also raised — at information level here because disposable_email_action is left at its default NO_ACTION.
{
  "node_id": "feature_email_1",
  "status": "Declined",
  "email": "user@mailinator.com",
  "is_breached": false,
  "breaches": [],
  "is_disposable": true,
  "is_undeliverable": false,
  "verification_attempts": 1,
  "verified_at": "2025-09-15T18:05:42.201734Z",
  "lifecycle": [
    {
      "type": "EMAIL_VERIFICATION_MESSAGE_SENT",
      "timestamp": "2025-09-15T18:05:01.000000+00:00",
      "details": { "status": "Success", "reason": null },
      "fee": 0.03
    },
    {
      "type": "VALID_CODE_ENTERED",
      "timestamp": "2025-09-15T18:05:42.000000+00:00",
      "details": { "code_tried": "654321", "status": "Approved" },
      "fee": 0
    },
    {
      "type": "EMAIL_VERIFICATION_DECLINED",
      "timestamp": "2025-09-15T18:05:42.201734+00:00",
      "details": { "reason": "EMAIL_IN_BLOCKLIST" },
      "fee": 0
    }
  ],
  "warnings": [
    {
      "feature": "EMAIL",
      "risk": "EMAIL_IN_BLOCKLIST",
      "additional_data": {
        "blocklisted_session_id": null,
        "blocklisted_session_number": null,
        "api_service": null
      },
      "log_type": "error",
      "short_description": "Email in blocklist",
      "long_description": "The system detected that the email is in the blocklist, which is not allowed.",
      "node_id": "feature_email_1"
    },
    {
      "feature": "EMAIL",
      "risk": "DISPOSABLE_EMAIL_DETECTED",
      "additional_data": null,
      "log_type": "information",
      "short_description": "Disposable email detected",
      "long_description": "The system detected that the email is disposable, which is not allowed.",
      "node_id": "feature_email_1"
    }
  ],
  "matches": [
    {
      "session_id": null,
      "session_number": null,
      "vendor_data": null,
      "verification_date": null,
      "email": "user@mailinator.com",
      "status": null,
      "is_blocklisted": true,
      "api_service": null,
      "source": "list_entry"
    }
  ]
}