The Spaw form helper is a small script that watches the email, phone and address inputs you mark, verifies the value when the visitor leaves the field, and renders a hint: for an email address a "Did you mean …?" correction when it looks like a typo and a notice when it cannot receive mail; for a phone number its international format when it is valid, a note when it is fictional, shared on SMS-receiving sites or virtual, and the reason when it is not valid; for a shipping or billing address the address as the country writes it, a note when it is a box or a locker rather than a front door, and the reason when it cannot stand as written. It never blocks a submit and never waits for the API, so a slow network or an empty credit balance only means the hint does not appear. It works on any page where you can add custom code, which covers Webflow, Framer, Squarespace, Carrd and a hand-written HTML form alike.
Add it to a form
- On the API keys page, create a publishable key and lock it to the domains your form runs on. A publishable key can only run single lookups from those origins, so it is safe in page source.
- Give the key a daily credit cap. Once the cap is spent the browser endpoint answers
429 KEY_SPEND_CAP_REACHEDuntil the next day, and the helper stays quiet. - Add the script and mark the input:
<script src="https://spaw.co/spaw-form.js" data-key="pk_live_…" defer></script>
<input type="email" name="email" data-spaw-email>
- For forms open to the public, store a Cloudflare Turnstile site key and secret on the publishable key and add
data-turnstile-site-keyto the script tag. The helper then fetches a fresh single-use token before every lookup and the endpoint refuses lookups without one, so a bot scripting your form cannot spend your credits. Turnstile only shows a challenge when Cloudflare needs one.
Phone fields
Mark the phone input data-spaw-phone. Numbers typed without a calling code need a country: fix it with data-spaw-country="GB" on the input, or name a country select on the same form with data-spaw-country-field and the helper reads the ISO code from it on every check.
<select name="country">
<option value="GB">United Kingdom</option>
<option value="US">United States</option>
</select>
<input type="tel" name="phone" data-spaw-phone data-spaw-country-field="country">
The hint shows the international format of a valid number, a note for fictional, disposable (shared on SMS-receiving websites) and virtual numbers, and a plain-words reason for an invalid one; a number in a block the regulator has not allocated reads "not in service". The live carrier check is never run from the browser.
Address fields
Mark the address input data-spaw-address. An address only means something measured against one country's postal format, so this mode always needs a country: data-spaw-country="GB" on the input, or a country select on the same form named by data-spaw-country-field. Until a country is there the helper stays quiet rather than spending a round trip on a lookup that would be refused, and the address is checked as soon as the select has an answer: the helper listens to that select as well as to the address field, so the ordinary checkout order — address typed, country picked afterwards — still gets the address checked, and picking the country again for a different one re-checks it.
<select name="country">
<option value="GB">United Kingdom</option>
<option value="US">United States</option>
</select>
<input name="address" data-spaw-address data-spaw-country-field="country">
Mark the one field that holds the whole address as it would be written on an envelope — house number, street, town and postcode together, on one line or several. That written line is what is sent, under address. The separated address_line1 and address_line2 fields are ignored whenever a written address is present, so a form that spreads the address over several inputs is a job for a server-side call rather than for this helper.
There is no single verdict word for an address, so the hint only says what the answer says. An address that cannot stand as written shows its reason in plain words: add the street and house number, add the town or city, that postcode is not the right shape for this country, that postcode has the right shape but the national directory does not list it. An address that can stand shows a note when address_type says the place is somewhere post is collected rather than a front door — a post-office box, a private mailbox at a mail-receiving agency, a parcel locker, a counter for general delivery — and otherwise the address as the country writes it, which is where a transposed house number or a dropped town shows itself. postal_code_valid is never false on an address that stands — a postcode of the wrong shape comes back as the reason instead — so it is true where a postcode was checked and null where there was none to check. The licensed deliverability check is never run from the browser.
When you cannot set an attribute on the input
Some builders let you add custom code to the page but not attributes to a form field. Mark the field from a short script placed before the helper's script tag. Listeners registered earlier run first, so the input is marked before the helper looks for it:
<script>
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('form input[type="email"]').forEach(function (input) {
input.setAttribute('data-spaw-email', '');
});
});
</script>
<script src="https://spaw.co/spaw-form.js" data-key="pk_live_…" defer></script>
In Webflow and Framer, paste both tags into the site's custom code section that lands in the head; Squarespace and Carrd take them in the page or site code injection settings. On a plain HTML page, put them anywhere in the document.
Keep your secret key on the server instead
If you would rather not use a publishable key, point data-endpoint at a route you host. The helper posts { "email": "…" } there and expects the API's JSON back unchanged; the route adds your secret key and forwards the request to POST /api/v1/email. Phone inputs proxy through data-phone-endpoint on the script tag (or data-endpoint on the phone input itself): that route receives { "phone": "…", "country": "GB" } and forwards it to POST /api/v1/phone. Address inputs use data-address-endpoint the same way, receive { "address": "…", "country": "GB" } and forward to POST /api/v1/address. A five-line proxy in any framework does it; the product page shows Laravel and Node versions. Rate-limit the routes, because they spend your credits.
Enforce your own policy
Every verdict is emitted as a bubbling spaw:result event on the input, with the full API response in event.detail plus type (email, phone or address) and the checked input. The helper itself only hints; the event is where you decide, and type is what tells the three signals apart:
<script>
addEventListener('spaw:result', function (event) {
var result = event.detail;
if (result.type === 'phone') {
if (!result.valid || result.is_disposable || result.is_fictional) {
document.querySelector('button[type="submit"]').disabled = true;
}
} else if (result.type === 'address') {
if (!result.valid || result.address_type === 'po_box') {
document.querySelector('button[type="submit"]').disabled = true;
}
} else if (result.disposable || result.deliverable === 'undeliverable') {
document.querySelector('button[type="submit"]').disabled = true;
}
});
</script>
The email, phone and address responses are the same answers the API gives a server, field for field, each minus the billing block, so page visitors never see your balance. Branch on deliverable, use did_you_mean to offer the fix, and read reason before refusing anything marked risky: a catch-all domain is risky but often a fine address. For an address, risk_score and risk_signals spell out why a place looks like somewhere post is collected rather than a home, and refusing a post-office box is a shipping policy, not a judgement about the person typing it.
Cost
One credit per answered lookup (an IP credit covers five), charged to the key's owner. Unverified email answers, undeliverable ones within the fair-use allowance, invalid numbers, addresses that cannot stand as written, repeats within seven days and the free test addresses on spaw.test cost nothing and never count toward the key's daily cap. Every account gets 100 free credits a month.