API docs

One POST request checks an address and returns a verdict, a 0–100 score and the reasons behind it. A free account gives you 100 checks a day: get your API key.

Authentication

Every request needs an API key. Sign in with your email to create one; it's shown once, so store it somewhere safe. Send it in the Authorization header:

Authorization: Bearer ls_your_key_here

An x-api-key: ls_your_key_here header works too. Keep keys on your server: anyone with a key can spend its quota. If a key leaks, revoke it on your account page; that takes effect within a minute.

Request

POST https://api.legitsignup.com/v1/check with a JSON body:

POST /v1/check
Authorization: Bearer ls_your_key_here
Content-Type: application/json

{ "email": "Jane.Doe+news@gmail.con" }

Send content-type: application/json. The body may be at most 1 KB and the address at most 320 characters. Addresses are only accepted in the body: a GET with the address in the URL is refused, because URLs end up in logs.

Response

A 200 response describes the address. This is a real response for the request above:

{
  "email": "Jane.Doe+news@gmail.con",
  "verdict": "invalid",
  "score": 0,
  "reasons": [
    { "code": "typo_suspected", "message": "Did you mean gmail.com?" },
    { "code": "no_mail_server", "message": "The domain does not exist." }
  ],
  "suggestion": "Jane.Doe+news@gmail.com",
  "canonical": "jane.doe+news@gmail.con",
  "flags": {
    "syntaxValid": true,
    "role": false,
    "freeProvider": false,
    "disposable": false,
    "typo": true,
    "dnsChecked": true,
    "mailServer": false,
    "nullMx": false,
    "burnerMx": false,
    "parked": false,
    "newDomain": false,
    "randomLocalPart": false
  }
}
FieldTypeMeaning
emailstringThe address as checked: trimmed, with the domain lowercased and international domains in punycode.
verdictstringok, risky or invalid. See Verdicts.
scorenumber0–100. Starts at 100 and loses points per reason.
reasonsarrayEvery problem found, each with a stable code and a human-readable message. Empty for a clean address.
suggestionstring | nullThe corrected address when the domain looks mistyped. Show it as “Did you mean …?”, never apply it silently.
canonicalstring | nullThe address with provider aliases removed (Gmail dots and +tags, Yahoo -keywords), lowercased. Compare it across your users to catch one person creating many accounts. null when the syntax is invalid.
flagsobjectEvery individual signal as a boolean. See Flags.

Verdicts

VerdictWhenSuggested handling
okScore 70 or more, no hard failure.Accept.
riskyScore below 70, no hard failure.Show the suggestion if there is one; otherwise accept but require email confirmation, or add friction such as a captcha.
invalidAny hard failure: bad syntax, a domain that doesn't exist or can't receive mail. Score is 0.Ask the user to fix the address.

No verdict means the mailbox exists. We never contact the recipient's mail server, so ok means the address is well-formed, its domain accepts email, and nothing about it looks fake.

Reason codes

Codes are stable; messages may change. Each code counts once.

CodeEffectMeaning
syntax_invalidinvalidThe address is malformed: bad characters, misplaced dots, too long, quoted local part, or an IP address as the domain.
no_mail_serverinvalidThe domain doesn't exist, or has no MX record and no address to fall back to.
null_mxinvalidThe domain publishes a “null MX” record (RFC 7505): it declares that it accepts no email.
mx_invalid_hostinvalidEvery mail server points to localhost or a private address.
disposable−70The domain is a known disposable or throwaway email service.
burner_domain−60Our domain intelligence has seen this domain serving disposable inboxes.
burner_mx−60The domain's mail is handled by a known disposable email service.
parked_domain−40The domain is parked or for sale, or its mail goes to a parking service.
typo_suspected−40The domain looks like a misspelling of a popular provider. See suggestion.
new_domain−30The domain was registered less than 30 days ago.
role_address−15A role or team address such as info@, admin@ or noreply@.
no_mx_record−10No MX record; mail would go to the domain's web server address.
dns_unavailable−10The domain's DNS couldn't be looked up right now. The result is incomplete; retrying later may help.
random_local_part−10The part before @ looks randomly typed or keyboard-mashed. A weak signal on its own.

Flags

FlagMeaning
syntaxValidThe address passed syntax checks. When false, no other check ran.
roleA role or team address.
freeProviderA consumer provider such as Gmail or Outlook. Not a penalty; useful if you only want work addresses.
disposableOn our disposable domain list.
typoA typo was suspected and suggestion is set.
dnsCheckedDNS was looked up for this check.
mailServertrue if the domain can receive mail, false if it can't, null if unknown.
nullMxThe domain declares it accepts no mail.
burnerMxThe mail servers belong to a disposable email service.
parkedThe domain is parked or for sale.
newDomainRegistered less than 30 days ago.
randomLocalPartThe part before @ looks random.

Errors

Errors return { "error": { "code": "…", "message": "…" } }.

StatusCodeCause
400invalid_jsonThe body isn't valid JSON.
400missing_emailThe body has no non-empty email string.
400email_too_longThe address is longer than 320 characters.
401api_key_requiredNo API key was sent. See Authentication.
401invalid_api_keyThe key doesn't exist or was revoked.
404not_foundUnknown path. The only endpoint is /v1/check.
405method_not_allowedAnything but POST (or OPTIONS).
413body_too_largeThe body is larger than 1 KB.
415unsupported_media_typeThe content type isn't application/json.
429quota_exceededThe account has used today's checks. retry-after says how many seconds until the quota resets at midnight UTC.
429rate_limitedMore than 30 requests in a minute. Wait for the number of seconds in retry-after.
500internal_errorSomething failed on our side. Retry, and fail open: let the signup through.

Limits

Free accounts get 100 checks a day, shared by all of the account's keys, and at most 30 requests a minute. The daily quota resets at midnight UTC. Requests rejected before checking (bad JSON, a missing email) don't count.

Every response to a request with a key reports the quota:

HeaderMeaning
x-ratelimit-limitChecks allowed per day.
x-ratelimit-remainingChecks left today.
x-ratelimit-resetSeconds until the quota resets.

If the API is slow, unreachable or over quota, let the signup through. A missed check is cheaper than a lost user.

Examples

curl

curl -X POST https://api.legitsignup.com/v1/check \
  -H "authorization: Bearer $LEGITSIGNUP_KEY" \
  -H "content-type: application/json" \
  -d '{"email": "jane@gmial.com"}'

JavaScript (Node 18+)

async function checkEmail(email) {
  try {
    const res = await fetch("https://api.legitsignup.com/v1/check", {
      method: "POST",
      headers: {
        authorization: `Bearer ${process.env.LEGITSIGNUP_KEY}`,
        "content-type": "application/json",
      },
      body: JSON.stringify({ email }),
      signal: AbortSignal.timeout(3000),
    });
    if (!res.ok) return null; // fail open
    return await res.json();
  } catch {
    return null; // fail open
  }
}

const result = await checkEmail("jane@gmial.com");
if (result?.verdict === "invalid") {
  // ask the user to fix the address
} else if (result?.suggestion) {
  // show: Did you mean result.suggestion?
}

Python

import os
import requests

def check_email(email: str) -> dict | None:
    try:
        res = requests.post(
            "https://api.legitsignup.com/v1/check",
            json={"email": email},
            headers={"authorization": f"Bearer {os.environ['LEGITSIGNUP_KEY']}"},
            timeout=3,
        )
        res.raise_for_status()
        return res.json()
    except requests.RequestException:
        return None  # fail open

result = check_email("jane@gmial.com")
if result and result["verdict"] == "invalid":
    ...  # ask the user to fix the address

PHP

function check_email(string $email): ?array {
    $ch = curl_init('https://api.legitsignup.com/v1/check');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => [
            'authorization: Bearer ' . getenv('LEGITSIGNUP_KEY'),
            'content-type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode(['email' => $email]),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 3,
    ]);
    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    return ($body !== false && $status === 200) ? json_decode($body, true) : null; // null = fail open
}

Privacy

We never store the addresses you check. DNS results are cached per domain for at most 24 hours, logs record the domain and the verdict, never the address, and checked domains are kept for 30 days to research their age. Responses are sent with cache-control: no-store.

Your own account email is stored so you can sign in, along with daily check counts. Deleting your account removes both.