API Documentation

Authenticate with an nxv_ API key, verify single or bulk emails, poll jobs, export results, and receive signed completion webhooks. Integrate in about 10 minutes with the quickstart below.

Download OpenAPIOpenAPI 3 spec at https://api.nexiphorverifier.com/openapi/v1.json

Quickstart

Create a sandbox key (nxv_test_) in Dashboard → API Keys, then run one of these — no credits charged, deterministic fake results:

# 1) Create a sandbox key in Dashboard → API Keys (prefix nxv_test_)
# 2) Verify one address (waits up to 15s for a result)
curl -X POST "https://api.nexiphorverifier.com/api/v1/verify/single?wait=true&waitTimeoutMs=15000" \
  -H "Authorization: Bearer nxv_test_YOUR_SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com"}'

Response (synchronous wait): 200 when the job finishes in time. Job status is Completed; the verdict lives on result.status (Valid / Invalid / Risky / Unknown). Use result.score, result.reasonCode, and result.verificationMethod (sandbox here; live SMTP uses values like smtp_positive). On live keys, Unknown / Failed are refunded.

{
  "jobId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "status": "Completed",
  "email": "user@example.com",
  "result": {
    "email": "user@example.com",
    "isValid": true,
    "status": "Valid",
    "score": 88,
    "reason": "Sandbox: recipient accepted (simulated). This is not proof the mailbox exists.",
    "reasonCode": "recipient_accepted_discriminated",
    "verificationMethod": "sandbox",
    "confidenceLevel": "High"
  }
}

Swap to a live nxv_ key when you are ready for real SMTP probes. Prefer an official client? See SDKs below.

Authentication

Create a key in Dashboard → API Keys. Send it as a Bearer token (preferred) or X-Api-Key header. Keys use scopes verify:single, verify:bulk, finder:single, and finder:bulk. There is no rotate endpoint — revoke a key and create a new one (optionally with the same scopes and IP allowlist).

Authorization: Bearer nxv_your_key_here
# or
X-Api-Key: nxv_your_key_here

Sandbox keys

Create a sandbox key (prefix nxv_test_) to integrate without burning credits or hitting SMTP. Results are deterministic fakes (verificationMethod: "sandbox"). Bulk sandbox is capped at 100 emails. Sandbox does not run real syntax/IDN short-circuits — addresses are hashed into Valid / Invalid / Risky / Unknown buckets. Use a live key to exercise invalid_syntax and Unicode local-part rejection.

Authorization: Bearer nxv_test_...

Single verify

POST https://api.nexiphorverifier.com/api/v1/verify/single returns 202 Accepted with a job id by default. Poll until complete, or opt into a synchronous wait.

curl -X POST https://api.nexiphorverifier.com/api/v1/verify/single \
  -H "Authorization: Bearer nxv_..." \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com"}'

# Poll
curl https://api.nexiphorverifier.com/api/v1/verify/single/{jobId} \
  -H "Authorization: Bearer nxv_..."

Accepted response shape:

{
  "jobId": "uuid",
  "status": "Queued",
  "email": "user@example.com",
  "pollUrl": "/api/v1/verify/single/{jobId}",
  "hubUrl": "/hubs/verification"
}

Synchronous wait (optional): add ?wait=true or header X-Nexiphor-Wait: true. Optional waitTimeoutMs (5,000–30,000, default 20,000). Returns 200 with the completed job payload if finished in time; otherwise 202 and you must poll. Bulk jobs stay async only.

curl -X POST "https://api.nexiphorverifier.com/api/v1/verify/single?wait=true&waitTimeoutMs=15000" \
  -H "Authorization: Bearer nxv_..." \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com"}'

Result fields

Completed single-job and bulk export rows share the same verification shape. status is one of Valid, Invalid, Risky, or Unknown. score is a ranking score, not a calibrated probability. Catch-all rows remain Risky regardless of score; a given value is not a percent chance of deliverability. Unknown and Failed verifications are refunded. The example below is a live SMTP outcome (verificationMethod: "smtp_positive"); sandbox keys return "sandbox" instead.

{
  "email": "user@example.com",
  "isValid": true,
  "status": "Valid",
  "score": 88,
  "reason": "The mail server accepted this recipient and rejected a randomized address.",
  "reasonCode": "recipient_accepted_discriminated",
  "suggestedEmail": null,
  "typoDetected": false,
  "provider": null,
  "providerName": null,
  "verificationMethod": "smtp_positive",
  "confidenceLevel": "VeryHigh",
  "checks": {
    "syntaxValid": true,
    "domainExists": true,
    "mxFound": true,
    "isDisposable": false,
    "smtpAccepted": true,
    "isCatchAll": false,
    "isRoleAccount": false,
    "isFreeEmail": true,
    "isToxicDomain": false,
    "isPossibleSpamTrap": false,
    "catchAllProvider": null,
    "domainDnsState": "exists",
    "mxState": "present"
  },
  "telemetry": {
    "mxHost": "gmail-smtp-in.l.google.com",
    "smtpCode": 250,
    "smtpResponse": "2.1.5 OK",
    "tlsUsed": true
  }
}

DNS evidence is tri-state for display and Full export: keep the boolean wire fields (domainExists / mxFound), but treatchecks.domainDnsState of transient_failure ornot_checked (and mxState=not_checked) as Unknown — never as a definitive "not found". Confirmed NXDOMAIN setstelemetry.dnsNxDomainConfirmed=true.

Common reasonCode values:

  • invalid_syntax / invalid_domain / disposable_domain → Invalid
  • mx_not_found / smtp_rejected / provider_account_not_found → Invalid
  • mx_hosts_unresolvable → Invalid. The domain publishes MX records, but none of the named mail servers exist in DNS.
  • gateway_recipient_marked_invalid → Invalid. The receiving gateway's RCPT reply names this recipient as invalid.
  • gateway_discriminated / provider_discriminated → Risky, send with caution. A filtering gateway or a non-Microsoft provider accepted the recipient and rejected a randomized address; this does not prove the mailbox exists. classification.result is most_likely_deliverable when a directory signal also supports the recipient, otherwise provider_unverifiable.
  • recipient_accepted_discriminated → Valid, send with caution. The server accepted the recipient and rejected a randomized address.
  • recipient_accepted_enumerating_provider → Valid, send with caution. The provider accepted the recipient; no randomized-address comparison was observed.
  • mailbox_exists → Valid only for an authoritative directory confirmation, or a sandbox simulation. Not the name of a 250.
  • catch_all_detected / provider_non_authoritative_rcpt → Risky
  • smtp_blocked / smtp_timeout / dns_timeout → Unknown (refunded)
  • toxic_domain / possible_spam_trap → Risky (also exported as is_toxic_domain / is_possible_spam_trap)

Bulk CSV/TSV/XLSX/JSON exports include these columns plus is_toxic_domain and is_possible_spam_trap. Prefer the valid_and_safe_catchall export preset when you want Valid plus all catch-alls (use with caution). The wire preset id is retained for API compatibility; the product label is "Valid + all catch-alls (use with caution)". Bulk summary counts use policy names: policyCatchAllCount (legacy safeCatchAll) and validAndPolicyCatchall (legacy validAndSafeCatchall); both names are returned. When rows change after the job completed (automatic re-check of pending rows), the summary adds resultsRevisedAt and revisedRowCount; completedAt keeps the first completion time and refunds are never reversed.

Team defaults (/api/v1/teams/verification-defaults): safeCatchAllMinScore is deprecated — still accepted, ignored, and no longer range-checked; Score is not a catch-all safety gate. Responses add effectiveDefaultExportPreset and warnings: with a Valid-only catchAllTreatment (risky_only or legacy safe_only), a default preset that adds every catch-all (valid_and_safe_catchall, reach_with_caution) resolves to maximum_deliverability and saving that pair returns valid_only_treatment_default_preset_includes_catch_alls. The stored preset is not rewritten.

Segmented ZIP export (/export/zip) splits into one CSV per recommended action: safe-to-send.csv, send-with-caution.csv, review-before-sending.csv, try-again.csv, do-not-send.csv — matching Campaign Nexiphor Recommended Action, not legacy status aliases.

Export columns query default is campaign(customer-facing file). Pass full, technical, or forensic for the technical/forensic file. CampaignNexiphor Decision matches Nexiphor Recommended Action (Decision is deprecated and may be removed in a later release); likewise Nexiphor Result duplicates Nexiphor Classification and is deprecated. Campaign files end with Nexiphor Unknown Reason, Nexiphor Control Check and Nexiphor Evidence Tier; Full files end with unknown_reason, control_check, evidence_tier (after conflict_codes). conflict_codes lists evidence that disagreed, for example directory_negative_smtp_accept, directory_positive_smtp_reject or domain_control_flip (the gateway answered this job's test addresses both ways); a conflict never changes the verdict. The same values are on the API result as unknownCause (Unknown rows only), controlCheck and evidenceTier. Status stays Valid for a discriminating accept — read evidenceTier (confirmed vs discriminated) and recommendedAction for send decisions. In Full exports, confidence_level is verdict certainty (how sure we are of the classification), not email quality or a calibrated delivery probability. Full CSV/TSV/XLSX no longer include always-empty columns inferred_mailbox_backend_provider, calibrated_probability, calibration_sample_size, calibration_version, or calibration_status (API JSON unchanged). riskCategory / export risk_category is populated again from the canonical reason code (CatchAll, Unverifiable, Degraded, PolicyRisk, or None). Catch-all rows remain Risky because domain-wide acceptance does not confirm an individual mailbox.

Internationalized domains (IDN) and locale

Unicode domain labels are supported. Before DNS and SMTP, addresses are normalized with Punycode (ACE). For example, test@日本語.jp is converted to an ASCII domain and then verified like any other address. Malformed Punycode labels fail closed as invalid_syntax.

SMTPUTF8 / Unicode local parts are not supported on live keys. Addresses such as ユーザー@example.com return invalid_syntax even when the domain is valid. Probes use ASCII SMTP; this keeps bulk lists aligned with deliverability on mainstream MX hosts. Sandbox keys skip this check (see Sandbox keys above).

Bare IP hostnames and reserved domains are rejected as invalid_domain. Dashboard UI text is English; date and number formatting may follow the browser locale.

Bulk verify

JSON list (requires fileName plus emails), paste text via /verify/bulk/paste, or CSV upload. Optional callbackUrl receives a completion webhook.

Concurrent bulk jobs: each plan includes soft parallel bulk limits (Business 4, Agency 6, Enterprise custom). Starting another bulk job while at the limit queues or waits — contact contact@nexiphor.com for Business+ concurrency packs.

# JSON emails (fileName is required)
curl -X POST https://api.nexiphorverifier.com/api/v1/verify/bulk \
  -H "Authorization: Bearer nxv_..." \
  -H "Content-Type: application/json" \
  -d '{"fileName":"list.json","emails":["a@x.com","b@y.com"],"callbackUrl":"https://hooks.zapier.com/..."}'

# Paste text
curl -X POST https://api.nexiphorverifier.com/api/v1/verify/bulk/paste \
  -H "Authorization: Bearer nxv_..." \
  -H "Content-Type: application/json" \
  -d '{"text":"a@x.com\nb@y.com"}'

# CSV upload
curl -X POST https://api.nexiphorverifier.com/api/v1/verify/bulk/upload \
  -H "Authorization: Bearer nxv_..." \
  -F "file=@list.csv" \
  -F "callbackUrl=https://hooks.zapier.com/..."

# Poll summary
curl https://api.nexiphorverifier.com/api/v1/verify/bulk/{batchId}/summary \
  -H "Authorization: Bearer nxv_..."

Export

Authenticated download: GET https://api.nexiphorverifier.com/api/v1/verify/bulk/{batchId}/export

Webhooks include an absolute exportUrl and a short-lived exportToken. Send that token in the X-Export-Token header (query-string ?token= is rejected). Lifetime is up to 24h, or until zero-storage purge. No API key is required for that URL. The token is cleared when the batch is purged.

Webhooks

Prefer the Webhook Manager: register durable public http or https endpoints (ports 80/443 only; no localhost or private IPs) in Dashboard → Webhooks or use the Zapier / Make recipes for Catch Hook setup. Subscribe to batch.completed and verification.completed, inspect delivery history, and retry failures. Managed webhook create / rotate / test require a Pro plan (otherwise 403 with code: "plan_required"). Per-batch callbackUrl remains a one-shot override. Failed deliveries retry with backoff (1m, 5m, 15m, 1h; up to 4 attempts). Opt in to bulk.results_revised to hear when rows of a completed bulk change afterwards (sent once the changes are quiet for 5 minutes; fetch exportUrl with your API key).

curl -X POST https://api.nexiphorverifier.com/api/v1/webhooks/endpoints \
  -H "Authorization: Bearer nxv_..." \
  -H "Content-Type: application/json" \
  -d '{"name":"Zapier","url":"https://hooks.zapier.com/...","events":["batch.completed"]}'

Generate a signing secret on the Webhooks page or in Settings → Privacy. Verify X-Nexiphor-Signature: sha256=... as HMAC-SHA256 of the raw body. Deduplicate with eventId / X-Nexiphor-Event-Id.

{
  "eventId": "batch.completed:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "batchId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  "status": "Completed",
  "totalCount": 1000,
  "completedCount": 1000,
  "results": { "valid": 820, "invalid": 100, "risky": 50, "unknown": 30 },
  "exportUrl": "https://api.nexiphorverifier.com/api/v1/verify/bulk/.../export",
  "exportToken": "<opaque>",
  "exportTokenHeader": "X-Export-Token",
  "exportExpiresAt": "2026-07-11T12:00:00Z",
  "completedAt": "2026-07-10T12:00:00Z"
}

Email Finder

Resolve a professional address from name + domain. Only SMTP-proven Valid results are returned — catch-all domains abort without a guess. Success costs 5 credits; misses cost 0. Requires API scopes finder:single / finder:bulk.

curl -X POST https://api.nexiphorverifier.com/api/v1/finder/single \
  -H "Authorization: Bearer nxv_..." \
  -H "Content-Type: application/json" \
  -d '{"firstName":"Ada","lastName":"Lovelace","domain":"example.com"}'

List recent single jobs: GET /api/v1/finder/single. Pro plan required. Webhook events include job.failed as an alias of verification.failed. After rotating a webhook signing secret,X-Nexiphor-Signature-Previous is sent for 24h.

Bulk CSV upload:

curl -X POST https://api.nexiphorverifier.com/api/v1/finder/bulk/upload \
  -H "Authorization: Bearer nxv_..." \
  -F "file=@people.csv"

Domain Intelligence

GET /api/v1/domains/lookup?domain=example.com (authenticated). Returns MX presence, provider hints, and a heuristic catch-all likelihood. This is not an SMTP RCPT probe of a specific mailbox — use single/bulk verify for mailbox-level truth.

curl "https://api.nexiphorverifier.com/api/v1/domains/lookup?domain=example.com" \
  -H "Authorization: Bearer nxv_..."

Result cache

Teams can enable a result cache TTL (days) under Settings. Cache hits return verificationMethod: "result_cache" and may include fromCache: true on the enqueue response. Pass forceRefresh: true on single/bulk verify to bypass cache. Owners/Admins can purge with POST /api/v1/teams/result-cache/purge.

Verification profiles

Apply a named profile (export/unknown defaults) with POST /api/v1/teams/verification-profiles/apply body {"profileId":"cold_email"}. List profiles via GET /api/v1/teams/verification-profiles.

Rate limits

Verify and finder endpoints are rate limited per API key (default ~120 requests per minute). Each key has its own bucket; JWT dashboard sessions share a per-user bucket. Exceeding the limit returns 429 with Retry-After and X-RateLimit-* headers. Opt-in synchronous single-verify waits use a tighter limit (~30/min). Polling job status uses a separate, higher quota.

SDKs

Official thin clients for Node and Python wrap single verify, bulk, status, and export.

Node — @nexiphor/verifier on npm

npm i @nexiphor/verifier

import { createClient } from '@nexiphor/verifier';

const client = createClient({ apiKey: process.env.NXV_API_KEY });
const job = await client.verifyEmail('user@example.com', { wait: true });
console.log(job.status, job.result);

Python — nexiphor-verifier on PyPI

pip install nexiphor-verifier

from nexiphor_verifier import create_client

client = create_client(api_key="nxv_test_...")
job = client.verify_email("user@example.com", wait=True)
print(job.status, job.result)

For codegen against the full surface, download the OpenAPI spec.

Errors

JSON error bodies typically include a message / Message (and sometimes a machine code). Request validation uses 400 with a failures / Failures map — not HTTP 422.

  • 400 — request validation failed (empty email, missing bulk fileName, private webhook URL, etc.). Malformed addresses such as not-an-email are still accepted as jobs; on live keys they complete as Invalid / invalid_syntax rather than failing at the HTTP layer
  • 401 — missing/invalid session or API key; expired export token
  • 403 — missing API key scope, or plan gate (requiredPlan for webhooks / integrations)
  • 402 — insufficient credits (live keys only)
  • 429 — rate limited (~120/min per API key by default). Check Retry-After and X-RateLimit-* headers.

Example validation error (empty email):

{
  "Message": "One or more validation failures occurred.",
  "Failures": {
    "Email": ["Email is required."]
  }
}

401 — invalid or missing API key:

{
  "message": "Invalid API key."
}

402 — insufficient credits (live keys):

{
  "code": "insufficient_credits",
  "message": "Insufficient credits to start this verification."
}

429 — rate limited (also set Retry-After):

{
  "message": "Too many requests. Please try again later."
}

Error handling

Check HTTP status before parsing the success body. Read message / Message, optional code, and validation Failures. On 429, sleep for Retry-After seconds (fallback 1s) and retry.

async function verifyOnce(email, apiKey) {
  const res = await fetch(
    "https://api.nexiphorverifier.com/api/v1/verify/single?wait=true",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ email }),
    }
  );
  const body = await res.json().catch(() => ({}));
  if (res.status === 429) {
    const retryAfter = Number(res.headers.get("Retry-After") || 1);
    await new Promise((r) => setTimeout(r, retryAfter * 1000));
    return verifyOnce(email, apiKey);
  }
  if (!res.ok) {
    const msg = body.message ?? body.Message ?? res.statusText;
    const code = body.code ? ` (${body.code})` : "";
    throw new Error(`HTTP ${res.status}${code}: ${msg}`);
  }
  return body;
}

Zero-storage

Enable in Settings or send X-Nexiphor-Zero-Storage: true. Results are kept only for your retention window (shortened after a successful webhook). Download before purgeAfterUtc.

Chrome extension

A browser extension is packaged in the Nexiphor frontend repo for one-click verify from CRM tabs. Chrome Web Store listing is pending founder submit — use API keys or the dashboard until the Store link is live. See also Dashboard → API Keys.

Base URL

Production: https://api.nexiphorverifier.com