Email verification, as one JSON request.
Send an address. Get back whether it is deliverable, whether the domain has working mail, whether it is a disposable or role address, and a confidence score. One request, one result object.
Every endpoint lives under https://api.emailsherlock.com. Requests are JSON, responses are JSON, authentication is an API key in a header. The example on the right is a complete, copy-paste call. New here? The quickstart walks you from key to first call in five minutes.
Base URL
|
https://api.emailsherlock.comstringrequired
All paths are relative to this. The API lives on its own host, separate from the website.
|
curl https://api.emailsherlock.com/v1/credits \ -H "X-API-Key: $ES_KEY" \ -H "Content-Type: application/json" \ -d '[]'
import { Emailsherlock } from '@emailsherlock/node';
const es = new Emailsherlock(process.env.ES_KEY);
const result = await es.credits({ });
from emailsherlock import Emailsherlock es = Emailsherlock(api_key=os.environ["ES_KEY"]) result = es.credits()
import emailsherlock "github.com/Emailsherlock1/go"
es := emailsherlock.New(os.Getenv("ES_KEY"))
result, err := es.Credits(ctx, )
<?php
use Emailsherlock\Client;
$es = new Client(getenv('ES_KEY'));
$result = $es->credits([]);
{
"plan": "Free",
"sandbox": false
}
Authentication
Authenticate with your API key in the X-API-Key header.
Create a key from your API keys settings and send it on every request. Every key can call both verify endpoints out of the box. Keep the key server-side: a leaked key spends your credits.
|
X-API-Keyheaderrequired
Your secret API key. A missing or invalid key returns 401 unauthorized.
|
curl https://api.emailsherlock.com/v1/credits \ -H "X-API-Key: $ES_KEY" \ -H "Content-Type: application/json" \ -d '[]'
import { Emailsherlock } from '@emailsherlock/node';
const es = new Emailsherlock(process.env.ES_KEY);
const result = await es.credits({ });
from emailsherlock import Emailsherlock es = Emailsherlock(api_key=os.environ["ES_KEY"]) result = es.credits()
import emailsherlock "github.com/Emailsherlock1/go"
es := emailsherlock.New(os.Getenv("ES_KEY"))
result, err := es.Credits(ctx, )
<?php
use Emailsherlock\Client;
$es = new Client(getenv('ES_KEY'));
$result = $es->credits([]);
{
"error": {
"code": "unauthorized",
"message": "Invalid API key."
}
}
Sandbox testing
A key that starts with es_test_ runs in sandbox mode: deterministic, fixtured results on the same endpoints and the same response shape, with no credits spent.
Create a sandbox key from your API keys settings (live keys start with es_live_). Every sandbox response carries the X-Sandbox: true header. The local part of the address you send drives the result, so you can exercise each branch of your integration without real traffic:
|
valid@any domain
result: valid, deliverable, score around 0.95.
|
|
invalid@
result: invalid, not deliverable.
|
|
catch-all@or catchall@
result: catch_all: the domain accepts every address, so a single mailbox can't be confirmed.
|
|
disposable@
result: disposable: a throwaway-inbox provider.
|
|
role@
result: role: a shared mailbox like info@ or support@.
|
|
unknown@
result: unknown: a transient outcome (greylisting). Retry after a short wait.
|
|
pending@
result: unknown with reason: verification_pending: the deferred-SMTP outcome the batch endpoint returns. Verify the address again later for the cached verdict.
|
|
ratelimit@
HTTP 429 rate_limit_exceeded, so you can test your backoff path.
|
|
servererror@
HTTP 503 verify_unavailable, so you can test your retry path.
|
|
async@or slowjob@
On the verification jobs endpoint only: include this in the batch and the sandbox runs the real async lifecycle. The submit returns processing and the job completes after a few seconds, so you can build and test your poll loop. Without it, sandbox jobs complete immediately.
|
|
anything else
Maps to a stable result derived from the address, so the same input always returns the same verdict while a varied list still exercises every branch.
|
curl https://api.emailsherlock.com/v1/verify/single \
-H "X-API-Key: es_test_…" \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]"}'
X-Sandbox: true
X-Credits-Remaining: 8420
{
"email": "[email protected]",
"result": "valid",
"deliverable": true,
"score": 0.95
}
The verify result
Both endpoints return the same result object, one per address. It carries the verdict and the signals behind it.
|
emailstring
The address you sent.
|
|
resultstring
The verdict for this address. One of valid invalid catch_all disposable role unknown.
|
|
deliverableboolean · nullable
Proven deliverability: true only after the mailbox accepted RCPT TO, false only when the address provably fails (bad syntax, no MX, hard reject). Null means unproven, not bad: disposable, role and catch_all short-circuit before the SMTP probe.
|
|
reasonstring · nullable
Why the pipeline decided. greylisted, smtp_timeout, smtp_unreachable and verification_pending are transient: retry the address later. Null on results cached before reasons existed. One of bad_syntax no_mx mailbox_accepts mailbox_not_found disposable_provider role_address catch_all_domain accept_all_provider greylisted smtp_timeout smtp_unreachable verification_pending .
|
|
mxboolean
The domain has reachable MX records.
|
|
mx_recordstring · nullable
The primary MX host, when one was resolved during this check.
|
|
disposableboolean
Throwaway / temporary-mail provider.
|
|
roleboolean
Role address such as info@ or sales@.
|
|
catch_allboolean
Host accepts mail for any local part.
|
|
free_emailboolean · nullable
The domain is a freemail provider (gmail.com, web.de, ...). Null when the domain is not classified yet.
|
|
relayboolean
The address sits on a relay / forwarding / alias-masking service (SimpleLogin, Apple Hide My Email, Cloudflare Email Routing, ...). The address is deliverable and reaches a real person, it is just masked, so this never changes the verdict or decision. Use it to apply your own policy on masked senders.
|
|
relay_providerstring · nullable
Name of the relay provider when relay is true, null otherwise.
|
|
parkedboolean
The address is on a parked or for-sale domain. Informational only: it never changes the verdict, but a parked domain rarely runs a real mailbox.
|
|
suggested_correctionstring · nullable
A corrected address when the domain looks like a typo of a high-volume domain (gmial.com -> [email protected]). Null when nothing looks mistyped. Advisory only: it never changes the verdict, so show it as a "did you mean" hint and let the sender decide.
|
|
smtp_diagnosisstring · nullable
Enhanced scan only: the granular mailbox state read from the SMTP reply (mailbox_full, mailbox_disabled, mailbox_not_found, greylist_deferred, policy_block, accepted). Null on Quick / Standard scans, which do not run the deep probe. Explains the verdict, never overrides it. One of accepted mailbox_full mailbox_disabled mailbox_not_found greylist_deferred policy_block .
|
|
scorenumber · nullable
0-1 confidence, higher is safer to send to.
|
|
freshnessstring
How recent the underlying data is. One of fresh cached_recent cached_stale_refreshed.
|
|
checked_atstring · nullable
When the underlying verification ran (ISO 8601). On cached results this is the original check, not the request time.
|
|
domainobject · nullable
Domain-level intelligence. Null when the domain has not been crawled yet.
|
|
decisionobject
|
The domain object
Domain-level intelligence behind the address: host classification, a 0-100 reputation score, the mail-auth DNS records (SPF, DKIM, DMARC, MTA-STS, TLS-RPT, BIMI, DANE), blacklist count, DNSSEC and CAA. null when we have not crawled the domain yet; it fills in once our pipeline has seen it.
|
namestring
The domain behind the address.
|
|
typesarray · nullable
Host classification. A domain can carry more than one type.
|
|
scorenumber · nullable
0-100 domain reputation score, higher is better. Null when the domain has not been scored yet.
|
|
spfboolean · nullable
An SPF record exists.
|
|
dkimboolean · nullable
A DKIM record was found.
|
|
dmarcboolean · nullable
A DMARC record exists.
|
|
dmarc_policystring · nullable
The published DMARC policy. One of none quarantine reject .
|
|
mta_stsboolean · nullable
An MTA-STS policy is published.
|
|
tls_rptboolean · nullable
A TLS-RPT record exists.
|
|
bimiboolean · nullable
A BIMI record exists.
|
|
daneboolean · nullable
DANE/TLSA records exist for the MX hosts.
|
|
blacklistsinteger · nullable
Number of public DNS blacklists currently listing this domain's mail infrastructure.
|
|
dnssecstring · nullable
DNSSEC chain status. One of secure insecure bogus .
|
|
caaboolean · nullable
A CAA record restricts which CAs may issue certificates.
|
{
"plan": "Free",
"sandbox": false
}
Read the credit balance and rate-limit status. Free: consumes no credits.
Body parameters
curl https://api.emailsherlock.com/v1/credits \ -H "X-API-Key: $ES_KEY" \ -H "Content-Type: application/json" \ -d '[]'
import { Emailsherlock } from '@emailsherlock/node';
const es = new Emailsherlock(process.env.ES_KEY);
const result = await es.credits({ });
from emailsherlock import Emailsherlock es = Emailsherlock(api_key=os.environ["ES_KEY"]) result = es.credits()
import emailsherlock "github.com/Emailsherlock1/go"
es := emailsherlock.New(os.Getenv("ES_KEY"))
result, err := es.Credits(ctx, )
<?php
use Emailsherlock\Client;
$es = new Client(getenv('ES_KEY'));
$result = $es->credits([]);
{
"plan": "Free",
"sandbox": false
}
Record a batch of Email-Guard decision events (free).
Body parameters
|
eventsarrayrequired
Guard decision events to record. Non-empty, capped per the batch limit. Each item: {domain?, verdict, action, reasons[], degraded, source, integration?, lib_version?, spec_version?, property_key?}. The full email address is never sent, only the domain. property_key is the optional public site-key of a property (EM-998); when it matches one of your properties the event is attributed to it, otherwise the event still records at the account level.
|
curl https://api.emailsherlock.com/v1/guard/events \
-H "X-API-Key: $ES_KEY" \
-H "Content-Type: application/json" \
-d '{"events":[{"domain":"mailinator.com","verdict":"disposable","action":"deny","reasons":["disposable_provider"],"degraded":false,"source":"local","integration":"symfony-bundle","lib_version":"0.1.2","spec_version":"1.0.0","property_key":"egp_4f3c2a1b8d9e0f1a2b3c4d5e6f70819a"}]}'
import { Emailsherlock } from '@emailsherlock/node';
const es = new Emailsherlock(process.env.ES_KEY);
const result = await es.guard.events({ events: [{ domain: 'mailinator.com', verdict: 'disposable', action: 'deny', reasons: ['disposable_provider'], degraded: false, source: 'local', integration: 'symfony-bundle', lib_version: '0.1.2', spec_version: '1.0.0', property_key: 'egp_4f3c2a1b8d9e0f1a2b3c4d5e6f70819a' }] });
from emailsherlock import Emailsherlock
es = Emailsherlock(api_key=os.environ["ES_KEY"])
result = es.guard.events(events=[{"domain": "mailinator.com", "verdict": "disposable", "action": "deny", "reasons": ["disposable_provider"], "degraded": False, "source": "local", "integration": "symfony-bundle", "lib_version": "0.1.2", "spec_version": "1.0.0", "property_key": "egp_4f3c2a1b8d9e0f1a2b3c4d5e6f70819a"}])
import emailsherlock "github.com/Emailsherlock1/go"
es := emailsherlock.New(os.Getenv("ES_KEY"))
result, err := es.Guard.Events(ctx, []any{map[string]any{"domain": "mailinator.com", "verdict": "disposable", "action": "deny", "reasons": []string{"disposable_provider"}, "degraded": false, "source": "local", "integration": "symfony-bundle", "lib_version": "0.1.2", "spec_version": "1.0.0", "property_key": "egp_4f3c2a1b8d9e0f1a2b3c4d5e6f70819a"}})
<?php
use Emailsherlock\Client;
$es = new Client(getenv('ES_KEY'));
$result = $es->guard->events(['events' => [['domain' => 'mailinator.com', 'verdict' => 'disposable', 'action' => 'deny', 'reasons' => ['disposable_provider'], 'degraded' => false, 'source' => 'local', 'integration' => 'symfony-bundle', 'lib_version' => '0.1.2', 'spec_version' => '1.0.0', 'property_key' => 'egp_4f3c2a1b8d9e0f1a2b3c4d5e6f70819a']]]);
[]
Verify one email address.
The core call. One address in, one result out, typically in under a second.
POST a JSON body with the address. We lowercase and trim it, check syntax, resolve MX, and probe deliverability. Free on any paid plan while you have monthly allowance left, otherwise 0 credits per call; a verify that the engine can't complete is refunded automatically.
Body parameters
|
emailstringrequired
The address to verify.
|
|
modestring · nullable
Scan depth. quick: syntax + DNS/MX only, no SMTP probe (0 credits). standard: + SMTP RCPT probe, default (1 credit). enhanced: + catch-all detection via random RCPT probe (3 credits; known catch-all hosts are billed at the standard rate). Omitting mode is identical to standard. One of quick standard enhanced.
|
|
asyncboolean · nullable
Return immediately without waiting for the live SMTP probe. The address is checked at DNS/MX depth; when a mailbox probe is still needed the response carries reason "verification_pending" while the SMTP check runs in the background. To read the verdict, call this endpoint again for the same address after roughly 1 to 3 seconds: the follow-up is a cache hit with the resolved result. Keep polling until reason is no longer "verification_pending". Each call is billed (standard verify is free on a paid plan, so polling costs nothing there). Billing is otherwise unchanged: async only changes when the result is delivered, not what you pay, so enhanced mode still runs its catch-all probe in the background and is billed at the enhanced rate. For large async batches, prefer POST /v1/verify/jobs, which returns a job id you poll with GET /v1/verify/jobs/{id} and whose status polls are free. Ignored for quick mode (already DNS-only). Default false.
|
The response is a single result object. The X-Credits-Remaining header reports your balance after the call.
Branch on deliverable when you need a yes/no: it is true only after the mailbox accepted our probe and false only when the address provably fails. null means unproven, not bad: disposable, role and catch-all addresses skip the probe. reason tells you why the verdict fell. The transient reasons (greylisted, smtp_timeout, smtp_unreachable) are worth a retry after 15 minutes or more; the mail server asked us to come back later.
Each result also includes a decision object with a recommendation (allow, deny, or review) and a reasons array explaining the signal that drove it. This is advice: policy authority stays with your application. deny covers addresses that are provably invalid or from disposable providers. review covers role addresses, catch-all domains, and transient unknowns where a retry or manual check makes sense. allow means the mailbox accepted the probe.
curl https://api.emailsherlock.com/v1/verify/single \
-H "X-API-Key: $ES_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]"}'
import { Emailsherlock } from '@emailsherlock/node';
const es = new Emailsherlock(process.env.ES_KEY);
const result = await es.verify.single({ email: '[email protected]' });
from emailsherlock import Emailsherlock es = Emailsherlock(api_key=os.environ["ES_KEY"]) result = es.verify.single(email="[email protected]")
import emailsherlock "github.com/Emailsherlock1/go"
es := emailsherlock.New(os.Getenv("ES_KEY"))
result, err := es.Verify.Single(ctx, "[email protected]")
<?php
use Emailsherlock\Client;
$es = new Client(getenv('ES_KEY'));
$result = $es->verify->single(['email' => '[email protected]']);
{
"email": "[email protected]",
"result": "valid",
"deliverable": true,
"reason": "mailbox_accepts",
"mx": true,
"mx_record": "mx1.acme-mail.com",
"disposable": false,
"role": false,
"catch_all": false,
"free_email": false,
"relay": false,
"relay_provider": "SimpleLogin",
"parked": false,
"suggested_correction": "[email protected]",
"smtp_diagnosis": "mailbox_full",
"score": 0.95,
"freshness": "fresh",
"checked_at": "2026-06-09T14:32:00+00:00",
"domain": {
"name": "acme.com",
"types": [
"company"
],
"score": 87,
"spf": true,
"dkim": true,
"dmarc": true,
"dmarc_policy": "quarantine",
"mta_sts": false,
"tls_rpt": false,
"bimi": false,
"dane": false,
"blacklists": 0,
"dnssec": "secure",
"caa": true
},
"decision": {
"recommendation": "allow",
"reasons": [
"mailbox_accepts"
]
}
}
Verify up to a batch of email addresses in one call.
Up to 100 addresses in one call. Each is verified independently. Free on the any paid plan while the whole batch fits your monthly allowance, otherwise billed per address.
Send an emails array. You get back a results array in the same order. A bad address in the list does not fail the whole call: that entry carries an error instead of a result, and the rest still run. If your balance runs out mid-batch we stop and the remaining entries report insufficient_credits, so you can see exactly how far we got.
Batches stay fast by skipping the live SMTP probe in the request cycle. An address we cannot settle from cache, DNS and our lists comes back as unknown with reason set to verification_pending, and the full check runs in the background. Request the same address again a few minutes later and you get the settled verdict from cache. Single calls always run the full probe inline.
Body parameters
|
emailsarrayrequired
The addresses to verify. Non-empty, capped per the batch limit.
|
|
modestring · nullable
Scan depth applied to every address in this batch. quick: syntax + DNS/MX only (0 credits each). standard: + SMTP RCPT probe, default (1 credit each). enhanced: + catch-all detection (3 credits each; known catch-all hosts billed at standard rate). Omitting mode is identical to standard. One of quick standard enhanced.
|
Per-entry errors
|
invalid_emailentry error
The entry was not a valid address. It was skipped and not billed.
|
|
insufficient_creditsentry error
Your balance ran out at this entry. This and the rest were not verified.
|
|
verify_unavailableentry error
The engine could not complete this address. It was refunded; retry it later.
|
curl https://api.emailsherlock.com/v1/verify/batch \
-H "X-API-Key: $ES_KEY" \
-H "Content-Type: application/json" \
-d '{"emails":["[email protected]","[email protected]"]}'
import { Emailsherlock } from '@emailsherlock/node';
const es = new Emailsherlock(process.env.ES_KEY);
const result = await es.verify.batch({ emails: ['[email protected]', '[email protected]'] });
from emailsherlock import Emailsherlock es = Emailsherlock(api_key=os.environ["ES_KEY"]) result = es.verify.batch(emails=["[email protected]", "[email protected]"])
import emailsherlock "github.com/Emailsherlock1/go"
es := emailsherlock.New(os.Getenv("ES_KEY"))
result, err := es.Verify.Batch(ctx, []string{"[email protected]", "[email protected]"})
<?php
use Emailsherlock\Client;
$es = new Client(getenv('ES_KEY'));
$result = $es->verify->batch(['emails' => ['[email protected]', '[email protected]']]);
{
"results": [
{
"email": "[email protected]",
"result": "valid",
"deliverable": true,
"reason": "mailbox_accepts",
"mx": true,
"mx_record": "mx1.acme-mail.com",
"disposable": false,
"role": false,
"catch_all": false,
"free_email": false,
"relay": false,
"relay_provider": "SimpleLogin",
"parked": false,
"suggested_correction": "[email protected]",
"smtp_diagnosis": "mailbox_full",
"score": 0.95,
"freshness": "fresh",
"checked_at": "2026-06-09T14:32:00+00:00",
"domain": {
"name": "acme.com",
"types": [
"company"
],
"score": 87,
"spf": true,
"dkim": true,
"dmarc": true,
"dmarc_policy": "quarantine",
"mta_sts": false,
"tls_rpt": false,
"bimi": false,
"dane": false,
"blacklists": 0,
"dnssec": "secure",
"caa": true
},
"decision": {
"recommendation": "allow",
"reasons": [
"mailbox_accepts"
]
}
},
{
"email": "nope@",
"error": "invalid_email"
}
]
}
Submit a list of addresses for asynchronous verification.
Every address runs the full pipeline including the SMTP probe, so the results carry definitive inbox verdicts. Poll GET /v1/verify/jobs/{id} until the status is completed. Credits are charged per address at submit; addresses that cannot be processed are refunded automatically.
Body parameters
|
emailsarrayrequired
The addresses to verify. Non-empty, capped at the job limit (10000).
|
curl https://api.emailsherlock.com/v1/verify/jobs \
-H "X-API-Key: $ES_KEY" \
-H "Content-Type: application/json" \
-d '{"emails":["[email protected]","[email protected]"]}'
import { Emailsherlock } from '@emailsherlock/node';
const es = new Emailsherlock(process.env.ES_KEY);
const result = await es.verify.jobs({ emails: ['[email protected]', '[email protected]'] });
from emailsherlock import Emailsherlock es = Emailsherlock(api_key=os.environ["ES_KEY"]) result = es.verify.jobs(emails=["[email protected]", "[email protected]"])
import emailsherlock "github.com/Emailsherlock1/go"
es := emailsherlock.New(os.Getenv("ES_KEY"))
result, err := es.Verify.Jobs(ctx, []string{"[email protected]", "[email protected]"})
<?php
use Emailsherlock\Client;
$es = new Client(getenv('ES_KEY'));
$result = $es->verify->jobs(['emails' => ['[email protected]', '[email protected]']]);
[]
Read the status and results of a verification job.
results is present once the status is completed: one entry per submitted address, in submit order. Results are retained for 7 days after submit.
Body parameters
curl https://api.emailsherlock.com/v1/verify/jobs/{id} \
-H "X-API-Key: $ES_KEY" \
-H "Content-Type: application/json" \
-d '[]'
import { Emailsherlock } from '@emailsherlock/node';
const es = new Emailsherlock(process.env.ES_KEY);
const result = await es.verify.jobs.{id}({ });
from emailsherlock import Emailsherlock
es = Emailsherlock(api_key=os.environ["ES_KEY"])
result = es.verify.jobs.{id}()
import emailsherlock "github.com/Emailsherlock1/go"
es := emailsherlock.New(os.Getenv("ES_KEY"))
result, err := es.Verify.Jobs.{id}(ctx, )
<?php
use Emailsherlock\Client;
$es = new Client(getenv('ES_KEY'));
$result = $es->verify->jobs->{id}([]);
{
"id": "0f8a3c1e-6b4d-4e2a-9c7f-2d1b3a4c5d6e",
"status": "processing",
"total": 250,
"created_at": "2026-06-10T14:32:00+00:00",
"expires_at": "2026-06-17T14:32:00+00:00",
"results": []
}
Credits & rate limits
On any paid plan, verify is free up to a monthly allowance. Past that, or without a subscription, verifies are paid in credits. A separate per-key rate limit caps how fast you go either way.
On any paid plan you get 10,000 free verifies a month. Those calls spend no credits, and X-Verify-Free-Remaining on the response tells you how many are left. Past the allowance, or without a subscription, each verified address costs 0 credits from your account balance, the same wallet the reverse-lookup tool and bulk jobs draw from. The X-Credits-Remaining header on every response tracks what is left. Run out and you get a 402 with X-Credits-Required telling you the shortfall.
The per-minute limit scales with your plan, so a higher tier gets a higher ceiling. Every response carries X-RateLimit-Limit (your current ceiling), X-RateLimit-Remaining and X-RateLimit-Reset (a unix timestamp). Exceed the window and you get a 429 with a Retry-After header. Read it rather than a fixed sleep. Short bursts are fine; only a sustained rate above your tier is throttled.
HTTP/2 200
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 119
X-RateLimit-Reset: 1751328600
X-Credits-Remaining: 8420
Errors
Conventional HTTP status codes, plus a typed error body you can switch on.
Top-level failures return { "error": { "code", "message" } }. The code is stable and safe to branch on; message is for humans. 2xx is success, 4xx means the request was wrong, and 5xx means we faltered, so retry with backoff.
Status codes
{
"error": {
"code": "insufficient_credits",
"message": "Not enough credits for this request."
}
}