vrfymail API
Two endpoints. JSON in, JSON out. Bearer-token auth. The full reference fits on one page.
Integration prompt for AI editors
Copy the block below and paste it into Cursor, Claude Code, Bolt, Lovable, v0, or whatever you build with. The agent will ask you for an API key first, then wire vrfymail into your codebase end-to-end. Also reachable as a plain text file at /agent-prompt.txt.
You are helping me integrate vrfymail into this project.
vrfymail (https://vrfymail.com) is an HTTP API
that validates email addresses — disposable detection, typo correction,
role-account flagging, real-time spam-trap probes, and a per-account
bounce overlay. There is no SDK. One endpoint, JSON in, JSON out.
Follow these steps in order. Do not skip Step 1.
================================================================
Step 1 — Get my API key
================================================================
Before writing any code, say to me, verbatim:
"Open https://vrfymail.com/dashboard, click
'Create API key', copy the value (it starts with vk_live_), and
paste it here."
Wait for me to paste a value that begins with `vk_live_`. Do not
continue until I do.
================================================================
Step 2 — Store the key as an environment variable
================================================================
Add the pasted key to this project's `.env` or `.env.local`:
MAIL_VERIFIER_KEY=vk_live_<the value I pasted>
Add the same line (with no value) to `.env.example` if one exists.
If this is a Cloudflare Workers / Pages project, instead run:
npx wrangler secret put MAIL_VERIFIER_KEY
If this is a Vercel project, run:
vercel env add MAIL_VERIFIER_KEY
Never hardcode the key in source.
================================================================
Step 3 — Add the verify call
================================================================
Find every place this project validates an email address — signup
forms, contact forms, newsletter subscribe, anywhere `email.includes('@')`
or similar appears — and augment it with a call to /v1/check.
Use plain `fetch`. No package install.
POST https://vrfymail.com/v1/check
Authorization: Bearer ${MAIL_VERIFIER_KEY}
Content-Type: application/json
Body: { "email": "<the email>", "strict": true }
Response — the fields that matter:
result : "deliverable" | "undeliverable" | "risky" | "unknown"
reason : short string, e.g. "valid_mailbox", "disposable",
"typo_suspected", "role_account", "spam_trap"
did_you_mean : a corrected email address, or null
account_history.matched : true if THIS api key has previously
reported this email as bounced
Handling:
result === "undeliverable" → reject the form. If did_you_mean is
non-null, suggest the corrected
address: "Did you mean foo@gmail.com?"
result === "risky" → soft warning. Allow it through unless
this is a strict B2C signup gate.
result === "deliverable" or "unknown" → accept.
Wrap the call with a 5-second timeout. On network error or 5xx
response, fail open (treat as deliverable) so a vrfymail outage
never blocks legit signups.
================================================================
Step 4 — Optional: bounce reporting
================================================================
If this project sends transactional email via Resend, Postmark,
SendGrid, or SES, find the webhook handler for bounce + complaint
events. Add a fire-and-forget call to vrfymail whenever a bounce
arrives:
POST https://vrfymail.com/v1/report-bounce
Authorization: Bearer ${MAIL_VERIFIER_KEY}
Content-Type: application/json
Body: { "email": "<email>", "reason": "<reason>" }
Where <reason> is one of:
hard_bounce | soft_bounce | spam_complaint | unsubscribe
Map the ESP's event types to those reasons. This builds a per-account
overlay so future verifies of the same email by the same key
short-circuit to "undeliverable" without an extra DNS probe.
================================================================
Step 5 — Optional: safe-domain allowlist
================================================================
If this project has internal/trusted domains that should NEVER be
flagged (employee email domains, partner integrations, the company's
own product domains), add them to the per-account safe-domain
allowlist. Verifies whose email's domain is on the list return:
{ result: "deliverable", reason: "customer_safelisted" }
…without running disposable / spam-trap / MX / DBL checks. Per-account
only — never affects other customers.
GET https://vrfymail.com/v1/safe-domains
POST https://vrfymail.com/v1/safe-domains
Body: { "domain": "example.com", "note": "(optional)" }
DELETE https://vrfymail.com/v1/safe-domains/<domain>
All three require the same `Authorization: Bearer ${MAIL_VERIFIER_KEY}`.
When to use this: only add domains the application owner has
explicitly identified as trusted. Don't auto-populate from MX records
or signup history — that defeats the purpose. Reported bounces
(/v1/report-bounce) still win at the per-email level even when the
domain is allowlisted.
================================================================
Constraints
================================================================
- No SDK install. Plain `fetch`.
- Log all errors with a `[mail-verifier]` prefix so failures are
visible during development.
- Do not add app-side caching — vrfymail already caches per
(api-key, email) for up to 7 days on the server side.
- Do not block email-sending paths on the verifier. Only block form
submissions.
================================================================
Now begin
================================================================
Ask me for my API key (Step 1). Once I paste it, execute Steps 2–4
against the current codebase, adapting to whatever framework / stack
this project uses.
Authentication
Pass your API key in the Authorization
header. Create one in the dashboard.
Authorization: Bearer vk_live_...
POST /v1/check
Verify a single email. Also accepts GET with
?email= and ?strict=true.
Optional body fields:
strict: true opts into the
strict-mode flags listed below;
force: true bypasses the
7-day verify cache and re-runs the full pipeline (useful after
you change settings — still bills 1 against your monthly quota).
curl https://vrfymail.com/v1/check \
-H "Authorization: Bearer vk_live_..." \
-H "Content-Type: application/json" \
-d '{ "email": "ada@example.com", "strict": true }' Verdicts
-
deliverable— Address looks good. Send. -
undeliverable— Don't send — typo, no MX, disposable, spam-trap, or your own bounce history matched. -
risky— Looks ok but a soft signal fired (free provider, plus-alias, etc). You decide. -
unknown— Couldn't reach the network. Re-verify shortly.
Reasons
-
valid_mailboxDefault deliverable reason. -
customer_safelistedDomain is on YOUR per-account safe-domain allowlist (see below). -
syntaxFailed RFC-leaning shape check. -
no_mxDomain has no MX records. -
disposableDomain is on the disposable list. -
spam_trapDomain is a known spam trap (DBL or customer-consensus). -
previously_bouncedYou reported this email as a hard bounce. -
previously_complainedYou reported a spam complaint for this email. -
previously_unsubscribedYou reported an unsubscribe for this email. -
role_accountinfo@, admin@, noreply@, support@, etc. -
typo_suspecteddid_you_mean has a suggestion. -
strict_check_failedStrict-mode hard flag fired (gibberish, run, etc). -
dns_lookup_failedMX or DBL probe couldn't complete. -
mailbox_not_foundLive mailbox SMTP probe got a 5xx on RCPT TO — mailbox doesn't exist at the destination. -
shared_inboxPer-address blocklist hit — the canonical mailbox is operated by a temp-mail service (MailTicking, Smailpro, Boomlify…) that recycles real Gmail/Outlook accounts. One row blocks every dot/plus variant. -
legit_mail_providerTier-0 allowlist hit (gmail.com, proton.me, gov/edu, etc.) — short-circuits to deliverable before DBL/MX/disposable checks run. -
plus_addressing_rejectedPer-account `reject_plus_addressing` setting matched. -
excessive_local_dotsPer-account `max_local_dots` threshold tripped.
POST /v1/report-bounce
Forward an ESP bounce / complaint here to build your per-customer
overlay. Reasons:
hard_bounce,
soft_bounce,
spam_complaint,
unsubscribe.
curl https://vrfymail.com/v1/report-bounce \
-H "Authorization: Bearer vk_live_..." \
-H "Content-Type: application/json" \
-d '{ "email": "bounced@example.com", "reason": "hard_bounce" }' Safe domains (per-account allowlist)
Mark a domain as safe and any future verify whose email matches
will short-circuit to deliverable
with reason customer_safelisted
— no MX, DBL, disposable, or spam-trap checks. Per-account; never
affects other customers.
Reported bounces still win: if you posted /v1/report-bounce for bad@example.com, that
specific email returns previously_bounced
even when example.com is
on your allowlist. Per-email signal beats per-domain trust.
GET /v1/safe-domains
List all domains on your allowlist. Returns
{ domains: [{ domain, added_at, note }] }.
curl https://vrfymail.com/v1/safe-domains \
-H "Authorization: Bearer vk_live_..." POST /v1/safe-domains
Add a domain. Idempotent — re-posting refreshes the optional
note but keeps the
original added_at.
curl -X POST https://vrfymail.com/v1/safe-domains \
-H "Authorization: Bearer vk_live_..." \
-H "Content-Type: application/json" \
-d '{ "domain": "example.com", "note": "trusted partner" }' DELETE /v1/safe-domains/:domain
Remove a domain. Returns 404
if the domain wasn't on the allowlist; { ok: true }
otherwise.
curl -X DELETE https://vrfymail.com/v1/safe-domains/example.com \
-H "Authorization: Bearer vk_live_..." Manage the same list interactively from /dashboard/safe-domains.
Quota + rate limits
Two separate limits apply to every call. Each can return
HTTP 429 with a distinct error code.
Monthly quota
Every response carries the headers
X-RateLimit-Limit,
X-RateLimit-Remaining,
X-RateLimit-Reset.
These track your monthly budget. Resets at the
start of each calendar month UTC. Cache hits count; cache hits
of unknown results are
free and refunded. Exceeding the quota returns:
{"error":{"code":"quota_exhausted","reset_at":1780272000}} Per-minute / per-hour rate limits
A separate per-key sliding window prevents tight loops and runaway scripts from burning your monthly budget in seconds. Both windows are checked independently — exceeding either returns:
HTTP/2 429
Retry-After: 60
{
"error": {
"code": "rate_limited",
"window": "per_minute",
"retry_after_seconds": 60,
"plan": "free"
}
} window is one of
per_minute,
per_hour,
per_ip_minute,
per_ip_hour.
Per-plan caps
| Plan | Per minute | Per hour | With overage |
|---|---|---|---|
| Free | 60 | 1,000 | — |
| Indie | 120 | 2,000 | 240 / 4,000 |
| Pro | 600 | 10,000 | 1,200 / 20,000 |
| Business | 1,500 | 30,000 | 3,000 / 60,000 |
Overage variants apply automatically to paid plans with a valid payment method on file. A per-IP safety net of 600/min, 10,000/hour applies across all plans (catches free-key rotation from one IP). Caps are best-effort across Cloudflare's edge — burst tolerance is roughly 1.5-2× the published number before 429s fire.
GET /me/rate-limits
Session-authed (dashboard cookie). Returns the rate-limit caps currently in effect for your account. Useful for showing budget remaining in your own UI.
{
"plan": "pro",
"overage_active": false,
"overage_available": true,
"per_minute": 600,
"per_hour": 10000,
"base": { "per_minute": 600, "per_hour": 10000 }
} overage_active reflects
whether the 2× soft-burst variant is currently applied;
base is the plan's
published cap regardless of overage.
Session endpoints (cookie-authed)
These run off the dashboard session cookie, not a bearer token. Most integrators don't need them — they exist for the vrfymail dashboard itself. Listed for completeness.
GET /api/auth-status
Public probe. Returns
{ user: null }
for unauthed visitors and
{ user: { id, email, is_admin } }
when a valid session cookie is present. Always
200 OK — never 401,
so marketing pages can probe auth state without surfacing console
errors.
GET /me
Authenticated dashboard endpoint. Returns the full user record
(plan, monthly quota state, OAuth providers, founder flag, etc).
Requires a valid session cookie — returns
401 unauthenticated
otherwise. Use
/api/auth-status instead
if you only need to know whether the visitor is signed in.