Email Verification API: JavaScript Example

Published 2026-08-17 · Reviewed by Engineering / Technical Team
Quick answer

Send a POST request to /v1/email/check with an Authorization: Bearer header carrying your API key and a JSON body containing the email address. The response includes a decision (allow/review/block), a risk score with confidence, and the specific signals behind it. Below is a complete, working example in JavaScript, plus the equivalent cURL and error-handling pattern.

A basic request

check-email.js
const response = await fetch("https://api.emailriskradar.com/v1/email/check", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.EMAIL_RISK_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    email: "user@example.com",
    mode: "standard",   // "fast" | "standard" | "full"
    context: "signup",  // "generic" | "signup" | "b2b_lead" | "crm" | "marketplace" | "payment"
  }),
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`Email check failed: ${error.code} - ${error.message}`);
}

const result = await response.json();
console.log(result.decision, result.risk.score, result.risk.level);

Both mode and context are optional and default to "standard" and "generic" respectively — set context explicitly whenever the form has a specific purpose (see How to Stop Fake SaaS Signups for why the signup context specifically matters).

The same request in cURL

request.sh
curl https://api.emailriskradar.com/v1/email/check \
  -H "Authorization: Bearer $EMAIL_RISK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com", "mode": "standard", "context": "signup"}'

Reading the response

response.json
{
  "email": "user@example.com",
  "decision": "allow",
  "risk": { "score": 5, "level": "low", "confidence": 0.8 },
  "verification": { "status": "likely_valid", "domain": "verified", "mailbox": "unconfirmed", "confidence": 0.75 },
  "deliverability": { "status": "likely_deliverable", "confidence": 0.75 },
  "email_quality": { "score": 95, "classification": "normal" },
  "signals": [],
  "checks": {
    "syntax_valid": true,
    "domain_exists": true,
    "mx_found": true,
    "smtp_check": "unknown",
    "disposable": false,
    "free_provider": false,
    "role_based": false,
    "catch_all": false,
    "typo_detected": false
  },
  "meta": { "processing_ms": 84, "cached": false, "mode": "standard", "context": "signup" }
}

In practice, most application code only needs to branch on result.decision, and read result.risk and result.signals when you want to log or display why a decision was made:

handle-decision.js
switch (result.decision) {
  case "allow":
    await createAccount(email);
    break;
  case "review":
    await createAccount(email, { requiresConfirmation: true });
    await sendConfirmationEmail(email);
    break;
  case "block":
    throw new Error("This email address can't be used to sign up.");
}

Handling errors

Errors follow a consistent shape — { error: { code, message, request_id } } — with an HTTP status matching the error code, so you can branch on either:

error.codeHTTP statusTypical cause
INVALID_REQUEST400Missing or malformed email field
AUTHENTICATION_REQUIRED / INVALID_API_KEY401Missing, malformed, or revoked API key
RATE_LIMIT_EXCEEDED429Too many requests per second for your plan — back off and retry
QUOTA_EXCEEDED402Monthly check quota exhausted
SERVICE_UNAVAILABLE503A dependency (e.g. Stripe, for billing routes) isn't configured — not expected on /email/check
error-handling.js
try {
  const result = await checkEmail(email);
  return result;
} catch (err) {
  if (err.code === "RATE_LIMIT_EXCEEDED") {
    await sleep(1000);
    return checkEmail(email); // simple retry - use exponential backoff in production
  }
  if (err.code === "QUOTA_EXCEEDED") {
    // fail open or closed depending on how critical the check is to your flow
    console.error("Email risk quota exhausted", err.message);
    return null;
  }
  throw err;
}

API keys and environments

Keys are issued per environment (live or test) from the dashboard, and never appear anywhere in a response — store the key in an environment variable, never hard-code it, and never expose it to client-side code. Every request must be made server-side, since the key is a bearer credential with no additional per-request scoping.

Checking many addresses at once

For batches rather than one-at-a-time checks, POST /v1/email/bulk accepts an array of addresses and processes them asynchronously — poll GET /v1/email/bulk/:jobId for status, or register a webhook endpoint to be notified when the batch completes, instead of holding a connection open for a large batch.

FAQ

Does the API support TypeScript?

The API itself is just JSON over HTTP, so any TypeScript project works with the same fetch call — just type the response shape yourself, or generate types from the documented response schema.

What's the difference between mode: "fast", "standard", and "full"?

Fast only consults cached DNS/domain data for lowest latency. Standard performs live DNS lookups. Full adds a live SMTP handshake to the mail server, which is slower but gives the strongest deliverability signal. See How SMTP Email Verification Works for what full mode adds specifically.

Can I call this API directly from the browser?

No — API keys are bearer credentials meant for server-side use only. Calling from the browser would expose your key to anyone viewing the page source or network requests.

Is there a rate limit?

Yes, per-second rate limits and monthly quotas are set by your plan. A 429 response with RATE_LIMIT_EXCEEDED means back off and retry; a 402 with QUOTA_EXCEEDED means the monthly allowance is used up.

See how this looks against a real address, or start checking your own traffic.

We use Google Analytics to understand site traffic, and only load it if you accept — nothing runs before you choose. Signing in still stores a strictly necessary session token regardless. See the Privacy Policy for details.