Skip to content
All Posts
SDK

RevAddress Checkout Guard: Drop-In Address Risk Widget

·Updated ·6 min read·By RevAddress·SDK

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

One tag, before the closing body tag
<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:

POST /api/widget/check
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:

functions.php
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

Theme-owned address form
<!-- 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:

checkout.risk_detected payload
{
"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

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.