mcpField
D
didit-email-verification

didit-email-verification

github.com/didit-protocolSecurity29200 installsUpdated 2w ago
Repository

About didit-email-verification

A Didit API skill covering email code delivery, code verification, and address risk checks. It combines verification with signals such as deliverability, breaches, disposable providers, duplicate use, and optional social footprint.

Key features

  1. 01Send one-time email codes
  2. 02Verify submitted codes
  3. 03Assess email risk without sending a code
  4. 04Check deliverability and known breaches
  5. 05Optionally inspect social footprint

Use cases

  1. 01Verify an email address during account creation
  2. 02Reject or flag disposable or breached addresses
  3. 03Assess address risk without a code-based flow

MCP support

This skill documents the Didit Email Verification API; it does not describe an MCP server or MCP-specific interface.

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/didit-protocol/skills --skill didit-email-verification

After installing, type/didit-email-verificationin Claude Code to confirm it loaded.

Bundled filesscripts/verify_email.py

Details

From the project README

Didit Email Verification API

Overview

Three standalone endpoints:

  1. Send a one-time code to an email address: POST /v3/email/send/
  2. Check the code the user typed and get the email report: POST /v3/email/check/
  3. Risk (no code sent): score an address in one call: POST /v3/email/risk/

Key constraints (send + check):

  • The check finds the verification by your application + the email address. It does not take request_id.
  • A pending verification lives 5 minutes from the first send. Retries do not extend it.
  • 3 code attempts per verification. Wrong codes 1 and 2 return "Failed"; the 3rd wrong code finalizes it as "Declined" (EMAIL_CODE_ATTEMPTS_EXCEEDED).
  • One resend per pending verification: a second send for the same address inside the window returns "Retry" with the same request_id and a fresh code (the previous code stops working). A further send starts a new verification with a new request_id.
  • Codes are compared case-insensitively. Only the most recent code is valid.
  • No pending verification (never sent, already finalized, undeliverable at send, or older than 5 minutes): the check returns HTTP 200 with status: "Expired or Not Found".
  • Each new verification is stored as an API session: request_id works with GET /v3/session/{sessionId}/decision/, shows in the Business Console, and fires status.updated webhooks as it progresses. Each risk call also creates an API session.

Capabilities: known data breaches (breaches[]), disposable providers, deliverability (syntax and DNS/MX), duplicates across your sessions (matches[]), your email blocklist/allowlist, optional social footprint (enable_social), fraud signals.

Sandbox: sandbox API keys skip delivery, billing and persistence. Send returns a static "Success" with a random request_id; on check use code 123456 (any well-formed code returns a simplified "Approved" payload). Risk returns deterministic approved data. Malformed input still returns 400.

Pricing: Email Verification $0.03 per successful send ("Success"); "Retry" and "Undeliverable" sends and checks are free (apart from the optional social add-on); standalone calls have no free tier. No list price is published for POST /v3/email/risk/ (the docs say it is lower than Email Verification) or for the social footprint add-on (enable_social, billed per request on top of the endpoint price, not billed when the lookup could not complete). These are the list prices until 1 November 2026, when Didit pricing changes and the 500 free checks a month become $10 of free credit a month. Check didit.me/pricing for current prices.

API Reference: Send Code | Check Code | Email Risk Feature Guide: https://docs.didit.me/core-technology/email-verification/overview


Authentication

Every request needs the x-api-key header. Use your application's API key from the Didit Business Console under Developers > API keys, or create an account and key programmatically (see Getting Started). Named keys with custom access need Sessions write to run standalone checks; a key limited to some workflows or to approved sessions cannot run them.

Getting Started (No Account Yet?)

If you don't have a Didit API key, create one in 2 API calls:

  1. Register: POST https://apx.didit.me/auth/v2/programmatic/register/ with {"email": "[email protected]", "password": "MyStr0ng!Pass"}
  2. Check email for a 6-character verification code
  3. Verify: POST https://apx.didit.me/auth/v2/programmatic/verify-email/ with {"email": "[email protected]", "code": "A3K9F2"}. The key is in the response at application.api_key.

To add credits: GET /v3/billing/balance/ to check, POST /v3/billing/top-up/ with {"amount_in_dollars": 50} for a Stripe checkout link.

See the didit-verification-management skill for full platform management (workflows, sessions, users, billing).


Step 1: Send Email Code

POST https://verification.didit.me/v3/email/send/
Headers
HeaderValueRequired
x-api-keyYour API keyYes
Content-Typeapplication/jsonYes
Body (JSON)
ParameterTypeRequiredDefaultConstraintsDescription
emailstringYes-Valid email, max 254Address to send the code to
options.code_sizeintegerNo64-8Code length
options.alphanumeric_codebooleanNofalse-true = uppercase A-Z + 0-9 instead of digits only
options.localestringNoenOne of 54 codesTemplate language, e.g. en, es, fr, pt-BR, zh-CN, zh-TW. Unsupported values return 400 listing the supported set
options.use_white_label_customizationbooleanNofalse-Use your application's white-label branding instead of the Didit template
signals.ipstringNo-IPv4 or IPv6End user's IP for fraud detection
signals.device_idstringNo-Max 255Android ANDROID_ID, iOS identifierForVendor
signals.device_platformstringNo-android, ios, ipados, tvos, webDevice platform
signals.device_modelstringNo-Max 255e.g. iPhone17,2
signals.os_versionstringNo-Max 64e.g. 18.0.1
signals.app_versionstringNo-Max 64e.g. 1.2.34
signals.user_agentstringNo-Max 512Browser or app user agent
vendor_datastringNo--Your user id. Stored on the session (from the first send) and echoed in send, check and webhooks
metadataobjectNonullJSON objectFree-form data stored on the session and echoed back
Example
import requests

response = requests.post(
    "https://verification.didit.me/v3/email/send/",
    headers={"x-api-key": "YOUR_API_KEY", "Content-Type": "application/json"},
    json={
        "email": "[email protected]",
        "options": {"code_size": 6, "locale": "en"},
        "signals": {"ip": "203.0.113.42"},
        "vendor_data": "user-1234",
    },
    timeout=30,
)
print(response.status_code, response.json())
const response = await fetch("https://verification.didit.me/v3/email/send/", {
  method: "POST",
  headers: { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" },
  body: JSON.stringify({
    email: "[email protected]",
    options: { code_size: 6, locale: "en" },
    signals: { ip: "203.0.113.42" },
    vendor_data: "user-1234",
  }),
});
const data = await response.json(); // { request_id, status, reason, vendor_data, metadata }
Response (200 OK)
{
  "request_id": "e39cb057-92fc-4b59-b84e-02fec29a0f24",
  "status": "Success",
  "reason": null,
  "vendor_data": "user-1234",
  "metadata": null
}
Status Values & Handling
StatusMeaningAction
"Success"Code emailed for a new verification (billed)Wait for the user's code, then call Check
"Retry"Second send inside the 5-minute window: fresh code for the same verification, same request_id, previous code invalid (free)Wait for the new code, then call Check
"Undeliverable"The address failed syntax or DNS/MX validation, or the email could not be sent. reason is "email_can_not_be_delivered". No email sent, nothing billed, the verification is already finalized as Declined (a check returns "Expired or Not Found")Ask the user for a different address
Error Responses
CodeMeaningAction
400Invalid requestRead the field errors in the response body (malformed email, options.code_size outside 4-8, unsupported options.locale)
401The API key has expiredRotate it or create a new key in Developers > API keys
403Missing, wrong or revoked API key; a scoped key without access to this endpoint; a request from outside the key's IP allowlist; or not enough creditsRead the message in detail or error: fix the x-api-key header or the key's access, or top up
429Rate limit: 300 POST/PATCH/DELETE and 600 GET requests a minute per keyWait the Retry-After seconds, then retry

The credit check runs before any send: 403 with {"error": "You don't have enough credits to perform this request."}.


Step 2: Check Email Code

Verifies the code and returns the email report. Call Send first, with the same email.

POST https://verification.didit.me/v3/email/check/

Headers: same as Step 1.

Body (JSON)
ParameterTypeRequiredDefaultValuesDescription
emailstringYes-Valid emailSame address used in Step 1 (this links check to send)
codestringYes-Max 10 charsThe code the user received (4-8 characters, case-insensitive)
duplicated_email_actionstringNo"NO_ACTION""NO_ACTION" / "DECLINE"Address already approved for a different user (different vendor_data) of your application
breached_email_actionstringNo"NO_ACTION""NO_ACTION" / "DECLINE"Address found in known data breaches
disposable_email_actionstringNo"NO_ACTION""NO_ACTION" / "DECLINE"Domain is a disposable or temporary-mail provider
enable_socialbooleanNofalse-Also look up the address's social footprint (adds email.email_social; billed per request as an add-on)
email_no_social_presence_actionstringNo"NO_ACTION""NO_ACTION" / "DECLINE"Decline when the social footprint check finds the address on no platform (profiles_registered: 0). Needs enable_social: true

Policy note: DECLINE turns a correct code into "Declined" when the risk is found. NO_ACTION records the risk in email.warnings (log type information) without changing the status. Blocklisted and undeliverable addresses always decline. Only NO_ACTION and DECLINE are accepted here.

Example
response = requests.post(
    "https://verification.didit.me/v3/email/check/",
    headers={"x-api-key": "YOUR_API_KEY", "Content-Type": "application/json"},
    json={
        "email": "[email protected]",
        "code": "123456",
        "breached_email_action": "DECLINE",
        "disposable_email_action": "DECLINE",
    },
    timeout=30,
)
result = response.json()
if result["status"] in ("Approved", "Declined"):
    print(result["email"]["is_breached"], result["email"]["is_disposable"])
const response = await fetch("https://verification.didit.me/v3/email/check/", {
  method: "POST",
  headers: { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" },
  body: JSON.stringify({
    email: "[email protected]",
    code: "123456",
    breached_email_action: "DECLINE",
    disposable_email_action: "DECLINE",
  }),
});
const result = await response.json(); // inspect result.status, not the HTTP code
Response (200 OK)
{
  "request_id": "e39cb057-92fc-4b59-b84e-02fec29a0f24",
  "status": "Approved",
  "message": "The verification code is correct.",
  "email": {
    "status": "Approved",
    "email": "[email protected]",
    "is_breached": false,
    "breaches": [],
    "is_disposable": false,
    "is_undeliverable": false,
    "verification_attempts": 1,
    "verified_at": "2026-06-12T01:24:47.311323Z",
    "warnings": [],
    "lifecycle": [
      {"type": "EMAIL_VERIFICATION_MESSAGE_SENT", "timestamp": "2026-06-12T01:23:39.580554+00:00", "details": {"status": "Success", "reason": null}, "fee": 0.03},
      {"type": "VALID_CODE_ENTERED", "timestamp": "2026-06-12T01:24:47.311201+00:00", "details": {"code_tried": "123456", "status": "Approved"}, "fee": 0},
      {"type": "EMAIL_VERIFICATION_APPROVED", "timestamp": "2026-06-12T01:24:47.384292+00:00", "details": null, "fee": 0}
    ],
    "matches": []
  },
  "vendor_data": "user-1234",
  "metadata": null,
  "created_at": "2026-06-12T01:24:47.401719+00:00"
}

A wrong code with attempts left:

{
  "request_id": "11111111-2222-3333-4444-555555555555",
  "status": "Failed",
  "message": "The verification code is incorrect. Attempts remaining: 2",
  "email": null,
  "vendor_data": "user-1234",
  "metadata": null,
  "created_at": "2026-06-12T01:24:21.512340+00:00"
}
Status Values & Handling
StatusMeaningrequest_id / emailAction
"Approved"Correct code, no declining riskSession id of the send / full reportEmail verified, proceed
"Declined"Terminal: correct code but a declining risk matched (a DECLINE action, blocklisted or undeliverable address), or the 3rd wrong codeSession id of the send / full reportRead email.warnings; block or ask for another address
"Failed"Wrong code, attempts remaining (message says how many)One-off UUID / nullAsk the user to re-enter the code
"Expired or Not Found"No pending verification for this address in the last 5 minutesOne-off UUID / absentSend a new code (Step 1)
Error Responses
CodeMeaningAction
400Invalid requestRead the field errors in the response body (malformed email, missing code, action not NO_ACTION/DECLINE)
401The API key has expiredRotate it or create a new key in Developers > API keys
403Missing, wrong or revoked API key; a scoped key without access to this endpoint; a request from outside the key's IP allowlist; or not enough creditsRead the message in detail or error: fix the x-api-key header or the key's access, or top up
429Rate limit: 300 POST/PATCH/DELETE and 600 GET requests a minute per keyWait the Retry-After seconds, then retry

Checks are free; the only credit check is for the enable_social add-on (403 with error when the balance cannot cover it).


Email Risk (no code sent)

Scores an address in one synchronous call: deliverability, disposable domain, breaches, duplicates and normalized risk intelligence. No email is sent and the user does nothing. Creates an API session (request_id).

POST https://verification.didit.me/v3/email/risk/
Body (JSON)
ParameterTypeRequiredDefaultDescription
emailstringYes-Address to score
country_codestringNo-ISO 3166-1 alpha-2 or alpha-3 country hint (max 3 chars) for intelligence coverage
signalsobjectNo-Same fields as signals in Step 1 (ip, device_id, device_platform, device_model, os_version, app_version, user_agent)
enable_socialbooleanNofalseAdd the email.email_social block (billed per request as an add-on)
vendor_datastringNo-Your reference, stored and echoed
metadataobjectNonullYour JSON metadata, stored and echoed
Example
response = requests.post(
    "https://verification.didit.me/v3/email/risk/",
    headers={"x-api-key": "YOUR_API_KEY", "Content-Type": "application/json"},
    json={"email": "[email protected]", "country_code": "US", "vendor_data": "lead-123"},
    timeout=30,
)
result = response.json()
print(result["status"], result["email"]["is_breached"], result["email"]["is_disposable"])
const response = await fetch("https://verification.didit.me/v3/email/risk/", {
  method: "POST",
  headers: { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" },
  body: JSON.stringify({ email: "[email protected]", country_code: "US", vendor_data: "lead-123" }),
});
const result = await response.json();
Response (200 OK)
{
  "request_id": "82397be9-5921-4970-a979-2d5b43e08b33",
  "status": "Approved",
  "email": {
    "status": "Approved",
    "email": "[email protected]",
    "is_breached": false,
    "breaches": [],
    "is_disposable": false,
    "is_undeliverable": false,
    "verification_attempts": 0,
    "verified_at": null,
    "warnings": [],
    "lifecycle": [],
    "matches": [],
    "email_intelligence": {
      "score": 4.0,
      "deliverable": "high",
      "breach_risk_level": "Low",
      "breach_risk_index": 4,
      "is_recent_breach": false,
      "is_frequent_breached": false,
      "is_disposable": false,
      "is_free_domain": false,
      "is_breached": false,
      "no_of_breaches": 0,
      "reason_codes": []
    },
    "enrichment": {
      "score": 4.0,
      "deliverable": "high",
      "breach_risk_level": "Low",
      "breach_risk_index": 4,
      "is_recent_breach": false,
      "is_frequent_breached": false,
      "is_disposable": false,
      "is_free_domain": false,
      "is_breached": false,
      "no_of_breaches": 0,
      "reason_codes": []
    }
  },
  "vendor_data": "lead-123",
  "metadata": null,
  "created_at": "2026-09-01T10:30:00.123456+00:00"
}
  • status (top level and email.status): "Approved", "In Review" or "Declined". Normally "Approved"; your application's email-risk rules can produce the others. HTTP is 200 for every completed lookup.
  • email.email_intelligence and email.enrichment are the same object (compatibility alias). They are best-effort: both keys are absent when no usable intelligence came back. Build rules on is_breached, is_disposable and is_undeliverable first and treat a missing block as "unknown", not "safe".
  • Billed once the lookup starts, even when best-effort fields are missing.

Errors: same table as Step 2 (400 field errors, 401, 403 including the credit check for the call plus the enable_social add-on, 429).


Response Field Reference

email Object (check and risk)
FieldTypeDescription
statusstringCheck: "Approved" or "Declined" on finalized checks. Risk: "Approved", "In Review", "Declined"
emailstringThe address
is_breachedbooleanFound in known data breaches
breachesarrayUp to 5 most recent breaches: {name, domain, breach_date, breach_emails_count, description, logo_path, data_classes, is_verified}. description may contain HTML
is_disposablebooleanDomain is a disposable or temporary-mail provider
is_undeliverablebooleanCannot receive mail (syntax, DNS/MX, or bounced)
verification_attemptsintegerNumber of code sends for this verification (1, or 2 after a retry). 0 on risk lookups
verified_atstring or nullWhen the correct code was entered
warningsarray{feature, risk, additional_data, log_type, short_description, long_description}. log_type: error declines, warning flags for review, information is recorded only
lifecyclearray{type, timestamp, details, fee}. Types: EMAIL_VERIFICATION_MESSAGE_SENT, EMAIL_VERIFICATION_RETRY_MESSAGE_SENT, VALID_CODE_ENTERED, INVALID_CODE_ENTERED, EMAIL_VERIFICATION_APPROVED, EMAIL_VERIFICATION_DECLINED, EMAIL_VERIFICATION_IN_REVIEW, EMAIL_VERIFICATION_EXPIRED. fee (USD) is non-zero only on the first successful send
matchesarrayUp to 5 other sessions of your application where a different user (vendor_data) used this address: {session_id, session_number, vendor_data, verification_date, email, status, is_blocklisted, api_service, source}. source is session, or list_entry for a blocklist entry (null session_id/session_number)
email_intelligence / enrichmentobjectRisk endpoint only, best-effort: score (0-100, higher is riskier), deliverable, breach_risk_level (Low/Medium/High), breach_risk_index, is_recent_breach, is_frequent_breached, is_disposable, is_free_domain, is_breached, no_of_breaches, reason_codes (open list; do not reject unknown codes)
email_socialobjectOnly with enable_social: true and a completed lookup. See below
email_social Object
FieldDescription
profiles_registered / profiles_checkedPlatforms where the address is registered, out of the platforms checked. 0 registered is a real, billable answer (a common synthetic-identity signal)
names_found, photos_found, communication_breadth, digital_footprint_diversityCounts
digital_sophistication_index, engagement_depth_index, subscription_affordability_index0-10 composites
free_platforms, full_paid_platforms, partial_paid_platformsRegistrations by platform pricing model
registered_platforms, not_registered_platformsPlatform lists with category and scope (global or local)
categories, profilesPer-category registered count and ratio; per-platform profile details when exposed

Every count can be null when the lookup returned no data for it.


Warning Tags

TagSeverityDescription
EMAIL_CODE_ATTEMPTS_EXCEEDEDerror (always declines)The 3rd wrong code on the standalone check
EMAIL_IN_BLOCKLISTerror (always declines)Address on your email blocklist (Lists API) or matched a blocklisted verification. additional_data: blocklisted_session_id, blocklisted_session_number, api_service (all null for a pure list hit)
UNDELIVERABLE_EMAIL_DETECTEDerror (always declines)Address cannot receive mail
BREACHED_EMAIL_DETECTEDPer breached_email_actionFound in known breaches (see breaches[])
DISPOSABLE_EMAIL_DETECTEDPer disposable_email_actionDisposable or temporary-mail domain
DUPLICATED_EMAILPer duplicated_email_actionAlready approved for a different user. additional_data: duplicated_session_id, duplicated_session_number, api_service
EMAIL_IN_ALLOWLISTinformationAddress is on your email allowlist, so the duplicate action was skipped

At most one of EMAIL_IN_BLOCKLIST, DUPLICATED_EMAIL and EMAIL_IN_ALLOWLIST fires per attempt (blocklist first, then duplicate, then allowlist). On this API a DECLINE action gives log type error, NO_ACTION gives information.


Common Workflows

Basic Email Verification
1. POST /v3/email/send/   -> {"email": "[email protected]"}
   If "Undeliverable"     -> ask for another address (nothing billed)
2. Wait for the user to type the code (valid 5 minutes from the first send)
3. POST /v3/email/check/  -> {"email": "[email protected]", "code": "123456"}
4. If "Approved"             -> email verified
   If "Failed"               -> ask the user to retry (3 attempts in total)
   If "Declined"             -> read email.warnings
   If "Expired or Not Found" -> go back to step 1
Strict Security Verification
1. POST /v3/email/send/   -> include signals.ip, signals.device_id, signals.user_agent and vendor_data
2. Wait for the user's code
3. POST /v3/email/check/  -> duplicated_email_action, breached_email_action, disposable_email_action = "DECLINE"
4. If "Approved"  -> safe to proceed
   If "Declined"  -> email.warnings[].risk says why; block or ask for another address
Pre-screen Without a Code
1. POST /v3/email/risk/   -> {"email": "[email protected]", "vendor_data": "lead-123"}
2. If email.is_undeliverable or email.is_disposable -> reject before sending any OTP
3. Otherwise continue with the send/check flow above

Utility Scripts

verify_email.py: Send codes, check codes and score email risk from the command line.

# Requires: pip install requests
export DIDIT_API_KEY="your_api_key"

python scripts/verify_email.py send [email protected] --locale es
python scripts/verify_email.py check [email protected] 123456 --decline-breached --decline-disposable
python scripts/verify_email.py risk [email protected] --country-code US

Can also be imported as a library:

from scripts.verify_email import send_code, check_code, score_email_risk

send_result = send_code("[email protected]")
check_result = check_code("[email protected]", "123456", decline_breached=True)
risk_result = score_email_risk("[email protected]")

Comments

Markdown supported