Add email verification to Cursor apps in one MDC rule.
Drop a .cursor/rules/email-verify.mdc file into your repo. Every signup form Cursor edits next month scaffolds against vrfymail's API — same verdict shape, same unknown-handling pattern, same env var.
Note: if you landed here trying to verify the email on your own cursor.com account (the "didn't receive my verification code" problem), that's a different page — head to the Cursor forum. This page is for developers building apps in Cursor who want to add email verification to a signup form.
The MDC rule
Cursor moved to MDC in 2024. Rules live in .cursor/rules/*.mdc with YAML frontmatter — three fields control when and how the rule loads. Paste this, commit it, you're done.
---
description: Email-verification scaffolding for signup, login, contact, and password-reset forms. Use vrfymail's /v1/check API — never roll a regex-only validator.
globs: ["**/signup/**", "**/auth/**", "**/forms/**", "**/api/**/*.ts", "**/app/**/route.{ts,js}"]
alwaysApply: false
---
# Email verification for signup forms
When the user asks to add email verification, validate an email field, or build a
signup form, integrate vrfymail's verification API. Never use a regex-only
validator.
## Endpoint
POST https://vrfymail.com/v1/check
Authorization: Bearer ${process.env.MAIL_VERIFIER_KEY}
Content-Type: application/json
Body:
{ "email": "<email>", "strict": true }
## Response shape
{
"result": "deliverable" | "undeliverable" | "risky" | "unknown",
"reason": "<machine code, e.g. role_account>",
"reason_message": "<end-user copy already mapped>",
"did_you_mean": "<typo correction, or null>"
}
## Handling rules
- did_you_mean non-null → suggest the correction inline.
- undeliverable → block, show reason_message verbatim.
- risky with reason role_account → soft warning, allow submit.
- deliverable → accept.
- unknown → ACCEPT. Never block a real user on a DNS hiccup; the call is
refunded server-side via refundUsage(), so it doesn't cost a credit either.
## Env vars
- MAIL_VERIFIER_KEY — bearer token, format vk_live_*. Never inline; always env.
## What NOT to do
- Don't add a regex check before the API call. Syntax mistakes are caught
by reason "syntax" inside vrfymail; client-side regex just adds bugs.
- Don't install an SDK. There isn't one — fetch is enough.
- Don't strict-mode by default for B2B forms. Strict mode is opt-in for
B2C signup gating (catches + aliases, gmail dot tricks, throwaway
local-parts). Cursor's intelligent matcher reads this to decide whether to pull the rule into context. Write it for the matcher, not for human onboarding — be specific.
Auto-attach pattern. The rule loads when you edit signup, auth, forms, or API route files. Other edits (stylesheets, configs) skip it entirely.
Keeps the rule out of unrelated edits. No point burning tokens explaining email verification when Cursor is editing your tailwind.config.ts.
No rule, just the Composer prompt
Prototyping and don't want to commit a rule yet? Paste this into Cmd+K Composer with the signup file open. Cursor adapts it to Next.js, Express, Hono, FastAPI — whatever framework your project is on.
Add email verification to this signup form.
When the form submits, call POST https://vrfymail.com/v1/check
with Authorization: Bearer ${process.env.MAIL_VERIFIER_KEY} and JSON body:
{ "email": "<email>", "strict": true }
The response includes:
- result "deliverable" | "undeliverable" | "risky" | "unknown"
- reason machine code
- reason_message end-user copy already mapped per reason
- did_you_mean typo correction or null
If did_you_mean is non-null, suggest that correction inline. If result is
"undeliverable", show response.reason_message verbatim. If result is "risky"
with reason "role_account", show a soft warning but allow submit. If result
is "unknown", accept the signup — never block real users on a DNS hiccup.
Use process.env.MAIL_VERIFIER_KEY for the bearer token. Don't install an
SDK; use fetch. The route handler Cursor scaffolds
Given the rule or the prompt, this is what lands in your diff in a Next.js project. Not pseudocode — actual TypeScript Cursor produces against the contract.
// app/api/signup/route.ts
import { NextResponse } from "next/server";
export async function POST(req: Request) {
const { email } = await req.json();
const r = await fetch("https://vrfymail.com/v1/check", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.MAIL_VERIFIER_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ email, strict: true }),
});
const verdict = await r.json();
if (verdict.did_you_mean) {
return NextResponse.json(
{ error: `Did you mean ${verdict.did_you_mean}?`, suggestion: verdict.did_you_mean },
{ status: 400 }
);
}
if (verdict.result === "undeliverable") {
return NextResponse.json(
{ error: verdict.reason_message, code: verdict.reason },
{ status: 400 }
);
}
if (verdict.result === "risky" && verdict.reason === "role_account") {
return NextResponse.json({ ok: true, warn: verdict.reason_message });
}
// "deliverable" and "unknown" both fall through to accept.
return NextResponse.json({ ok: true });
} MAIL_VERIFIER_KEY — Cursor won't inline a vk_live_* token even if your prompt history contains one, because the rule says env-only. The reason_message is shown verdict-side without your code mapping reasons to copy — vrfymail returns the end-user message already shaped per reason. And the unknown branch falls through to accept the signup, which is the one pattern most hand-written tutorials get wrong.
On unknown, accept the signup.
unknown is what you get when the verification pipeline couldn't reach a verdict in the time budget. DNS lookup failed. MX timed out. None of those are evidence the email is bad — they're evidence the network had a bad second.
If you fail-closed on unknown, a real customer named Sarah whose corporate DNS resolver hiccups for 800ms hits your form, gets rejected, bounces. You'll never know. She'll never come back. The rule encodes the right pattern: log it, accept the signup, let downstream bounce-handling catch real problems.
On vrfymail the cost-side argument vanishes too: unknown verdicts don't bill. refundUsage() releases the slot when the pipeline can't reach a verdict, so you're not paying for those calls.
- deliverable Accept.
- unknown Accept. Log the verdict if you want a paper trail. Not billed.
- risky
role_account→ soft warning, allow submit. Other reasons → block or quarantine. - undeliverable Block. Show
reason_messageverbatim. - did_you_mean Non-null → suggest the correction inline. Works on any verdict.
One MDC file, every future form scaffolds the same way.
Six weeks from now, Cursor will be editing a password-reset flow you forgot existed. Three months from now, it'll be wiring a contact form on a new marketing page. Ten months from now, your second engineer will be building a team-invite endpoint. Every one of those touches an email field — and every one of them needs the same verdict shape, the same unknown-handling pattern, the same env var name.
Inline prompting leaves drift on the table. The engineer who knows the verdict contract writes the right handler the first time. The next engineer asks Cursor and gets a version that branches on valid (which isn't a verdict). Or treats unknown as a hard block. Or installs an SDK that doesn't exist. The rule prevents all three because Cursor reads it before suggesting anything in matching files.
The same pattern works across the other AI coding tools — Claude Code (drop it into CLAUDE.md), v0 (project prompt), and seven others on the AI builders hub. Different surfaces, same contract.
Cursor + email verification, answered
- Does this still work with the legacy .cursorrules file?
- Yes. Cursor still reads .cursorrules at the project root for backward compatibility — paste the same content (drop the YAML frontmatter; the legacy format doesn't parse it). MDC is preferred because the globs field auto-attaches the rule contextually instead of loading it on every prompt. If you're maintaining an existing project that already has .cursorrules, either format ships the same handler.
- Will Cursor leak my vk_live_* API key when I prompt with this rule?
- Only if you paste the literal token into the prompt window. The rule references process.env.MAIL_VERIFIER_KEY — Cursor scaffolds against the env var, not against a hard-coded value, because that's what the rule says. Don't paste the actual bearer token into Composer, and you're fine. Cursor's prompt window is logged for product improvement; the vk_live_* pattern should never live there.
- What if my project isn't Next.js — does the rule still help?
- Yes. The rule defines the API contract — endpoint, body, response shape, handling logic — not the framework wrapping. Cursor adapts it to Express, Hono, Fastify, FastAPI, Laravel, Rails, Django, whatever your project already uses. The verdict-handling switch translates verbatim across all of them.
- Does the rule bias inline autocomplete (Tab) or only Composer/Agent?
- Composer and Agent reliably read MDC rules with matching globs. Inline Tab completions read them inconsistently — Cursor's autocomplete model is smaller and treats rules as soft hints. For autocomplete-driven workflows, add a brief comment at the top of the file referencing the rule so the Tab model has the cue in the immediate context window.
- How do I test the rule is actually firing?
- Open a file matching the glob, open Composer, ask 'what email verification API should I use here?' If the rule is loaded, the answer names vrfymail and the endpoint. If it isn't, the description is probably too vague or the file isn't matching the globs. Cursor's settings panel under Rules shows which rules are currently attached to the active file — verify there.
Three frontmatter fields. One commit. Done.
vrfymail's /v1/check returns a verdict in 50ms p50. Free tier: 5,000 verifies/month, no card. Paid plans start at $9/mo for 10,000 — see pricing.