Skip to content
All Posts
Migration Guide

EasyPost to USPS API Migration: Every Call, Mapped

·Updated ·13 min read·By RevAddress·Migration Guide

EasyPost’s March 17, 2026 auto-enrollment is what sent most people to this page, and the options and cost piece covers whether moving is the right call at your volume. This one assumes you have decided, and walks the migration.

Short version of the shape: the code is small and the paperwork is not. Address validation is a one-call swap. The USPS license behind labels and rates takes weeks. Sequence accordingly.

Start the USPS license first

This is the step that was not in the original version of this guide, because it did not exist yet. Since August 1, 2026 the USPS Addresses API requires a signed license agreement and bills on a consumption curve, and label creation additionally needs a funded Enterprise Payment Account.

That splits the migration into two halves you can run in parallel:

  • No postal agreement needed. Standardization, geocoding and address extract run against US Census Bureau data. 1,000 requests a month on the free tier, no card. If list hygiene and coordinates are the whole job, you are done after the code swap.
  • Your own USPS license. DPV confirmation, ZIP+4, rates, service standards, tracking and labels run against USPS with credentials you hold. Sign the agreement, fund the payment account, connect the credentials.

The USPS API pricing guide documents the fee curve and the signature sequence, and the CRID and MID enrollment guide documents the account setup with the errors we hit doing it. Start there today; write the code below while it clears.

The endpoint mapping

The surface is smaller than an EasyPost integration makes it look, because EasyPost wraps a multi-carrier abstraction over what is, for USPS-only shippers, four operations.

EasyPost RevAddress
client.address.create(verify=["delivery"]) POST /api/address/validate
client.Shipment.create() then .buy() POST /api/labels
client.Tracker.create() GET /api/tracking/{trackingNumber}
client.Shipment.lowestRate() POST /api/rates

The two-step shipment dance collapses into one label call, because there is no rate object to buy against: you quote a rate if you want one, then create the label. The full Web Tools and v3 mapping, if you are also carrying legacy XML, is in the endpoint mapping guide.

Address validation, before and after

Before — EasyPost, Pythonpython
import easypost

client = easypost.EasyPostClient("EASYPOST_API_KEY")

address = client.address.create(
  street1="1600 Pennsylvania Ave NW",
  city="Washington",
  state="DC",
  zip="20500",
  country="US",
  verify=["delivery"],
)

print(address.verifications.delivery.success)

After the swap there are two honest paths, and the original version of this guide blurred them into one. Pick deliberately.

Path 1: the RevAddress REST API. One key, our infrastructure, caching and retries handled.

After — RevAddress REST APIbash
curl -X POST "https://api.revaddress.com/api/address/validate" \
-H "X-API-Key: rv_live_your_key" \
-H "Content-Type: application/json" \
-d '{
  "streetAddress": "1600 Pennsylvania Ave NW",
  "city": "Washington",
  "state": "DC",
  "ZIPCode": "20500"
}'

The response carries the standardized address, the DPV confirmation, and a resolution block naming what to do next:

Responsejson
{
"address": {
  "streetAddress": "1600 PENNSYLVANIA AVE NW",
  "city": "WASHINGTON",
  "state": "DC",
  "ZIPCode": "20500",
  "ZIPPlus4": "0005"
},
"additionalInfo": {
  "DPVConfirmation": "Y",
  "vacant": "N",
  "deliveryPoint": "00",
  "business": "Y"
},
"cached": false,
"resolution": {
  "classification": "deliverable_exact",
  "nextAction": "done",
  "userMessage": "USPS confirmed this address."
}
}

resolution.nextAction is the field to branch on. A D or S confirmation returns collect_secondary, which is the difference between a checkout that asks for an apartment number and one that shows a red box.

Mapping the response, field by field

The endpoint table above is the easy half. The half that produces bugs three weeks later is the response, because EasyPost’s shape and USPS’s shape disagree about what an answer is.

EasyPost RevAddress What changes
verifications.delivery.success resolution.classification + resolution.nextAction A boolean becomes a verdict and the action it implies
verifications.delivery.errors[] resolution.userMessage One sentence per verdict, written for a customer to read
verifications.zip4.success address.ZIPPlus4 The ZIP+4 itself, not a flag saying one was found
verifications.delivery.details.latitude coordinates on Census-standardized answers only Not on a USPS-verified answer; use the geocoding route
corrected street1 address.streetAddress Full form, with streetAddressAbbreviation returned beside it
corrected street2 address.secondaryAddress An explicit field; nothing is moved between fields for you
zip address.ZIPCode Five digits, with the +4 kept separate

Four of those rows deserve more than a table cell.

A boolean cannot carry the state you most want. verifications.delivery.success is true or false. USPS’s delivery-point confirmation has four outcomes, and the second one is the valuable one: the building matched but the unit did not. That address is not undeliverable. It is deliverable the moment somebody supplies an apartment number, and a checkout that asks for one converts where a checkout showing a validation error does not. Branch on resolution.nextAction, not on a truth value, or you will have folded four states back down to two on your first day.

Coordinates move. EasyPost hands back latitude, longitude and a time zone inside the verification details. A USPS-verified response from POST /api/address/validate carries none — geocoding is its own route. One caveat worth writing down before it surprises you: when the answer is standardized against Census Bureau reference data instead — what a standardization-tier key receives, and what every key receives while USPS confirmation is unavailable — the payload does include a coordinates object alongside resolution.classification: "standardized_unverified". So the field is present on some answers and absent on others, and presence is not a thing to branch on. If anything downstream reads a coordinate, point it at the geocoding route, and find it before the cutover rather than after.

EasyPost has been mutating your input. Two behaviors, both documented on EasyPost’s own address verification guide, read August 25, 2026. Since August 25, 2025 it stops abbreviating street names on USPS verification unless the validated street1 runs past 40 characters. And for US and Canada addresses, when street1 runs past 35 characters with an empty street2 and ends in something it recognizes as a unit, such as Apt 101 or Suite 205, it moves that fragment into street2 for you. Neither happens here: streetAddress and secondaryAddress are what you sent, standardized, and a unit typed into the street line stays in the street line. If your stored addresses were split by that logic rather than by your own form, the split is now yours to do.

verify: false still verifies. The same guide is explicit that including the verify parameter turns verification on regardless of its value, and that only omitting it prevents the call. Anyone passing a flag through from config has been paying for verification they believed was off. Worth reconciling your invoice against your config before you use either as a migration baseline.

Run both, then compare

Address validation and tracking are read-only, so there is no reason to cut over blind. Send every address to both services for a week and log the pair.

Dual-run comparisonpython
import easypost, requests

ep = easypost.EasyPostClient(EASYPOST_KEY)

def compare(addr: dict) -> dict:
  old = ep.address.create(**addr, country="US", verify=["delivery"])
  new = requests.post(
      "https://api.revaddress.com/api/address/validate",
      headers={"X-API-Key": REVADDRESS_KEY},
      json={
          "streetAddress": addr["street1"],
          "secondaryAddress": addr.get("street2", ""),
          "city": addr["city"],
          "state": addr["state"],
          "ZIPCode": addr["zip"],
      },
      timeout=8,
  ).json()

  return {
      "input": addr,
      "easypost_ok": old.verifications.delivery.success,
      "classification": new["resolution"]["classification"],
      "zip4_new": new["address"]["ZIPPlus4"],
      # The row worth reading: EasyPost said no, USPS says a unit would fix it.
      "recoverable": (
          not old.verifications.delivery.success
          and new["resolution"]["nextAction"] == "collect_secondary"
      ),
  }

Read the recoverable column first. Those are orders that a boolean sent down a failure branch and a unit-number prompt would have saved. On a consumer file with apartments in it, that column is usually the migration’s business case, and it is measurable before you change a line of checkout code.

Then read the ZIP+4 disagreements. Both services take the +4 from USPS, so a mismatch almost always means one of them standardized the street line differently, and the abbreviation behavior above is the usual reason.

Path 2: the open-source usps-v3 SDK. This talks to USPS directly with your own USPS Developer Portal consumer key and secret. No RevAddress key is involved.

from usps_v3 import USPSClient

client = USPSClient(
  consumer_key="your_key",
  consumer_secret="your_secret"
)

result = client.addresses.validate(
  street_address="1600 Pennsylvania Ave NW",
  city="Washington",
  state="DC",
  zip_code="20500"
)
print(result["address"]["ZIPPlus4"])  # "0005"

Install with pip install usps-v3, npm install usps-v3, or composer require revaddress/usps-v3-php. All three are MIT licensed and manage the OAuth token lifecycle for you, including proactive refresh before expiry.

Rates and labels

The label call replaces both halves of EasyPost’s create-then-buy:

Quote a rate, then create the labelbash
curl -X POST "https://api.revaddress.com/api/rates" \
-H "X-API-Key: rv_live_your_key" \
-H "Content-Type: application/json" \
-d '{
  "originZIP": "94105",
  "destinationZIP": "20500",
  "weight": 16,
  "mailClass": "USPS_GROUND_ADVANTAGE"
}'

Labels run on your own USPS license, and postage is drawn from your own Enterprise Payment Account. We do not create labels on our credentials for you, because postage has to be paid by the entity that owes it. That is the same condition EasyPost states as “postage not included” on Free Access; it is normal, and it is worth knowing before you plan a cutover.

Connecting your USPS credentials

BYOK means your USPS Developer Portal credentials run through our infrastructure. Calls draw on your USPS license and your rate limit rather than a shared pool, so an increase USPS grants you applies in full. One call turns it on:

Store your USPS credentialsbash
curl -X POST "https://api.revaddress.com/api/byok/credentials" \
-H "X-API-Key: rv_live_your_key" \
-H "Content-Type: application/json" \
-d '{
  "client_id": "your-usps-consumer-key",
  "client_secret": "your-usps-consumer-secret",
  "crid": "12345678",
  "master_mid": "900000001",
  "label_mid": "900000002",
  "epa_account": "1000000001"
}'
Verified (201)json
{
"merchant_id": "stripe_cus_abc123",
"status": "verified",
"has_payment_fields": true,
"verification": { "success": true, "message": "OAuth token obtained successfully" },
"message": "Credentials verified — your USPS tokens are now managed separately."
}

Four things worth knowing about that call:

  • client_id and client_secret are your USPS Developer Portal consumer key and secret. Not Web Tools credentials. Web Tools is retired, and the shutdown timeline covers what replaced it.
  • crid, master_mid, label_mid and epa_account are optional, and only for labels and payments. Validation, rates and tracking need only the key pair.
  • Verification is immediate. A 201 with status: verified means OAuth succeeded. A 200 with pending_verification means the credentials stored but USPS rejected the token exchange, which almost always means a bad key pair.
  • Credentials are AES-GCM encrypted at rest with a per-merchant derived key, and each BYOK merchant gets isolated token management. GET /api/byok/status reports live token health.

BYOK connects on any API key, Free included. There is no surcharge for it, which is worth contrasting with the $20 a month EasyPost charges for BYOCA.

The checklist

  1. Start the USPS license. Signed agreement plus a funded Enterprise Payment Account. Weeks, not hours. Everything else waits on nothing.
  2. Get a free API key at revaddress.com/signup. 1,000 requests a month, no card.
  3. Swap address validation first. It is the highest-volume call and the smallest change. Field aliases mean street1 maps with a shallow rename; street_address, address1, line1, zip and postal_code all resolve.
  4. Move rates and tracking. POST /api/rates and GET /api/tracking/{trackingNumber}.
  5. Connect BYOK once the USPS credentials land, then move labels last. Labels are the only step that genuinely cannot run before the license does.
  6. Watch the rate limit. USPS allows 60 requests an hour per application by default, shared across every endpoint. That ceiling surprises people more than the pricing does; the rate-limit guide covers caching, queue smoothing and requesting an increase.

Run both integrations side by side through step 4 if you can. Address validation and tracking are read-only, so double-writing costs nothing but requests.

What you give up

Worth stating plainly, because a migration guide that only lists wins is a sales page.

Carrier breadth. EasyPost reaches a hundred-plus carriers. This is USPS only. If you ship UPS or FedEx, you are not replacing EasyPost, you are adding a second integration alongside it and moving only the USPS half.

A managed carrier relationship. EasyPost’s Free Access uses its own wallet carrier accounts, so you never sign anything. Here you sign the USPS agreement yourself. That is more control and more paperwork, and for some organizations the paperwork is the blocker.

One vendor for insurance and claims. EasyPost sells shipping insurance at 1 percent of value and runs claims programs. There is no equivalent here.

Sources

EasyPost plan structure and the BYOCA base fee were read from the EasyPost pricing page on August 23, 2026; the per-label rate is no longer published there and the $0.08 figure in the companion cost piece is dated to EasyPost’s February 2026 announcement. The verification field names, the verify parameter behavior, the August 25, 2025 street-abbreviation change and the street1-to-street2 splitting rule were read from EasyPost’s address verification guide and Address API reference on August 25, 2026. RevAddress endpoints, the BYOK request and response shapes, and the SDK constructors come from the API reference and the SDK page.

Start here

Questions

How long does it take to migrate USPS calls off EasyPost?
The code is short. Address validation is a one-call swap and a field rename, and most integrations finish in an afternoon. The USPS license is the long pole: a signed agreement and a funded Enterprise Payment Account take weeks, so start that first and write the code while it clears.
What replaces the EasyPost SDK?
Either the RevAddress REST API with an X-API-Key header, or the open-source usps-v3 SDK for Python, Node.js and PHP. The SDK talks to USPS directly with your own USPS Developer Portal consumer key and secret; the REST API talks to RevAddress with a RevAddress key. They are different paths, not two names for one thing.
How do I map EasyPost calls to USPS v3 endpoints?
Address create with verify becomes POST /api/address/validate. Shipment create plus buy becomes POST /api/labels. Tracker create becomes GET /api/tracking/{trackingNumber}. Shipment lowestRate becomes POST /api/rates.
What replaces verifications.delivery.success in the response?
resolution.classification, paired with resolution.nextAction, and the pair carries more than the boolean did. A USPS-verified answer is deliverable_exact, review_response or not_deliverable; an answer standardized against Census Bureau reference data, which is what a standardization-tier key receives and what any key receives while USPS confirmation is unavailable, is standardized_unverified. Branch on resolution.nextAction rather than on a true or false value: the one that matters is collect_secondary, where the building matched and the unit did not.
Does the validation response still return latitude and longitude?
Not on a USPS-verified answer. EasyPost puts coordinates inside the verification details; a USPS-verified response from POST /api/address/validate carries none, and geocoding is a separate route. A response standardized against Census Bureau reference data does return a coordinates object, so presence is not something to branch on. Anything reading those fields, such as a delivery-zone check or a map pin, should call the geocoding route rather than depend on the validation payload.
What is BYOK and how do I turn it on?
Bring Your Own Keys: your USPS Developer Portal credentials run through RevAddress infrastructure so calls draw on your USPS license and your rate limit. POST your client_id and client_secret to /api/byok/credentials. Credentials are AES-GCM encrypted at rest with a per-merchant key, and any API key can connect, including Free.
Do I need a USPS license to create labels?
Yes. Labels run on your own USPS license, and the BYOK call takes your CRID, mailer IDs and Enterprise Payment Account alongside the credentials. Postage is drawn from your own payment account, because postage is paid by the entity that owes it.