Form protection
On this page
Form protection checks the email field on your own sign-up, contact and checkout forms while the visitor types. Add one script tag and a data-simplecold attribute to the field, and SimpleCold stops disposable addresses, catches typos like [email protected] with a "Did you mean …?" link, and (in full mode) rejects mailboxes that don't exist, before the form is submitted.
It uses a publishable key (sc_pub_…), not your secret API key. A publishable key is safe to put in public HTML: it only works on the domains you allow, it can only check addresses, and it never returns more than a short verdict.
Basic and full mode
Every publishable key has a mode:
| Mode | What it checks | Cost |
|---|---|---|
basic | The address format, disposable and throwaway domains, shared addresses like info@, and common domain typos. | Free. Answers instantly. |
full | Everything in basic, plus whether the mailbox exists and whether the domain accepts every address. | 1 credit per unique address. |
In full mode the result for an address is cached for 30 days. A visitor who submits the same address twice, or a second form that sees an address already checked in the last 30 days, doesn't use another credit.
Full mode answers within about 1.5 seconds. If the visitor's mail server is slow to answer, the widget gets the reason unverified and lets the address through, so a slow mail server never holds up your form.
Create a publishable key
| Kind of address | Default | Modes |
|---|---|---|
| Disposable | Block | Basic and full |
Shared (info@, sales@, …) | Warn | Basic and full |
| Risky (accept-all domains, mailboxes we can't fully confirm) | Warn | Full only |
| Undeliverable (no such mailbox or domain) | Block | Full only |
Invalid addresses and likely typos are handled in every mode. Only the owner and admins of an organization can create, edit and delete publishable keys; every member can see them and their usage. You can have up to 10 publishable keys per organization.
Allowed domains
The widget only answers requests from pages on the domains you allow. Each entry is either:
A wildcard does not include the domain itself: *.example.com doesn't match example.com. If your form is on both, add both. Leave out the scheme, port and path. Add localhost to try the widget on your own machine. A key can have up to 20 allowed domains.
Install the snippet
Copy the snippet from the key's page in the dashboard, or use this one with your publishable key:
<script src="https://simplecold.com/api/v1/widget/v1.js" data-key="sc_pub_XXXX" defer></script>
<input type="email" name="email" data-simplecold>Add the script once per page, anywhere in the HTML. Then add data-simplecold to every email input you want checked. The script is under 8 KB and has no dependencies.
When the visitor enters an address, the widget checks it and shows a short message next to the input. If the verdict is block, the submit is stopped until the address is fixed.
Script options
Set these as data attributes on the <script> tag:
| Attribute | Default | What it does |
|---|---|---|
data-key | – | Required. Your publishable key. |
data-selector | inputs with data-simplecold | A CSS selector for the inputs to protect, for example form#signup input[name=email]. |
data-block-submit | true | Set to "false" to never stop a submit and only show messages. |
data-turnstile | false | Set to "true" to send the form's Cloudflare Turnstile token with each check. Needs a Turnstile secret on the key. |
Messages
The default messages are:
| Reason | Message |
|---|---|
invalid | Please enter a valid email address. |
typo | Did you mean [email protected]? |
disposable | Please use a permanent email address, not a disposable one. |
role | Please use a personal address rather than a shared one like info@ or sales@. |
risky | We could not fully confirm this address. Please double-check it. |
undeliverable | This email address does not seem to exist. |
Change any of them with a data-msg-<reason> attribute on the input:
<input
type="email"
name="email"
data-simplecold
data-msg-disposable="Please sign up with your work email."
data-msg-role="Please use your own address, not a team inbox."
>Styling
The message is a <div class="sc-fp-msg" data-verdict="warn" aria-live="polite"> shown next to the input. data-verdict is ok, warn or block. Change the look with these CSS variables on any ancestor, for example :root:
| Variable | Default |
|---|---|
--sc-fp-ok | #15803d |
--sc-fp-warn | #b45309 |
--sc-fp-block | #b91c1c |
--sc-fp-font-size | 13px |
--sc-fp-gap | 4px |
--sc-fp-link | inherit |
:root {
--sc-fp-warn: #9a6700;
--sc-fp-block: #c2410c;
--sc-fp-font-size: 14px;
}Events
The widget dispatches two DOM events on the input. Both bubble, so you can listen on the input, the form or document.
| Event | detail | When |
|---|---|---|
simplecold:verdict | { email, allow, verdict, reason, suggestion } | Every time an address is checked. |
simplecold:suggestion | { from, to } | The visitor clicked "Did you mean …?" and the input was corrected. |
const form = document.querySelector("#signup");
form.addEventListener("simplecold:verdict", (event) => {
const { email, allow, verdict, reason, suggestion } = event.detail;
// Turn the submit button off while the address is blocked.
form.querySelector("button[type=submit]").disabled = !allow;
if (verdict === "warn") console.log(`${email}: ${reason}`);
});
form.addEventListener("simplecold:suggestion", (event) => {
const { from, to } = event.detail;
analytics.track("email_typo_fixed", { from, to });
});The check endpoint
The widget calls these public endpoints. You don't need to call them yourself, but they are documented so you know exactly what leaves the page.
| Method | Path | What it does |
|---|---|---|
GET | /api/v1/widget/v1.js | The script. |
POST | /api/v1/widget/session | Starts a page session: { key, turnstileToken? } returns the first { nonce }. |
POST | /api/v1/widget/check | Checks one address and returns the verdict and the next nonce. |
POST | /api/v1/widget/event | Records that the visitor accepted a suggestion: { key, event: "suggestion_accepted", nonce }. |
/api/v1/widget/checkThey need no API key and no session cookie. Requests must come from a page on one of the key's allowed domains: the Origin (or Referer) header is checked against the list.
{
"key": "sc_pub_XXXX",
"email": "[email protected]",
"nonce": "…"
}turnstileToken is added when the script has data-turnstile="true".
{
"allow": true,
"verdict": "warn",
"reason": "typo",
"suggestion": "[email protected]",
"nonce": "…"
}| Field | Type | Description |
|---|---|---|
allow | boolean | false only when the verdict is block. |
verdict | string | ok, warn or block, from the key's rules. |
reason | string | ok, invalid, typo, disposable, role, risky, undeliverable or unverified. |
suggestion | string or null | The corrected address for a likely typo, for example [email protected]. |
nonce | string | The nonce for the next request (see Bot protection). |
The answer is deliberately minimal. It never includes the full verification result, the mail server, or anything else about the address. Use the single validation endpoint with your secret API key on your server if you need the full result.
Fails open
The widget never breaks your form. If our service is slow or unreachable, a request fails, the key is turned off, its monthly limit is reached, or the visitor is rate limited, the form submits as normal without a check. In full mode, a mailbox that doesn't answer within about 1.5 seconds comes back as unverified with allow: true.
Because the widget runs in the visitor's browser, a determined visitor can always bypass it. If you need a guarantee, also check the address on your server with your secret API key.
Rate limits
The widget endpoints are rate limited per publishable key and per visitor IP address, so one visitor can't use up your monthly checks or credits. Normal visitors never get near the limits. A rate-limited request gets a 429, and the widget lets the form submit without a check.
The monthly check limit on the key caps how many addresses it checks in a calendar month (10,000 by default; you can raise, lower or remove it). After the limit, the form keeps working without checks until the next month. In full mode, this is also your ceiling on credits spent by the form.
Bot protection
A publishable key is public, so every check is protected against scripted abuse: