API Documentation

Run SPF, DKIM, DMARC and blacklist checks from your own code. One endpoint, JSON in, JSON out.

Overview

  • Base URL: https://em-check.com
  • Format: JSON request and response bodies, UTF-8
  • Availability: the API is included in the Agency plan

Authentication

Every API request is authenticated with an API key. Agency subscribers can create up to 5 keys in Settings → API Keys. Keys start with ek_live_ and are shown only once — store them in a secret manager or environment variable.

Send the key in the Authorization header:

Authorization: Bearer ek_live_YOUR_API_KEY

Never embed a key in client-side code or commit it to a repository. If a key leaks, revoke it in Settings — revocation takes effect immediately.

Check a domain

POST/api/domain/check

Runs the full deliverability check: SPF, DKIM (common selectors), DMARC, IP and domain blacklists, MX and other infrastructure checks. A fresh check typically takes a few seconds.

Request body

FieldTypeDescription
domainstring, requiredDomain to check, e.g. example.com. A leading https:// and any path are stripped automatically.

Examples

cURL
curl -X POST https://em-check.com/api/domain/check \
  -H "Authorization: Bearer ek_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain": "example.com"}'
JavaScript (Node 18+)
const response = await fetch('https://em-check.com/api/domain/check', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.EMAIL_CHECK_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ domain: 'example.com' }),
});

const data = await response.json();

if (!data.success) {
  throw new Error(`${data.error.code}: ${data.error.message}`);
}

console.log(data.data.result.grade, data.data.result.totalScore);
Python
import os
import requests

response = requests.post(
    "https://em-check.com/api/domain/check",
    headers={"Authorization": f"Bearer {os.environ['EMAIL_CHECK_API_KEY']}"},
    json={"domain": "example.com"},
    timeout=60,
)

data = response.json()
if not data["success"]:
    raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")

result = data["data"]["result"]
print(result["grade"], result["totalScore"])

Response format

A successful response has success: true and a data object with a checkId and the full result. The check is also saved to your history, and you can open it in the browser at https://em-check.com/check/{checkId}.

Result fields

FieldTypeDescription
domainstringNormalized domain that was checked
checkedAtstring (ISO 8601)When the check was performed
totalScorenumberOverall deliverability score, 0–100
grade"A" | "B" | "C" | "D" | "F"Letter grade derived from the score
cachedbooleantrue if the result was served from the 24-hour cache
spfobjectSPF record, validity, DNS lookup count, mechanisms, errors and warnings (score 0–20)
dkimobjectProbed selectors, valid selectors and their public key records (score 0–25)
dmarcobjectDMARC record, policy, subdomain policy, pct, report addresses (score 0–20)
blacklistobjectIP and domain blacklist results, including delisting URLs for any listing (score 0–15)
additionalobjectMX, reverse DNS (PTR), SSL and other infrastructure checks (score 0–25)
recommendationsstring[]Prioritized, human-readable steps to improve deliverability

Example response

{
  "success": true,
  "data": {
    "checkId": "3f1c9a52-8b0e-4c1d-9d7a-2e6f0b4a1c55",
    "result": {
      "domain": "example.com",
      "checkedAt": "2026-09-28T10:15:30.000Z",
      "totalScore": 82,
      "grade": "B",
      "cached": false,
      "spf": {
        "found": true,
        "record": "v=spf1 include:_spf.google.com ~all",
        "valid": true,
        "lookupCount": 4,
        "mechanisms": ["include:_spf.google.com", "~all"],
        "errors": [],
        "warnings": [],
        "score": 18
      },
      "dkim": {
        "found": true,
        "selectors": ["default", "google", "k1", "mail"],
        "validSelectors": ["google"],
        "records": { "google": "v=DKIM1; k=rsa; p=MIIBIjANBgkq..." },
        "warnings": [],
        "score": 20
      },
      "dmarc": {
        "found": true,
        "record": "v=DMARC1; p=none; rua=mailto:dmarc@example.com",
        "valid": true,
        "policy": "none",
        "subdomain_policy": null,
        "percentage": 100,
        "rua": ["mailto:dmarc@example.com"],
        "ruf": [],
        "errors": [],
        "warnings": ["Policy is set to none - emails are not protected"],
        "score": 10
      },
      "blacklist": {
        "checked": true,
        "listed": [],
        "clean": ["zen.spamhaus.org", "b.barracudacentral.org"],
        "errors": [],
        "totalChecked": 2,
        "totalListed": 0,
        "score": 15
      },
      "additional": {
        "hasMX": true,
        "hasPTR": true,
        "domainAge": null,
        "ssl": true,
        "warnings": [],
        "score": 19
      },
      "recommendations": [
        "Upgrade your DMARC policy from p=none to p=quarantine once reports look clean"
      ]
    }
  }
}

Rate limits

  • API calls share the same monthly check quota as checks run from the dashboard.
  • Quotas reset on the 1st day of each month at 00:00 UTC. Current limits are listed on the pricing page.
  • Cached results (see below) still count as a check.
  • When the quota is exhausted the API returns HTTP 429 with a rateLimitInfo object that tells you when it resets:
{
  "success": false,
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit exceeded. You have 0 checks remaining. Resets on 10/1/2026"
  },
  "rateLimitInfo": {
    "tier": "agency",
    "limit": 10000,
    "remaining": 0,
    "resetAt": "2026-10-01T00:00:00.000Z"
  }
}

Errors

Errors always have success: false and an error object with a stable machine-readable code and a human-readable message. Branch on code, not on the message text.

HTTPCodeMeaning
400INVALID_JSONThe request body is not valid JSON
400INVALID_DOMAINThe domain is missing or not a valid hostname
401INVALID_API_KEYThe API key is malformed, unknown or has been revoked
402PAYMENT_REQUIREDThe latest subscription payment failed
402SUBSCRIPTION_EXPIREDThe subscription has expired
402SUBSCRIPTION_CANCELLEDThe subscription was cancelled and the paid period has ended
403API_ACCESS_REQUIRES_AGENCYThe key owner is not on the Agency plan
429RATE_LIMIT_EXCEEDEDThe monthly check quota is used up
429SUBSCRIPTION_PAUSEDThe subscription is paused
429SUBSCRIPTION_INACTIVEThe subscription is not active
500INTERNAL_ERRORUnexpected server error — safe to retry with backoff

Best practices

  • Results are cached for 24 hours. Repeated checks of the same domain within that window return the cached result (cached: true). After changing DNS, allow for DNS propagation before re-checking.
  • Run checks sequentially or with low concurrency. Each check performs dozens of DNS lookups; bursts of parallel requests may be throttled.
  • Set a generous timeout (30–60 seconds) — some blacklist servers respond slowly.
  • Retry only on 5xx with exponential backoff. 4xx errors will not succeed on retry without changes.
  • Monitor on a schedule — a daily or weekly check per domain is enough to catch blacklistings and DNS regressions early.

Questions about the API?

Need a higher limit or a custom integration? We're happy to help.

Contact Us