Skip to main content
Technical Preview

Overview

EasyPost is a multi-carrier shipping API (USPS, UPS, FedEx, DHL, DPD, CTT Express and 100+ more): it verifies addresses, compares rates, buys labels, schedules pickups, prints scan forms (manifests) and tracks packages, and it pushes tracking updates as webhook Events. This connector lets the broker ship what the plant produces: when a packing line finishes a pallet or carton, an MQTT message creates the shipment, the cheapest allowed rate is bought, the label goes to the line’s ZPL printer, and tracking updates come back on MQTT under logistics/shipments/<id>. The template (EasypostRestApi.lot) is one client connector, EasyPostApi, on https://api.easypost.com, with 53 events: 25 GET, 27 POST and 1 PATCH (no PUT or DELETE). 29 are reads, 19 are writes and 5 are purchases that spend money with a production key (BuyShipment, InsureShipment, BuyOrder, BuyBatch, BuyPickup). CreateShipment and CreateOrder also purchase when you add service and carrier_accounts (One-Call Buy). The optional companion (fleet_easypost_webhook.lot) is a server connector, EasyPostWebhookReceiver, that receives EasyPost webhook Events and publishes them on MQTT.
Purchases cost money. With a production key (EZAK...), every event marked Purchase (and a One-Call Buy) charges the EasyPost Wallet: postage, insurance or a pickup fee. Start with a test key (EZTK...): test mode buys nothing and contacts no carrier. Restrict who can publish to fleet/easypost/+/buy, fleet/easypost/shipment/insure and fleet/easypost/shipment/create with broker ACLs.

Events

Every reply is published on the event topic plus /response. Objects carry object and mode (test or production). Lists return {"<objects>": [...], "has_more"}, newest first; page_size is at most 100 (most default to 20), and the next page uses before_id = the last id (or start_datetime / end_datetime). Errors return {"error": {"code", "message", "errors"}} with a 4xx or 5xx status.

Webhook receiver (server connector)

EasyPost sends every Event of a mode to every webhook of that mode; there is no per-type filter. Create one webhook with the production key (https://<public host>/easypost/webhooks) and, for testing, one with the test key (https://<public host>/easypost/webhooks/test), with CreateWebhook or in the Dashboard (Webhooks and Events). Route by the Event’s description (tracker.updated, batch.updated, …) in an action. Delivery is at least once, so make handlers idempotent (Event id). EasyPost needs HTTPS with a publicly trusted certificate: put a reverse proxy or tunnel in front of SERVER_PORT 9014. Answer 2xx; webhooks that keep failing are disabled, and UpdateWebhook re-enables them.
With a webhook_secret, EasyPost signs each body in the X-Hmac-Signature header (HMAC-SHA256); with https://user:pass@host/... it sends Basic auth instead. The receiver connector checks neither. Verify the signature in the reverse proxy, or keep the URL secret and restrict source addresses. Deploying the receiver opens an HTTP port on the broker.

Prerequisites

  • An EasyPost account. EasyPost issues API keys only after the Wallet is set up and a ship-from address is added.
  • An API key from Account Settings > API Keys: a test key (EZTK...) to start, a production key (EZAK...) for real labels.
  • At least one carrier account for the key (ListCarrierAccounts), such as the account’s default USPS account or your own UPS, FedEx, DHL Express or CTT Express account.
  • Address and parcel data: parcel length, width, height in inches and weight in ounces; customs info for international shipments.
  • Outbound HTTPS from the broker to api.easypost.com; for webhooks, a public HTTPS URL that forwards to port 9014.
  • Optional: Python scripts enabled on the broker for the EasyPostHelper script (auto-buy example).

Authentication

An EasyPost API key acts on the whole account, including the Wallet. Store it in Hub secrets, read it with GET SECRET, never put it in a connector file, and do not retain topics that carry it.
EasyPost uses HTTP Basic auth: the API key is the username and the password is empty, so the header is Authorization: Basic <base64 of "<api key>:"> (note the trailing colon). Every event sends Authorization: Basic {basic}, where basic comes from the payload. Store the encoded value once as the secret EASYPOST_BASIC:
Keys are per mode: a test key creates test objects and test webhooks, a production key real ones. A request without Authorization gets 401 UNAUTHORIZED; a wrong, disabled or revoked key gets 403 APIKEY.INACTIVE.

Payload fields

RerateShipment, RefundShipment, BuyBatch, CreateBatchScanForm and CancelPickup send no body. Pass id values you got from EasyPost, never raw user input, because they go into the URL path.

Examples

Create a shipment for a finished pallet’s carton and get rates (no charge):
fleet/easypost/shipment/create
Buy the label for one rate (purchase with a production key):
fleet/easypost/shipment/buy
Convert the bought label to ZPL for a Zebra printer on the line:
fleet/easypost/label/convert
Track an inbound package bought elsewhere (with a test key, EZ1000000001-style codes simulate statuses and send test webhooks):
fleet/easypost/tracker/create
End of shift: batch the day’s shipments, then buy them, print one merged label file and a scan form:
fleet/easypost/batch/create
Register the webhook receiver:
fleet/easypost/webhook/create

Example actions

These actions create a shipment when a pallet completes, buy the cheapest allowed rate, send the label to the line printer (ZPL), turn tracker webhooks into logistics/shipments/<shipment id> messages (or logistics/trackers/<tracking code> for trackers without a shipment), and track inbound packages. Replace the <...> placeholders.
EasyPostBuyCheapestRate buys postage for every shipment created on fleet/easypost/shipment/create. Run it with a test key first. With a production key, every pallet costs money.
EasyPostBuyCheapestRate needs the Python script EasyPostHelper (easypost_helper.py), because LoT cannot loop over the rates array. Deploy it before the actions:

Connector template

Webhook receiver template

Limits and behaviour

  • Units: parcel dimensions in inches, weight in ounces; money as decimal strings in USD (insurance, amount).
  • Pallets: EasyPost is a parcel API. A whole pallet over the carriers’ parcel weight or size limits needs LTL freight, which this API does not cover. Ship the pallet’s cartons as parcels, one CreateShipment each or one multi-parcel CreateOrder (only some carriers support orders).
  • Labels are PNG by default; ConvertShipmentLabel converts a bought label to ZPL, EPL2 or PDF, or set options.label_format when you create the shipment. CreateBatchLabel builds one merged file for a bought batch.
  • Asynchronous objects: batch buy and batch labels, scan forms and refunds finish later; follow them with GetBatch / GetScanForm or the batch.updated, scan_form.updated and refund.successful webhook Events.
  • Refunds: RefundShipment and CreateRefund work on unused labels; USPS refunds can take up to 15 days (refund.successful).
  • Trackers are created automatically when a label is bought. Standalone CreateTracker calls for labels bought elsewhere may be billed in production; check EasyPost pricing.
  • Test mode: a test key returns test objects (mode: test), buys nothing and contacts no carrier; test tracking codes simulate tracker statuses and send test-mode webhook Events.
  • Webhooks: every Event goes to every webhook of the mode, at least once. ListEvents and ListEventPayloads help to catch up after downtime.
  • Errors: {"error": {"code", "message", "errors"}}. 401 UNAUTHORIZED means no credentials, 403 APIKEY.INACTIVE a wrong or disabled key, 404 NOT_FOUND an unknown id or path, 422 a validation error (for example SHIPMENT.POSTAGE.EXISTS when buying twice). Rate-limited calls answer 429.

Notes on the EasyPost docs

  • The rendered cURL tabs on several docs.easypost.com pages show a different call than their heading: POST /shipments/:id/buy shows POST /v2/shipments (create), GET /shipments/:id/label shows a plain retrieve, POST /trackers/batch shows POST /v2/trackers, POST /orders/:id/buy shows POST /v2/orders, POST /pickups/:id/buy and /cancel show POST /v2/pickups, GET /events/:event_id/payloads shows the single-payload URL, and the EndShipper page labels its create call POST /shipments/:id/buy. The template follows the headings, the official EasyPost/examples cURL files and easypost-python v10.7.0, and every method + path was confirmed live (401 without auth).
  • Some docs JSON bodies are not valid JSON (trailing commas in the webhook create / update examples, unquoted [UPS] in the SmartRate deliver_by / deliver_on examples); the template examples are valid JSON.
  • A fake or revoked key gets 403 APIKEY.INACTIVE, not 401; only a request without Authorization gets 401 UNAUTHORIZED. Without auth, an unknown method or path answers 404 NOT_FOUND, so the unauthenticated probe tells existing calls apart.
  • The rate estimate API is POST /beta/rates, outside /v2, so the connector base is https://api.easypost.com and each endpoint carries /v2 or /beta.
  • The docs describe HTTP Basic (key as username, empty password); the official client libraries send Authorization: Bearer <key>. The auth layer parses both (same 403 for a fake key); the template uses Basic as documented.
  • GetRate (GET /v2/rates/{id}) has no docs page; it is the official client’s rate.retrieve.
  • No official OpenAPI spec is published (llms.txt points to Markdown twins and doc-index.json); request parameter tables on the docs pages are rendered client-side and missing from the Markdown twins, so query parameter names come from cURL examples, the pagination page and the official client.

Status

Blocked (beta). Smoke checks pass (565/565 static 443/443, mock 57/57, controls 12/12, public 53/53, live 0/0), but nothing has run against a real EasyPost account. Unblock: An EasyPost account (free sign-up) with an EasyPost Wallet set up and a ship-from address added (EasyPost requires both before it issues API keys); its TEST API key (EZTK…, Dashboard > Account Settings > API Keys) stored as broker secret EASYPOST_BASIC = base64 of ‘<test key>:’ (and exported as EASYPOST_API_KEY for the smoke script), never a production key for the first run; at least one carrier account available to that key (ListCarrierAccounts must list it, e.g. the account’s default USPS account or a connected UPS / FedEx / DHL Express / CTT Express account) so CreateShipment returns rates; for the webhook path a public HTTPS URL with a publicly trusted certificate (reverse proxy or tunnel) forwarding to broker port 9014, registered as a test-mode webhook (CreateWebhook with the test key, url https://<host>/easypost/webhooks/test, optional webhook_secret EASYPOST_WEBHOOK_SECRET verified in the proxy); Python scripts enabled on the broker for EasyPostHelper (only for the auto-buy example). Then run EASYPOST_API_KEY=EZTK… python3 smoke_easypost.py (GetUser, ListCarrierAccounts, CreateAddress, CreateParcel, CreateShipment, BuyShipment and ConvertShipmentLabel ZPL and RefundShipment in test mode (free), CreateTracker EZ1000000001, GetTracker, RetrieveTrackerBatch, ListTrackers, ListEvents, ListWebhooks, GetCarrierMetadata), plus a broker MQTT round trip with the test key (pallet-complete -> CreateShipment -> EasyPostBuyCheapestRate -> BuyShipment -> EasyPostLabelToPrinter -> ConvertShipmentLabel; CreateTracker EZ1000000001 -> tracker.updated on fleet/easypost/webhook-test/received -> EasyPostTrackerToShipment subscribed to that test topic -> logistics/…). A production key (EZAK…) comes only after that, set by Hugo, because every PURCHASE event and One-Call Buy spends wallet money.
Last modified on October 21, 2026