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

# Email Verification Report

> Parse Email Verification responses: OTP lifecycle, breach exposure, disposable and undeliverable flags, cross-session matches, and risk warnings.

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.

<Frame>
  <img src="https://mintcdn.com/didit-0f962782/z6T2GHM4Zh-iSj-K/images/email-verification-report.png?fit=max&auto=format&n=z6T2GHM4Zh-iSj-K&q=85&s=4d153b7d3d9d4c88bc3932466af7ed29" alt="Didit email verification report showing OTP outcome, breach data and risk flags" width="2929" height="1158" data-path="images/email-verification-report.png" />
</Frame>

## 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](/core-technology/email-verification/warnings-email-verification)).
* 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/`](/standalone-apis/email-send) delivers the code and [`POST /v3/email/check/`](/standalone-apis/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.

```json theme={null}
{
  "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](/reference/data-models#email-verification) page.

```typescript theme={null}
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

| Status         | Meaning                                                                                                                               |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `Not Finished` | The OTP send has been initiated but the verification has not been finalized yet.                                                      |
| `Approved`     | The user entered a valid OTP and no declining risk matched.                                                                           |
| `Declined`     | An auto-decline warning fired (blocklist, undeliverable address, attempts exceeded) or a risk action configured to `DECLINE` matched. |
| `In Review`    | A risk action configured to `REVIEW` routed the step to manual review.                                                                |
| `Expired`      | The 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 type                              | Emitted when                                                            |
| --------------------------------------- | ----------------------------------------------------------------------- |
| `EMAIL_VERIFICATION_MESSAGE_SENT`       | First OTP send attempt — including sends that come back undeliverable.  |
| `EMAIL_VERIFICATION_RETRY_MESSAGE_SENT` | Subsequent OTP send after the user requested a resend.                  |
| `VALID_CODE_ENTERED`                    | The user submitted the correct OTP code.                                |
| `INVALID_CODE_ENTERED`                  | The user submitted a wrong or expired OTP code (counts toward the cap). |
| `EMAIL_VERIFICATION_APPROVED`           | Feature-level status was set to `Approved`.                             |
| `EMAIL_VERIFICATION_DECLINED`           | Feature-level status was set to `Declined`.                             |
| `EMAIL_VERIFICATION_IN_REVIEW`          | Feature-level status was set to `In Review`.                            |
| `EMAIL_VERIFICATION_EXPIRED`            | The 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 events** — `null`, 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](/management-api/lists/overview). 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.

```typescript theme={null}
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

```json theme={null}
{
  "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.

```json theme={null}
{
  "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`.

```json theme={null}
{
  "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"
    }
  ]
}
```

## Related

* [Email Verification overview](/core-technology/email-verification/overview) — feature behavior and pricing.
* [Email Verification warnings](/core-technology/email-verification/warnings-email-verification) — every risk code, decline triggers and configurable actions.
* [Data models — email verification](/reference/data-models#email-verification) — canonical field-by-field schema.
* [Email API — Send OTP](/standalone-apis/email-send) and [Check OTP](/standalone-apis/email-check) — standalone server-to-server endpoints.
* [Management API — Lists](/management-api/lists/overview) — manage the email blocklist that feeds `matches[]` with `source: list_entry`.
