RevAddress Checkout Guard: Drop-In Address Risk Widget
Checkout Guard checks a shipping address while the customer is still typing and renders a badge under the form. One script tag, no dependencies, no build step. It works on any address form you control: custom HTML, a WooCommerce checkout, a Magento template, a Shopify theme page. It does not work inside Shopify’s hosted checkout, which does not accept arbitrary script tags outside Shopify Plus.
This is the contract, written from the shipped widget rather than a roadmap.
Install
<script src="https://revaddress.com/sdk/revaddress-guard.js"
data-api-key="rv_live_pub_your_key_here"
data-target="#shipping-address"
defer></script>The widget finds the address inputs inside data-target, listens for input and change, waits 600 milliseconds after typing stops, and posts to POST /api/widget/check with an Authorization: Bearer header carrying your publishable key.
Two behaviours worth knowing before you test. It will not fire until the street field holds at least five characters, so a partially typed address stays silent. And if the target selector is not in the DOM yet, it retries every 800 milliseconds instead of giving up, which is what makes it work on forms that render after page load.
Attributes
| Attribute | Default | What it does |
|---|---|---|
data-api-key |
required | Your publishable key (rv_live_pub_…). Without it the widget logs a warning and stops |
data-target |
body |
CSS selector for the form container |
data-field-street |
auto-detect | Explicit selector for the street input |
data-field-city |
auto-detect | Explicit selector for the city input |
data-field-state |
auto-detect | Explicit selector for the state or province input |
data-field-zip |
auto-detect | Explicit selector for the ZIP or postal input |
data-api-base |
https://api.revaddress.com |
Override for a staging environment |
Setting data-field-street switches the whole widget into explicit mode. The other three selectors are only read when the street selector is present, so specify all four or none.
How field discovery actually works
The widget does not look for exact attribute values. It concatenates each input’s name, id, and autocomplete into one lowercase string and matches a pattern against it, taking the first input that matches each role:
| Role | Pattern it matches |
|---|---|
| Street | address1, address-1, address line, or street anywhere in the string |
| City | city, locality, or level2 |
| State | state, province, region, or level1 |
| ZIP | zip, postal, or postcode |
That covers name="shipping_address_1", autocomplete="address-line1", id="billing-city", and most of what checkout frameworks emit. It also means a field named address2 matches the street pattern, so on a form where the apartment input comes first in DOM order, the widget will bind to the wrong one. That is the case to set explicit selectors for.
For single-page apps, a MutationObserver watches the container and re-binds if the street input is replaced, which is what happens on most framework re-renders.
Badge states
The badge lives in a closed shadow root inserted after the closest .form-group, .field, or .form-row wrapping your ZIP input, falling back to the street input. Closed shadow DOM means your site CSS cannot reach it and it cannot leak into yours; it also means you cannot restyle it.
Three colours, five messages. The verified states below are what the badge reports once a USPS license is connected to your account; see plan access further down for what it says before that.
| Badge | Message | When |
|---|---|---|
| Green | Verified delivery address | Confirmed deliverable, nothing corrected |
| Green | Address check complete | The check ran and returned no strong signal |
| Yellow | Address corrected to USPS standard | The standardized form differs from what was typed |
| Yellow | Unit number may be needed | The building matched but the apartment or suite did not |
| Red | Address could not be verified | Not found, or scored as high risk |
There is no grey state and no fourth colour. A green badge fades out after three seconds; yellow and red stay. While a check is in flight the badge shows a spinner and the word “Verifying”.
The response behind the badge
If you want to make a decision rather than show a hint, call the endpoint yourself. It returns five fields and nothing else, deliberately: the full intelligence payload is not exposed to a browser.
Here it is against a sandbox key and one of the reserved test streets, which anyone can run without an account of their own:
curl -X POST https://api.revaddress.com/api/widget/check \
-H "Authorization: Bearer rv_test_pub_your_key_here" \
-H "Content-Type: application/json" \
-d '{"street":"3 TEST CORRECT ST","city":"Testville","state":"CA","zip":"90210"}'{
"status": "valid",
"corrected_address": {
"street": "3 TEST CORRECT ST",
"secondary": "",
"city": "TESTVILLE",
"state": "CA",
"zip": "90210-1234"
},
"risk_level": "safe",
"missing_unit": false,
"message": "Address corrected to USPS standard",
"sandbox": true
}
A live key returns the same five fields without the sandbox flag. status is one of valid, partial, undeliverable, or risky. risk_level is safe, elevated, or high. corrected_address is null unless the standardized street or city differs from the input, so its presence is the signal that something was changed rather than merely confirmed.
Two rules govern the endpoint. Only publishable keys are accepted: send a secret key and it answers 403 with secret_key_rejected rather than processing the request. And it is rate-limited to 10 requests per minute per visitor IP, separately from your account’s own limits.
What it does when the check fails
This is the part to understand before you wire anything to it.
If the upstream address service errors or times out, the endpoint returns status: "valid" with a null message. The widget then shows nothing and your checkout proceeds exactly as it would without the script. That is deliberate. An address widget that blocks orders during an upstream incident costs more than the bad addresses it prevents.
The consequence is that a green badge means “no problem was found”, which is not the same as “this address was verified against a postal database this second”. If your business rule needs the stronger claim, read status and risk_level from your own server-side call rather than trusting the badge.
Confirmed results are cached for seven days, so a repeat customer typing the same address does not spend a second lookup.
Test every state before you ship
Generate a sandbox publishable key prefixed rv_test_pub_. Four street values return fixed responses with no upstream call and no cost:
| Street you type | What comes back |
|---|---|
1 TEST FAIL ST |
undeliverable, high risk, red badge |
2 TEST UNIT ST |
partial, missing_unit: true, yellow badge |
3 TEST CORRECT ST |
valid with a corrected_address, yellow badge |
4 TEST RISKY ST |
risky, high risk, red badge |
| anything else | valid, safe, green badge |
Type each one into your real form with the sandbox key in place. That is a five-minute pass that catches the two failures nobody catches otherwise: the badge landing in the wrong place because your form markup has no .form-group wrapper, and the widget binding to address2 instead of address1.
WooCommerce
WordPress strips unknown attributes from enqueued scripts, so add them with a filter rather than trying to pass them to wp_enqueue_script:
add_action( 'wp_enqueue_scripts', function () {
if ( ! is_checkout() ) {
return;
}
wp_enqueue_script(
'revaddress-guard',
'https://revaddress.com/sdk/revaddress-guard.js',
[],
null,
true
);
} );
add_filter( 'script_loader_tag', function ( $tag, $handle ) {
if ( 'revaddress-guard' !== $handle ) {
return $tag;
}
return str_replace(
' src=',
' data-api-key="' . esc_attr( REVADDRESS_PUB_KEY ) . '"'
. ' data-target="form.checkout" defer src=',
$tag
);
}, 10, 2 );WooCommerce names its fields shipping_address_1, shipping_city, shipping_state, and shipping_postcode. All four match the discovery patterns, so no explicit selectors are needed. Scoping data-target to form.checkout keeps the widget off the billing block.
Shopify
<!-- Add before </body> on a theme page that owns its address fields -->
<script src="https://revaddress.com/sdk/revaddress-guard.js"
data-api-key="rv_live_pub_your_key_here"
data-target="#address_form"
defer></script>Shopify’s hosted checkout cannot be modified with arbitrary script tags on non-Plus stores. The two places this works are the customer account address form and any branded pre-checkout page you own. For validation inside checkout itself, Shopify Checkout UI Extensions is the supported path.
Plan access, and what the license buys
Two different things are being paid for here, and conflating them is how people end up disappointed.
The widget itself ships with the Growth plan and above, and each check spends one request from that plan’s monthly allowance, the same pool your direct API calls draw from. The pricing page carries the current allowances and prices, which is where to read them rather than a figure typed into a blog post.
What the widget can tell you depends on a USPS license connected to your account. Delivery-point verification, ZIP+4, and the CMRA and vacancy flags are USPS data, and USPS data has required a signed license since August 1, 2026. So does the fraud score, because its signals are derived from those same delivery-point records. Connect your own license once from the dashboard — about three minutes — and the verified states above are what the badge reports, with the OAuth lifecycle, caching, and retries handled for you.
Without a connected license, address standardization and geocoding still run against Census Bureau data at no per-lookup cost, which catches malformed and nonexistent streets. They do not return DPV or any USPS deliverability flag, so a green badge on that path means the address parsed and standardized, not that USPS confirmed a mailbox.
A high-risk address also fires a checkout.risk_detected webhook to any endpoint registered in your dashboard. The webhook plumbing is part of the Growth plan; the score that triggers it rides the same connected license. Use it for review queues and fulfilment holds rather than anything customer-facing:
{
"event": "checkout.risk_detected",
"fraud_risk_score": 0.75,
"fraud_risk_level": "high",
"risk_signals": [
{ "signal": "cmra_detected", "weight": 0.40, "detail": "Commercial mail receiving agency" }
],
"recommended_action": "review",
"timestamp": "2026-04-15T12:00:00Z"
}What it does not do yet
Named plainly, so you do not build against something that is not there. The widget dispatches no custom browser events, exposes no JavaScript API for reading the last result, and cannot block form submission on its own. Anything conditional belongs in your own call to POST /api/widget/check, where you get the full response and decide what to do with it.
Start here
- Get a publishable key — generate a
rv_test_pub_key first and walk the four reserved test streets before a live key touches your form - Plans and allowances — the widget ships with Growth and above; USPS-verified results run on a license you connect
- WooCommerce USPS migration — if your rates broke in the same checkout
Questions
- Does Checkout Guard block a customer from placing an order?
- No. The widget renders a badge and nothing else. It never blocks submit, and it fails open by design. If the upstream check errors, the badge hides and checkout continues untouched. To gate submission, call POST /api/widget/check from your own code and decide there.
- Can I use it on Shopify checkout?
- Not on the hosted checkout, which does not accept arbitrary script tags outside Shopify Plus. Use it on theme-owned surfaces such as the account address form or a pre-checkout page, and use Shopify Checkout UI Extensions for in-checkout validation.
- Is it safe to put the key in client-side HTML?
- That is what a publishable key is for. Keys prefixed rv_live_pub_ are the only ones the widget endpoint accepts; a secret key sent to it is refused with HTTP 403 and the error code secret_key_rejected rather than being processed.
- How do I test the badge states without a real bad address?
- Use a sandbox publishable key prefixed rv_test_pub_. Four street values return fixed responses with no upstream call — 1 TEST FAIL ST, 2 TEST UNIT ST, 3 TEST CORRECT ST, and 4 TEST RISKY ST. Anything else returns the clean default.
- What does it cost per address?
- Each check spends one request from your plan's monthly allowance, the same pool your direct API calls draw from. The widget ships with Growth and above; the USPS-verified part of the answer — DPV, ZIP+4, CMRA and vacancy flags, and the fraud score derived from them — runs on a USPS license you connect to your account, never as a flat-rate inclusion. The endpoint is also capped at 10 requests per minute per visitor IP, which no real customer typing an address will reach.
Read next
WooCommerce USPS Migration: From Web Tools to the v3 REST API
USPS Web Tools died January 25, 2026. Update the official extension to 5.2.5, enter REST credentials, and handle the 60-per-hour cap it does not cache.
9 min readGuideUSPS API Pricing 2026 — What It Actually Costs
USPS address validation stopped being free on August 1, 2026. The published fee curve, the license that gates it, and what direct vs third-party costs.
8 min read