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 secret sk_… 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.

HTML
<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.

HTML (one form only)
<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
curl -X POST https://usevetto.com/v1/check \
  -H "Authorization: Bearer $VETTO_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "jane@mailinator.com"}'
Response 200
{
  "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
}
FieldMeaning
verdictallow, flag, suggest, warn or block. Reject the signup on block; flag means "allow but mark for review".
messageText you can show the user, or null
suggestionCorrected address for typos (jane@gmail.com), or null
reasonsSignals that fired, as { code, action }
tagsInformational labels such as relay, freemail, role
repeatPro: how many other aliases of the same Gmail inbox already signed up
tokenSigned verdict on committed, non-blocked checks (Pro), else null
failOpentrue when the check was skipped (site paused or over quota); verdict is then allow
Node.js (18+)
// 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
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"}'
Response 200
{
  "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.

Node.js
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
curl -X POST https://usevetto.com/v1/audit \
  -H "Content-Type: application/json" \
  -d '{"domains": ["mailinator.com", "gmail.com", "doesnotexist-xyz-123.com"]}'
Response 200
{
  "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 }
  }
}
statusMeaning
okDomain can receive mail. mx lists its mail servers (empty for well-known free providers).
skippedKnown throwaway domain, not looked up (disposable is true)
nxdomain, no_mx_a, null_mx, no_mailDomain cannot receive mail
deferredThis call ran out of DNS lookups: send those domains again in the next call
errorDNS 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.

CodeFires whenDefaultPlan
invalidMalformed addressblockFree
typoLikely typo of a common domain (gmial.com); comes with a suggestionsuggestFree
disposableKnown throwaway domain, or a domain on known throwaway mail serversblockFree
deadDomain cannot receive mail (no MX/A record, null MX, NXDOMAIN)blockFree
relayPrivacy relay (Apple Hide My Email, Firefox Relay, DuckDuckGo…)allowFree
botHidden honeypot field was filledblockFree
lookalikeDomain imitates a popular providerwarnPro
repeatSame Gmail inbox already signed up through dots, +tags or googlemail.comwarnPro
networkSignup from a VPN or data-center networkflagPro
burstToo many signup attempts from one IP in an hourblockPro
freemailFree webmail address (gmail.com, outlook.com…)allowPro
roleRole address (admin@, info@, support@…)allowPro
denylistMatches your own denylistblockPro

Errors and limits

Errors return { "ok": false, "error": "<code>", "message": "…" }. The script tag treats every error and timeout as allow.

StatuserrorWhen
400invalid_json, bad_key, missing_emailBody is not a JSON object, the site key is malformed, or email is missing
401bad_secretMissing or unknown secret key
402pro_requiredThe server API and /v1/verify need a Pro site
404unknown_site, not_foundUnknown site key or endpoint
429rate_limitedToo 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.