USPS Node.js SDK Quickstart: TypeScript Client for the v3 API
usps-v3 is an MIT-licensed TypeScript client for the USPS v3 REST API. Zero runtime dependencies, built on native fetch, with type declarations bundled. This walks installation, authentication, and the six operations most Node integrations need.
Settle one thing before you write code: the SDK runs on your USPS credentials. Since August 1, 2026 the Addresses API sits behind a signed license agreement and bills on a consumption curve, so the license and the invoice are yours. The pricing guide has the published fee table and the signature sequence.
Prerequisites
- Node.js 18 or newer. The SDK uses the built-in
fetch, so there is no polyfill to install. - Zero dependencies. Nothing else lands in your lockfile.
- A USPS application. Register at
developer.usps.comto get a Client ID and Client Secret. - An executed Addresses API license for address validation. The CRID and MID guide walks the enrollment screens.
npm install usps-v3Authentication
OAuth 2.0 client credentials, 8-hour tokens, all of it handled inside the client.
import { USPSClient } from 'usps-v3';
const client = new USPSClient({
clientId: process.env.USPS_CLIENT_ID,
clientSecret: process.env.USPS_CLIENT_SECRET,
});Both values fall back to those environment variables if you omit them. The full constructor surface:
const client = new USPSClient({
clientId: 'your-client-id', // or USPS_CLIENT_ID
clientSecret: 'your-client-secret', // or USPS_CLIENT_SECRET
baseUrl: 'https://apis.usps.com', // default
timeout: 30000, // ms, default
// Label creation only:
crid: '...', // or USPS_CRID
masterMid: '...', // or USPS_MASTER_MID
labelMid: '...', // or USPS_LABEL_MID
epaAccount: '...', // or USPS_EPA_ACCOUNT
});baseUrl is how you reach the testing host. USPS runs one at apis-tem.usps.com with separate credentials; point baseUrl there and keep the two credential sets apart. Crossing them is the most common cause of a 403 that looks like an entitlement problem. The OAuth guide covers the rest of the failure modes.
Token handling is automatic, and the client exposes it:
// Inspect without forcing a call
console.log(client.tokenStatus);
// { hasOAuthToken: true, oauthExpiresIn: 27000, ... }
// Force a refresh
await client.refreshTokens();
// Release resources on shutdown
client.close();Tokens are cached in memory and refreshed 30 minutes before expiry. Instantiate the client once, at startup, not per request: every token fetch counts against your hourly quota, and a per-request client re-authenticates constantly.
Address validation
const result = await client.addresses.validate({
streetAddress: '1600 Pennsylvania Ave NW',
city: 'Washington',
state: 'DC',
ZIPCode: '20500',
});
console.log(result.address);
// { streetAddress: '1600 PENNSYLVANIA AVE NW', city: 'WASHINGTON',
// state: 'DC', ZIPCode: '20500' }ZIPCode is capitalised, not zipCode. The SDK mirrors the USPS field names rather than normalizing them to camelCase, and a lowercase zipCode is silently ignored rather than rejected, which turns into a validation that quietly ran without the ZIP you thought you sent.
The parameter rule catches people. streetAddress is always required, and USPS wants either city and state, or ZIPCode, or all three. A street line alone throws a ValidationError.
City and state from a ZIP is its own call:
const info = await client.addresses.cityState('10001');Reading DPV
DPVConfirmation is what the call is for. The USPS specification defines four values:
| Code | Meaning | What to do |
|---|---|---|
Y |
Confirmed for the primary number and, if present, the secondary number | Accept it |
D |
Confirmed for the primary number only, secondary information missing | Collect the unit number |
S |
Confirmed for the primary number only, secondary information present but not confirmed | Offer a correction, do not re-prompt from empty |
N |
Neither primary nor secondary confirmed | Reject or route to manual review |
Y is not a delivery guarantee. USPS states in its own specification that a Y does not necessarily imply USPS delivers to that address, and carrierRoute values such as R777 and R779 can mean the recipient collects mail elsewhere. D and S are also different problems: one is a field you never collected, the other is a field the customer filled in wrong. The address validation quickstart covers the full attribute set.
Package tracking
const tracking = await client.tracking.track('9400111899223033005282');
console.log(tracking.statusCategory); // 'Delivered', 'In Transit', ...The tracking number is a plain string argument. Do not cache the response: tracking goes stale within minutes. Poll at a sane interval or register for USPS tracking notifications.
Rates, estimates, and drop-off
// Domestic
const rates = await client.prices.domestic({
originZIPCode: '10001',
destinationZIPCode: '90210',
weight: 2.5,
});
// Outbound international
const intlRates = await client.prices.international({
originZIPCode: '10001',
destinationCountryCode: 'GB',
weight: 3.0,
});const estimates = await client.standards.estimates('10001', '90210');
// [{ mailClass: 'PRIORITY_MAIL', daysToDelivery: 2 }, ...]
const locations = await client.locations.dropoff({
destinationZIP: '20500',
mailClass: 'PRIORITY_MAIL',
});Note the shape difference: standards.estimates takes two positional strings, while locations.dropoff takes an object with destinationZIP. They are not consistent with each other, and reading the signature beats guessing.
Label creation
Labels need payment authorization on top of OAuth, which means the four enrollment identifiers on the constructor.
const client = new USPSClient({
clientId: process.env.USPS_CLIENT_ID,
clientSecret: process.env.USPS_CLIENT_SECRET,
crid: process.env.USPS_CRID,
masterMid: process.env.USPS_MASTER_MID,
labelMid: process.env.USPS_LABEL_MID,
epaAccount: process.env.USPS_EPA_ACCOUNT,
});
const label = await client.labels.create({
fromAddress: {
streetAddress: '228 Park Ave S',
city: 'New York',
state: 'NY',
ZIPCode: '10003',
},
toAddress: {
streetAddress: '1600 Pennsylvania Ave NW',
city: 'Washington',
state: 'DC',
ZIPCode: '20500',
},
mailClass: 'PRIORITY_MAIL',
weight: 2.0,
});
console.log(label.trackingNumber);The CRID, MIDs, and EPA account come from COP claims linking, a manual enrollment step with no API path. Postage is drawn from your own payment account, so label creation is always on your own USPS license.
Error handling
Five error classes, all exported from the package root.
import {
USPSClient,
ValidationError,
AuthError,
RateLimitError,
} from 'usps-v3';
try {
await client.addresses.validate({ streetAddress: '' });
} catch (err) {
if (err instanceof ValidationError) {
console.log('Bad field: ' + err.field);
} else if (err instanceof RateLimitError) {
console.log('Retry after ' + err.retryAfter + 's');
} else if (err instanceof AuthError) {
console.log('Check credentials');
} else {
throw err;
}
}| Error class | Raised when |
|---|---|
ValidationError |
Invalid input parameters (carries field) |
AuthError |
OAuth or payment authorization failure |
RateLimitError |
429 from USPS (carries retryAfter) |
APIError |
USPS returned an error response |
NetworkError |
Connection timeout or DNS failure |
Import them from 'usps-v3', not a subpath. The package declares a single export entry point, so 'usps-v3/errors' and 'usps-v3/types' do not resolve. Types come through the root import as well; there is no @types/ package to add.
RateLimitError is the one to design around. USPS defaults to 60 requests per hour per application, shared across every endpoint your app calls, and the SDK surfaces the 429 rather than throttling for you.
import { RateLimitError, AuthError, ValidationError } from 'usps-v3';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function validateWithRetry(client, address, attempts = 3) {
for (let attempt = 0; attempt < attempts; attempt++) {
try {
return await client.addresses.validate(address);
} catch (err) {
if (err instanceof RateLimitError) {
// Honour USPS's own number when it sends one.
await sleep((err.retryAfter ?? 2 ** attempt) * 1000);
continue;
}
// Neither of these clears on a retry.
if (err instanceof AuthError || err instanceof ValidationError) throw err;
throw err;
}
}
throw new Error('USPS validation failed after retries');
}Retry the 429. Never retry a ValidationError, which returns identically forever, or an AuthError, which needs a credential fix. The rate limit guide covers the caching and queueing that keep you under the ceiling to begin with.
Express route
One client at startup, reused across every request.
import express from 'express';
import { USPSClient, ValidationError, RateLimitError } from 'usps-v3';
const app = express();
app.use(express.json());
// Once, at startup. The token is cached and shared across requests.
const usps = new USPSClient({
clientId: process.env.USPS_CLIENT_ID,
clientSecret: process.env.USPS_CLIENT_SECRET,
});
app.post('/api/validate-address', async (req, res) => {
try {
const result = await usps.addresses.validate({
streetAddress: req.body.street,
city: req.body.city,
state: req.body.state,
ZIPCode: req.body.zip,
});
res.json({ address: result.address });
} catch (err) {
if (err instanceof ValidationError) {
return res.status(400).json({ error: err.message, field: err.field });
}
if (err instanceof RateLimitError) {
res.set('Retry-After', String(err.retryAfter ?? 60));
return res.status(429).json({ error: 'usps_rate_limited' });
}
return res.status(502).json({ error: 'usps_upstream_error' });
}
});
app.listen(3000);
process.on('SIGTERM', () => usps.close());Three things that matter in production and are easy to skip. Map RateLimitError to a real 429 with a Retry-After header so your own callers can back off correctly instead of hammering you. Return 502 rather than 500 for an upstream USPS failure, because the distinction is what tells your on-call whether the bug is yours. And call client.close() on shutdown.
Migrating from usps-webtools
usps-webtools and usps-webtools-promise target USPS Web Tools, which stopped answering on January 25, 2026.
| usps-webtools | usps-v3 |
|---|---|
verify(address, callback) |
client.addresses.validate(address) |
zipCodeLookup(address, callback) |
client.addresses.cityState(zip) |
track(trackingNumber, callback) |
client.tracking.track(trackingNumber) |
rates(params, callback) |
client.prices.domestic(params) |
| USERID string auth | OAuth 2.0, handled by the client |
| XML responses | JSON responses |
Field names moved too: Address2 was the street line and becomes streetAddress; Address1 was the unit and becomes secondaryAddress. Callbacks become promises throughout. The migration checklist runs the whole move phase by phase, including the license step that did not exist under Web Tools.
Running it through RevAddress instead
If you would rather not hold USPS credentials in your own process, the managed route is one header.
import { USPSClient } from 'usps-v3';
const client = new USPSClient({
clientId: process.env.USPS_CLIENT_ID,
clientSecret: process.env.USPS_CLIENT_SECRET,
});
const result = await client.addresses.validate({
streetAddress: '1600 Pennsylvania Ave NW',
city: 'Washington',
state: 'DC',
ZIPCode: '20500',
});const params = new URLSearchParams({
streetAddress: '1600 Pennsylvania Ave NW',
city: 'Washington',
state: 'DC',
ZIPCode: '20500',
});
const res = await fetch(
'https://api.revaddress.com/api/address/validate?' + params,
{ headers: { 'X-API-Key': 'rv_live_your_key_here' } },
);
const result = await res.json();What the subscription buys is the infrastructure, not the USPS data. Standardization, geocoding, and address extract run on US Census Bureau reference data and are included in every plan, including Free at 1,000 requests a month. DPV verification, rates, service standards, tracking, and labels run on your USPS license (BYOK), so you sign the agreement, USPS bills your Enterprise Payment Account, and the managed layer handles the credential vault, the token lifecycle, caching, retries, and rate-limit budgeting.
Start here
- Get a free API key, 1,000 requests a month, no credit card, no USPS license needed for standardization and geocoding.
- Python SDK quickstart is the same walkthrough with the Python client.
- Address validation, start to finish covers the raw HTTP call and every response field.
- Rate limit strategies covers caching, queueing, and the increase request.
- usps-v3 on npm and the source on GitHub, MIT licensed.
Questions
- How do I install the USPS Node.js SDK?
- Run npm install usps-v3. The package requires Node.js 18 or newer, ships zero runtime dependencies, and uses the built-in fetch. TypeScript declarations are bundled, so no @types package is needed.
- Where do the error classes come from in usps-v3?
- The package root. Import ValidationError, AuthError, RateLimitError, APIError, and NetworkError directly from 'usps-v3' alongside USPSClient. The package exports map defines a single entry point, so subpath imports do not resolve.
- How do I point the Node SDK at the USPS testing host?
- Pass baseUrl to the constructor. It defaults to https://apis.usps.com; set it to the testing host to run against USPS test credentials. Testing and production credentials are not interchangeable.
- Does the SDK manage OAuth tokens?
- Yes. Tokens are cached in memory and refreshed automatically 30 minutes before expiry. You can inspect client.tokenStatus, force a refresh with client.refreshTokens(), and release resources with client.close().
- Do I need a USPS license to use the Node SDK?
- For address validation, yes. Since August 1, 2026 the USPS Addresses API requires a signed license agreement and bills on a monthly consumption curve against an Enterprise Payment Account. The SDK runs on your own credentials, so the license and the invoice are yours.
Read next
USPS Python SDK Quickstart: Validate, Track, and Rate with usps-v3
Install usps-v3, authenticate once, and call address validation, tracking, and rates from Python. Working code, real return shapes, typed errors.
11 min readQuickstartUSPS v3 Address Validation: One Request, Start to Finish
The USPS v3 address call in curl, Python and Node: OAuth, the required parameters, every DPV code, and the license USPS has required since August 1, 2026.
9 min readTroubleshootingUSPS v3 OAuth Troubleshooting: Every Error and How to Fix It
401, 403, and 429 from USPS v3, with the exact fix for each, including the Addresses API license that has gated address calls since August 1, 2026.
9 min read