Docs · Script tag and REST API
Vetto API documentation
Install the script tag on any signup form, or call the REST API from your server. JSON in, JSON out, CORS enabled, and every failure lets the signup through.
By the Vetto team · Updated
Overview
- Base URL:
https://usevetto.com. All endpoints take and return JSON. CORS is open (*). Machine-readable spec: openapi.json (OpenAPI 3.1). - Two keys per site, from your dashboard: a public
pk_…key for the browser and a secretsk_…key for your server. Never put the secret key in front-end code. - Plans: Free covers 1 site and 1,000 distinct emails a month through the script tag, plus the keyless domain lookup (section 4). Pro ($12/mo, 3 sites, 20,000 per site) adds the server API, signed tokens and the Pro signals below.
1. Script tag (Free and Pro)
Paste it once, before </body>. It finds the email fields on your forms, checks them on blur and on submit, and shows the message under the field.
<script src="https://usevetto.com/v.js" data-site="pk_xxxxxxxxxxxxxxxxxxxx" defer></script>Options: data-forms limits it to forms matching a CSS selector. The site key only works on the domain you registered for that site.
<script src="https://usevetto.com/v.js" data-site="pk_xxxxxxxxxxxxxxxxxxxx"
data-forms="#signup-form" defer></script>On submit, every guarded form gets a hidden vetto_token field. On Pro it carries a signed verdict your server can check (section 3); on Free it is empty.
2. POST /v1/check (server, Pro)
Send Authorization: Bearer sk_… and { "email": "…" }. From the server, commit defaults to true: the signup is recorded in your dashboard and counts toward the quota. Send "commit": false for a read-only check.
curl -X POST https://usevetto.com/v1/check \
-H "Authorization: Bearer $VETTO_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "jane@mailinator.com"}'{
"ok": true,
"verdict": "block",
"message": "Please use a permanent email address. Temporary inboxes are not accepted.",
"suggestion": null,
"reasons": [{ "code": "disposable", "action": "block" }],
"tags": [],
"repeat": 0,
"token": null,
"branding": false
}| Field | Meaning |
|---|---|
| verdict | allow, flag, suggest, warn or block. Reject the signup on block; flag means "allow but mark for review". |
| message | Text you can show the user, or null |
| suggestion | Corrected address for typos (jane@gmail.com), or null |
| reasons | Signals that fired, as { code, action } |
| tags | Informational labels such as relay, freemail, role |
| repeat | Pro: how many other aliases of the same Gmail inbox already signed up |
| token | Signed verdict on committed, non-blocked checks (Pro), else null |
| failOpen | true when the check was skipped (site paused or over quota); verdict is then allow |
// Fail open: a timeout or network error never blocks a real signup.
export async function vettoVerdict(email) {
try {
const res = await fetch('https://usevetto.com/v1/check', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.VETTO_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(2500),
});
const v = await res.json();
return v.ok ? v.verdict : 'allow';
} catch {
return 'allow';
}
}Framework guides: Supabase, Firebase, Auth.js / NextAuth. For Better Auth there is an open-source plugin, better-auth-vetto.
3. POST /v1/verify (server, Pro)
Bots can skip your front end and post straight to your signup endpoint. Check the vetto_token that the script added to the form, together with the submitted email.
curl -X POST https://usevetto.com/v1/verify \
-H "Authorization: Bearer $VETTO_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"token": "<vetto_token from the form>", "email": "jane@gmail.com"}'{
"ok": true,
"valid": true,
"reason": null,
"verdict": "allow",
"reasons": [],
"repeat": 0,
"expiresAt": 1792336800
}reason is null when valid, otherwise malformed, bad_signature, expired, email_mismatch, blocked or wrong_site.
Offline verification
You can skip the network call: the token is an HMAC signed with your secret key and valid for 15 minutes.
import { createHmac, createHash, timingSafeEqual } from 'node:crypto';
// token = base64url(payload JSON) + "." + base64url(HMAC-SHA256(secretKey, firstPart))
export function verifyVettoToken(token, email, secretKey) {
const [body, sig] = String(token || '').split('.');
if (!body || !sig) return false;
const expected = createHmac('sha256', secretKey).update(body).digest('base64url');
if (expected.length !== sig.length || !timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) return false;
const p = JSON.parse(Buffer.from(body, 'base64url').toString());
const h = createHash('sha256').update(email.trim().toLowerCase()).digest('hex');
return p.x > Date.now() / 1000 && p.v !== 'block' && p.h === h;
}4. POST /v1/audit: free domain lookup (no key)
Check up to 50 email domains per call for throwaway providers and dead domains. No account or key needed, CORS open. It is the endpoint behind the free email list audit, and it is rate-limited to 300 calls a day per IP.
curl -X POST https://usevetto.com/v1/audit \
-H "Content-Type: application/json" \
-d '{"domains": ["mailinator.com", "gmail.com", "doesnotexist-xyz-123.com"]}'{
"ok": true,
"results": {
"mailinator.com": { "status": "skipped", "mx": [], "disposable": true },
"gmail.com": { "status": "ok", "mx": [], "disposable": false },
"doesnotexist-xyz-123.com": { "status": "nxdomain", "mx": [], "disposable": false }
}
}| status | Meaning |
|---|---|
| ok | Domain can receive mail. mx lists its mail servers (empty for well-known free providers). |
| skipped | Known throwaway domain, not looked up (disposable is true) |
| nxdomain, no_mx_a, null_mx, no_mail | Domain cannot receive mail |
| deferred | This call ran out of DNS lookups: send those domains again in the next call |
| error | DNS lookup failed; treat as unknown |
disposable is also true for unknown domains whose mail servers belong to a known throwaway service. Prefer batches of about 20 domains.
Signals
Each signal has a default action you can change per site in the dashboard. On Free, Pro signals run in shadow mode: they show in the API response from your dashboard but never affect the verdict.
| Code | Fires when | Default | Plan |
|---|---|---|---|
| invalid | Malformed address | block | Free |
| typo | Likely typo of a common domain (gmial.com); comes with a suggestion | suggest | Free |
| disposable | Known throwaway domain, or a domain on known throwaway mail servers | block | Free |
| dead | Domain cannot receive mail (no MX/A record, null MX, NXDOMAIN) | block | Free |
| relay | Privacy relay (Apple Hide My Email, Firefox Relay, DuckDuckGo…) | allow | Free |
| bot | Hidden honeypot field was filled | block | Free |
| lookalike | Domain imitates a popular provider | warn | Pro |
| repeat | Same Gmail inbox already signed up through dots, +tags or googlemail.com | warn | Pro |
| network | Signup from a VPN or data-center network | flag | Pro |
| burst | Too many signup attempts from one IP in an hour | block | Pro |
| freemail | Free webmail address (gmail.com, outlook.com…) | allow | Pro |
| role | Role address (admin@, info@, support@…) | allow | Pro |
| denylist | Matches your own denylist | block | Pro |
Errors and limits
Errors return { "ok": false, "error": "<code>", "message": "…" }. The script tag treats every error and timeout as allow.
| Status | error | When |
|---|---|---|
| 400 | invalid_json, bad_key, missing_email | Body is not a JSON object, the site key is malformed, or email is missing |
| 401 | bad_secret | Missing or unknown secret key |
| 402 | pro_required | The server API and /v1/verify need a Pro site |
| 404 | unknown_site, not_found | Unknown site key or endpoint |
| 429 | rate_limited | Too many checks from one IP (browser calls) |
Frequently asked questions
Is there a free API for disposable email detection?
Yes, through the browser. On the Free plan the script tag calls https://usevetto.com/v1/check with your public site key for one site and 1,000 distinct emails a month, no card. Calling the API from your server with a secret key is part of Pro.
What happens when Vetto is down or slow?
Everything fails open. The script lets the visitor through on any error or timeout, and over-quota or paused sites get verdict "allow" with failOpen: true. Do the same in your server code: treat errors as allow.
Do repeated checks of the same email count against the quota?
No. The monthly quota counts distinct addresses on committed signups. Read-only checks (commit: false) are not counted.
How long is a vetto_token valid?
15 minutes. It is bound to the email and site, and it is never issued for a blocked verdict.
Give your signup form sharp eyes.
Free on one site. One line of HTML. Remove it any time and your form works exactly as before.
Founding offer: Pro for life for $69 once, first 100 buyers, until Nov 27.