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 underlogistics/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
| Event | Topic | Request | Kind | What it does |
|---|---|---|---|---|
GetUser | fleet/easypost/user/get | GET /v2/users | Read | The authenticated user (id, name, balance, recharge settings, child users) |
ListCarrierAccounts | fleet/easypost/carrier-account/list | GET /v2/carrier_accounts | Read | Carrier accounts on the EasyPost account (ids ca_… for carrier_accounts in shipments) |
ListCarrierTypes | fleet/easypost/carrier-type/list | GET /v2/carrier_types | Read | Carrier types that can be added and their credential fields |
GetCarrierMetadata | fleet/easypost/carrier-metadata/get | GET /v2/metadata/carriers?{query} | Read | Carrier metadata: service levels, predefined packages, shipment options, supported features |
CreateAddress | fleet/easypost/address/create | POST /v2/addresses | Write | Create an address; verify / verify_strict run address verification (result in verifications) |
CreateAndVerifyAddress | fleet/easypost/address/create-verify | POST /v2/addresses/create_and_verify | Write | Create and strictly verify an address in one call; fails with ADDRESS.VERIFY.FAILURE when it cannot be verified |
VerifyAddress | fleet/easypost/address/verify | GET /v2/addresses/{id}/verify | Read | Verify an existing address |
GetAddress | fleet/easypost/address/get | GET /v2/addresses/{id} | Read | One address by id |
CreateParcel | fleet/easypost/parcel/create | POST /v2/parcels | Write | Create a parcel (inches and ounces, or a predefined_package) |
CreateCustomsInfo | fleet/easypost/customs-info/create | POST /v2/customs_infos | Write | Customs declaration (with customs items) for international shipments |
CreateShipment | fleet/easypost/shipment/create | POST /v2/shipments | Write (One-Call Buy can purchase) | Create a shipment and get its rates (no charge). Adding service + carrier_accounts makes it a One-Call Buy that purchases postage |
GetShipment | fleet/easypost/shipment/get | GET /v2/shipments/{id} | Read | One shipment (rates, selected_rate, postage_label, tracker, fees) |
ListShipments | fleet/easypost/shipment/list | GET /v2/shipments?{query} | Read | Shipments, newest first; page_size (max 100), before_id / after_id, start_datetime / end_datetime; has_more |
BuyShipment | fleet/easypost/shipment/buy | POST /v2/shipments/{id}/buy | Purchase | Buy the label for one rate (adds tracking_code, postage_label, tracker); optional insurance (USD string) and end_shipper_id |
ConvertShipmentLabel | fleet/easypost/label/convert | GET /v2/shipments/{id}/label?{query} | Read | Convert a bought PNG label to ZPL, EPL2 or PDF (label_zpl_url / label_epl2_url / label_pdf_url) |
RerateShipment | fleet/easypost/rate/rerate | POST /v2/shipments/{id}/rerate | Read | Regenerate the rates of an unbought shipment (no body, no charge) |
GetShipmentSmartRates | fleet/easypost/smartrate/get | GET /v2/shipments/{id}/smartrate | Read | SmartRate: rates with time-in-transit percentiles for the shipment |
InsureShipment | fleet/easypost/shipment/insure | POST /v2/shipments/{id}/insure | Purchase | Insure a bought shipment for amount (USD); charges the insurance fee |
RefundShipment | fleet/easypost/shipment/refund | POST /v2/shipments/{id}/refund | Write | Request a refund of an unused label (refund_status submitted; USPS refunds take up to 15 days) |
GetRate | fleet/easypost/rate/get | GET /v2/rates/{id} | Read | One rate by id |
EstimateRates | fleet/easypost/rate/estimate | POST /beta/rates | Read | Stateless rates for addresses and a parcel without creating a shipment (beta API under /beta) |
SmartRateDeliverBy | fleet/easypost/smartrate/deliver-by | POST /v2/smartrate/deliver_by | Read | Delivery date predictions by carrier and service for a ZIP pair and ship date |
CreateTracker | fleet/easypost/tracker/create | POST /v2/trackers | Write | Track a package not bought through EasyPost (EZ… test codes simulate statuses with a test key); trackers may be billed in production |
GetTracker | fleet/easypost/tracker/get | GET /v2/trackers/{id} | Read | One tracker (status, est_delivery_date, tracking_details, carrier_detail) |
ListTrackers | fleet/easypost/tracker/list | GET /v2/trackers?{query} | Read | Trackers, newest first; filter by tracking_code and carrier |
RetrieveTrackerBatch | fleet/easypost/tracker/batch | POST /v2/trackers/batch | Read | Many trackers by tracking_codes in one call (POST, read only) |
CreateOrder | fleet/easypost/order/create | POST /v2/orders | Write (One-Call Buy can purchase) | Multi-parcel order (one shipment per carton) with rates; only some carriers (FedEx, UPS, DHLExpress, …); service + carrier_accounts make it a One-Call Buy |
BuyOrder | fleet/easypost/order/buy | POST /v2/orders/{id}/buy | Purchase | Buy every shipment of an order for one carrier + service |
GetOrder | fleet/easypost/order/get | GET /v2/orders/{id} | Read | One order with its shipments and rates |
CreateBatch | fleet/easypost/batch/create | POST /v2/batches | Write | Group shipments into a batch (end-of-shift label run) |
GetBatch | fleet/easypost/batch/get | GET /v2/batches/{id} | Read | One batch (state, status counts, label_url, scan_form) |
ListBatches | fleet/easypost/batch/list | GET /v2/batches?{query} | Read | Batches, newest first |
AddBatchShipments | fleet/easypost/batch-shipment/add | POST /v2/batches/{id}/add_shipments | Write | Add shipments to a batch |
RemoveBatchShipments | fleet/easypost/batch-shipment/remove | POST /v2/batches/{id}/remove_shipments | Write | Remove shipments from a batch |
BuyBatch | fleet/easypost/batch/buy | POST /v2/batches/{id}/buy | Purchase | Buy every shipment in a batch (asynchronous; batch.updated events; no body) |
CreateBatchLabel | fleet/easypost/batch-label/create | POST /v2/batches/{id}/label | Write | One merged label file (PDF, EPL2 or ZPL) for a bought batch (asynchronous; label_url) |
CreateBatchScanForm | fleet/easypost/batch-scan-form/create | POST /v2/batches/{id}/scan_form | Write | ScanForm (manifest) for a bought batch (no body) |
CreateScanForm | fleet/easypost/scan-form/create | POST /v2/scan_forms | Write | ScanForm (manifest) for bought shipments of one carrier and from address, for the driver to scan at pickup |
GetScanForm | fleet/easypost/scan-form/get | GET /v2/scan_forms/{id} | Read | One ScanForm (status, form_url, tracking_codes) |
ListScanForms | fleet/easypost/scan-form/list | GET /v2/scan_forms?{query} | Read | ScanForms, newest first |
CreatePickup | fleet/easypost/pickup/create | POST /v2/pickups | Write | Request a carrier pickup window for a bought shipment (or batch); reply carries pickup_rates |
BuyPickup | fleet/easypost/pickup/buy | POST /v2/pickups/{id}/buy | Purchase | Buy one pickup rate by carrier + service (not by id); confirmation in the reply |
CancelPickup | fleet/easypost/pickup/cancel | POST /v2/pickups/{id}/cancel | Write | Cancel a scheduled pickup (no body) |
GetPickup | fleet/easypost/pickup/get | GET /v2/pickups/{id} | Read | One pickup (status, confirmation, pickup_rates) |
ListPickups | fleet/easypost/pickup/list | GET /v2/pickups?{query} | Read | Pickups, newest first |
CreateRefund | fleet/easypost/refund/create | POST /v2/refunds | Write | Refund labels by carrier + tracking codes |
ListEvents | fleet/easypost/event/list | GET /v2/events?{query} | Read | Events (tracker.updated, batch.updated, scan_form.updated, refund.successful, …) for webhook catch-up; result is not included |
GetEvent | fleet/easypost/event/get | GET /v2/events/{id} | Read | One event (description, status, pending_urls, completed_urls) |
ListEventPayloads | fleet/easypost/event-payload/list | GET /v2/events/{id}/payloads | Read | Webhook delivery attempts of one event (request body, response code, URL) |
CreateWebhook | fleet/easypost/webhook/create | POST /v2/webhooks | Write | Register a webhook URL for this key’s mode (a test key registers a test-mode webhook); every event is sent to every webhook |
ListWebhooks | fleet/easypost/webhook/list | GET /v2/webhooks | Read | Webhooks of this mode (url, disabled_at) |
GetWebhook | fleet/easypost/webhook/get | GET /v2/webhooks/{id} | Read | One webhook |
UpdateWebhook | fleet/easypost/webhook/update | PATCH /v2/webhooks/{id} | Write | Re-enable a disabled webhook and / or replace webhook_secret and custom_headers (custom_headers is replaced as a whole) |
/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)
| Event | Receives | Publishes on | EasyPost events |
|---|---|---|---|
WebhookEvents | POST /easypost/webhooks | fleet/easypost/webhook/received | production-mode events: tracker.created / tracker.updated, batch.created / batch.updated, scan_form.created / scan_form.updated, refund.successful, insurance., report., payment., shipment.invoice., claim.* |
TestWebhookEvents | POST /easypost/webhooks/test | fleet/easypost/webhook-test/received | test-mode events (webhook created with a test API key; EZ… test tracking codes trigger tracker events) |
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,heightin inches andweightin ounces; customs info for international shipments. - Outbound HTTPS from the broker to
api.easypost.com; for webhooks, a public HTTPS URL that forwards to port9014. - Optional: Python scripts enabled on the broker for the
EasyPostHelperscript (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.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:
printf '%s:' "$EASYPOST_API_KEY" | base64
Authorization gets 401 UNAUTHORIZED; a wrong, disabled or revoked key gets 403 APIKEY.INACTIVE.
Payload fields
| Field | Used by | Meaning |
|---|---|---|
basic | every event | base64 of <api key>: (Hub secret EASYPOST_BASIC) |
id | events with {id} in the path | EasyPost object id (shp_..., rate_..., adr_..., trk_..., batch_..., order_..., pickup_..., sf_..., evt_..., hook_...). Placed in the path |
query | list events, GetCarrierMetadata, ConvertShipmentLabel | URL query string without ?, e.g. page_size=20&before_id=shp_...; URL-encode values yourself |
body | POST / PATCH events with a body | JSON request body with EasyPost’s wrapper key where it has one ({"shipment": {...}}, {"tracker": {...}}, {"address": {...}}, …) |
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
{
"basic": "<base64 of '<api key>:' (Hub secret EASYPOST_BASIC)>",
"body": {
"shipment": {
"reference": "PALLET-2026-10-06-0042",
"to_address": {
"name": "Dr. Steve Brule",
"street1": "179 N Harbor Dr",
"city": "Redondo Beach",
"state": "CA",
"zip": "90277",
"country": "US",
"phone": "8573875756",
"email": "dr_steve_brule@gmail.com"
},
"from_address": {
"name": "EasyPost",
"street1": "417 Montgomery Street",
"street2": "5th Floor",
"city": "San Francisco",
"state": "CA",
"zip": "94104",
"country": "US",
"phone": "4153334445",
"email": "support@easypost.com"
},
"parcel": {
"length": 20.2,
"width": 10.9,
"height": 5,
"weight": 65.9
},
"carrier_accounts": [
"<ca_... from ListCarrierAccounts>"
]
}
}
}
fleet/easypost/shipment/buy
{
"basic": "<base64 of '<api key>:' (Hub secret EASYPOST_BASIC)>",
"id": "shp_...",
"body": {
"rate": {
"id": "rate_..."
}
}
}
fleet/easypost/label/convert
{
"basic": "<base64 of '<api key>:' (Hub secret EASYPOST_BASIC)>",
"id": "shp_...",
"query": "file_format=ZPL"
}
EZ1000000001-style codes simulate statuses and send test webhooks):
fleet/easypost/tracker/create
{
"basic": "<base64 of '<api key>:' (Hub secret EASYPOST_BASIC)>",
"body": {
"tracker": {
"tracking_code": "EZ1000000001",
"carrier": "USPS"
}
}
}
fleet/easypost/batch/create
{
"basic": "<base64 of '<api key>:' (Hub secret EASYPOST_BASIC)>",
"body": {
"batch": {
"shipments": [
{
"id": "shp_..."
},
{
"id": "shp_..."
}
]
}
}
}
fleet/easypost/webhook/create
{
"basic": "<base64 of '<api key>:' (Hub secret EASYPOST_BASIC)>",
"body": {
"webhook": {
"url": "https://<public host>/easypost/webhooks",
"webhook_secret": "<webhook secret (Hub secret EASYPOST_WEBHOOK_SECRET)>"
}
}
}
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 intologistics/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.# Example actions for the EasyPostApi / EasyPostWebhookReceiver routes (docs page). Not part of the route template.
# Secret: EASYPOST_BASIC = base64 of "<api key>:" (API key as the Basic username, empty password), e.g.
# printf '%s:' "$EASYPOST_API_KEY" | base64. Start with a test key (EZTK...): nothing is bought, no carrier is
# contacted. Replace the <...> placeholders (address ids, carrier account id) with ids from your account.
# EasyPostBuyCheapestRate needs the Python script EasyPostHelper (easypost_helper.py) deployed first, and it BUYS
# postage for every created shipment when EASYPOST_BASIC holds a production key.
DEFINE ACTION EasyPostPalletShipment
ON TOPIC "porto/line3/palletizer/pallet-complete" DO
SET "auth" WITH GET SECRET "EASYPOST_BASIC"
SET "pallet" WITH (GET JSON "pallet_id" IN PAYLOAD AS STRING)
SET "oz" WITH (GET JSON "weight_oz" IN PAYLOAD AS DOUBLE)
PUBLISH TOPIC "fleet/easypost/shipment/create" WITH { "basic": {auth}, "body": { "shipment": { "reference": {pallet}, "from_address": { "id": "<adr_... ship-from dock>" }, "to_address": { "id": "<adr_... customer>" }, "parcel": { "length": 24, "width": 16, "height": 12, "weight": {oz} }, "carrier_accounts": [ "<ca_... from ListCarrierAccounts>" ] } } }
DEFINE ACTION EasyPostBuyCheapestRate
ON TOPIC "fleet/easypost/shipment/create/response" DO
SET "auth" WITH GET SECRET "EASYPOST_BASIC"
SET "shipment" WITH PAYLOAD
CALL PYTHON "EasyPostHelper.pick_rate"
WITH ({shipment}, "UPS,FedEx,USPS", "")
RETURN AS {pick}
SET "ok" WITH (GET JSON "ok" IN {pick} AS BOOL)
IF {ok} EQUALS TRUE THEN
SET "sid" WITH (GET JSON "shipment_id" IN {pick} AS STRING)
SET "rid" WITH (GET JSON "rate_id" IN {pick} AS STRING)
PUBLISH TOPIC "fleet/easypost/shipment/buy" WITH { "basic": {auth}, "id": {sid}, "body": { "rate": { "id": {rid} } } }
ELSE
PUBLISH TOPIC "porto/line3/shipping/rate-error" WITH {pick}
DEFINE ACTION EasyPostLabelToPrinter
ON TOPIC "fleet/easypost/shipment/buy/response" DO
SET "auth" WITH GET SECRET "EASYPOST_BASIC"
SET "sid" WITH (GET JSON "id" IN PAYLOAD AS STRING)
SET "code" WITH (GET JSON "tracking_code" IN PAYLOAD AS STRING)
SET "ref" WITH (GET JSON "reference" IN PAYLOAD AS STRING)
SET "png" WITH (GET JSON "postage_label.label_url" IN PAYLOAD AS STRING)
IF {code} == "null" THEN
PUBLISH TOPIC "porto/line3/shipping/buy-error" WITH PAYLOAD
ELSE
PUBLISH TOPIC "porto/line3/shipping/label" WITH { "shipment_id": {sid}, "reference": {ref}, "tracking_code": {code}, "label_url": {png} }
PUBLISH TOPIC "fleet/easypost/label/convert" WITH { "basic": {auth}, "id": {sid}, "query": "file_format=ZPL" }
DEFINE ACTION EasyPostTrackerToShipment
ON TOPIC "fleet/easypost/webhook/received" DO
SET "desc" WITH (GET JSON "description" IN PAYLOAD AS STRING)
IF {desc} STARTS_WITH "tracker." THEN
SET "sid" WITH (GET JSON "result.shipment_id" IN PAYLOAD AS STRING)
SET "code" WITH (GET JSON "result.tracking_code" IN PAYLOAD AS STRING)
SET "carrier" WITH (GET JSON "result.carrier" IN PAYLOAD AS STRING)
SET "status" WITH (GET JSON "result.status" IN PAYLOAD AS STRING)
SET "eta" WITH (GET JSON "result.est_delivery_date" IN PAYLOAD AS STRING)
IF {sid} == "null" THEN
PUBLISH TOPIC "logistics/trackers/" + {code} WITH { "tracking_code": {code}, "carrier": {carrier}, "status": {status}, "est_delivery_date": {eta}, "event": {desc} }
ELSE
PUBLISH TOPIC "logistics/shipments/" + {sid} WITH { "tracking_code": {code}, "carrier": {carrier}, "status": {status}, "est_delivery_date": {eta}, "event": {desc} }
DEFINE ACTION EasyPostTrackInbound
ON TOPIC "porto/receiving/inbound/expected" DO
SET "auth" WITH GET SECRET "EASYPOST_BASIC"
SET "code" WITH (GET JSON "tracking_code" IN PAYLOAD AS STRING)
SET "carrier" WITH (GET JSON "carrier" IN PAYLOAD AS STRING)
PUBLISH TOPIC "fleet/easypost/tracker/create" WITH { "basic": {auth}, "body": { "tracker": { "tracking_code": {code}, "carrier": {carrier} } } }
EasyPostBuyCheapestRate needs the Python script EasyPostHelper (easypost_helper.py), because LoT cannot loop over the rates array. Deploy it before the actions:
# Script Name: EasyPostHelper
# Coreflux broker Python helper for the EasyPostApi REST_API route (beta). Optional: only the
# EasyPostBuyCheapestRate example action uses it. LoT cannot loop over the rates array of a Shipment reply, so this
# script picks the rate to buy.
# pick_rate(shipment, carriers, services)
# shipment the JSON reply of CreateShipment / GetShipment / RerateShipment (string or object)
# carriers comma-separated carrier names to allow, e.g. "UPS,FedEx,USPS" ("" = any carrier)
# services comma-separated service names to allow, e.g. "Ground,GROUND_HOME_DELIVERY" ("" = any service)
# Returns {"ok": true, "shipment_id", "rate_id", "carrier", "service", "rate", "currency", "delivery_days",
# "reference"} for the cheapest matching rate, or {"ok": false, "reason", "shipment_id", "messages"} when the reply
# is an error, the shipment is already bought, or no rate matches (carrier errors are in "messages").
import json
def _csv(value):
return [v.strip().lower() for v in (value or "").split(",") if v.strip()]
def pick_rate(shipment, carriers="", services=""):
try:
s = json.loads(shipment) if isinstance(shipment, str) else (shipment or {})
except ValueError:
return {"ok": False, "reason": "reply is not JSON", "shipment_id": None, "messages": []}
if not isinstance(s, dict):
return {"ok": False, "reason": "reply is not a JSON object", "shipment_id": None, "messages": []}
sid = s.get("id")
messages = s.get("messages") or []
if "error" in s:
return {"ok": False, "reason": "EasyPost error: " + str((s.get("error") or {}).get("code")), "shipment_id": sid, "messages": messages}
if s.get("object") != "Shipment" or not sid:
return {"ok": False, "reason": "not a Shipment object", "shipment_id": sid, "messages": messages}
if s.get("selected_rate") or s.get("postage_label"):
return {"ok": False, "reason": "shipment already bought", "shipment_id": sid, "messages": messages}
allow_c, allow_s = _csv(carriers), _csv(services)
best = None
for r in s.get("rates") or []:
try:
price = float(r.get("rate"))
except (TypeError, ValueError):
continue
if allow_c and str(r.get("carrier", "")).lower() not in allow_c:
continue
if allow_s and str(r.get("service", "")).lower() not in allow_s:
continue
if best is None or price < best[0]:
best = (price, r)
if best is None:
return {"ok": False, "reason": "no rate matches the carrier / service filter", "shipment_id": sid, "messages": messages}
r = best[1]
return {"ok": True, "shipment_id": sid, "rate_id": r.get("id"), "carrier": r.get("carrier"), "service": r.get("service"),
"rate": r.get("rate"), "currency": r.get("currency"), "delivery_days": r.get("delivery_days"), "reference": s.get("reference")}
Connector template
# Coreflux REST_API template — EasyPost (beta): multi-carrier shipping labels, rates and tracking.
# Addresses (create, verify), parcels, customs, shipments (create with rates, buy label, convert label to ZPL /
# EPL2 / PDF, rerate, SmartRate, insure, refund), stateless rate estimates, trackers (create, get, list, batch),
# multi-parcel orders, batches (bulk buy, merged label, scan form), scan forms (manifests), carrier pickups, label
# refunds, events and webhooks, carrier accounts / types / metadata and the account user (wallet balance).
# Hugo catalog #197, Tier 3 (fleet). Sources: docs.easypost.com Markdown twins (llms.txt, cached in
# openapi-cache/easypost/pages/), the official EasyPost/examples cURL files (examples-curl/) and the official
# easypost-python v10.7.0 services (client-python/). EasyPost publishes no OpenAPI spec. Credentials are
# placeholders only.
# Docs: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/easypost
#
# One client route EasyPostApi on https://api.easypost.com. Endpoints carry /v2 (the queue hint
# https://api.easypost.com/v2/) except EstimateRates, which is the beta API under /beta.
# Auth: HTTP Basic with the API key as the username and an empty password, i.e. Authorization: Basic
# base64("<api key>:"). Pass that base64 value as "basic" in every payload; keep it in a Hub secret
# (EASYPOST_BASIC, or build it from EASYPOST_API_KEY) and read it with GET SECRET in an action. Never commit it.
# Test keys (EZTK...) are free: no postage is bought and no carrier is contacted. Production keys (EZAK...) spend
# wallet money on every event marked # PURCHASE (and on One-Call Buy). Keys act on the whole account.
# Live (2026-10-06): no Authorization -> 401 UNAUTHORIZED for every packaged method + path (an unknown method or
# path -> 404 NOT_FOUND); a fake key, Basic or Bearer -> 403 APIKEY.INACTIVE.
# Payload fields (control fields stay out of bodies; request bodies come from {value.json.body}):
# basic, id, query, body
# id is the EasyPost object id (shp_..., rate_..., adr_..., trk_..., batch_..., order_..., pickup_..., sf_...,
# evt_..., hook_...), placed in the path: pass ids you got from EasyPost, never raw user input. query is the URL
# query string without '?', e.g. page_size=20&before_id=shp_... (URL-encode values yourself). body is the JSON body
# with EasyPost's wrapper key where it has one ({"shipment": {...}}, {"tracker": {...}}, {"address": {...}}, ...).
# Units: parcel length / width / height in inches, weight in ounces; money as decimal strings in USD.
# Replies: objects as JSON with "object" and "mode" (test / production); lists as {"<objects>": [...], "has_more"}
# (newest first, page_size max 100, next page with before_id = last id). Errors: {"error": {"code", "message",
# "errors": [...]}} with 4xx / 5xx. Rate-limited calls answer 429.
DEFINE ROUTE EasyPostApi WITH TYPE REST_API
ADD REST_API_CONFIG
WITH BASE_ADDRESS "https://api.easypost.com"
WITH ENABLE_CLIENT "true"
WITH USE_SSL "true"
WITH REQUEST_TIMEOUT '60'
WITH DEFAULT_CONTENT_TYPE "application/json"
# Smoke (test key): CreateAddress -> CreateShipment -> BuyShipment -> ConvertShipmentLabel -> CreateTracker -> GetTracker -> ListEvents -> RefundShipment.
# The authenticated user (id, name, balance, recharge settings, child users)
ADD EVENT GetUser
WITH SOURCE_TOPIC "fleet/easypost/user/get"
WITH DESTINATION_TOPIC "fleet/easypost/user/get/response"
WITH METHOD "GET"
WITH ENDPOINT "/v2/users"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# Carrier accounts on the EasyPost account (ids ca_... for carrier_accounts in shipments)
ADD EVENT ListCarrierAccounts
WITH SOURCE_TOPIC "fleet/easypost/carrier-account/list"
WITH DESTINATION_TOPIC "fleet/easypost/carrier-account/list/response"
WITH METHOD "GET"
WITH ENDPOINT "/v2/carrier_accounts"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# Carrier types that can be added and their credential fields
ADD EVENT ListCarrierTypes
WITH SOURCE_TOPIC "fleet/easypost/carrier-type/list"
WITH DESTINATION_TOPIC "fleet/easypost/carrier-type/list/response"
WITH METHOD "GET"
WITH ENDPOINT "/v2/carrier_types"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# Carrier metadata: service levels, predefined packages, shipment options, supported features
ADD EVENT GetCarrierMetadata
WITH SOURCE_TOPIC "fleet/easypost/carrier-metadata/get"
WITH DESTINATION_TOPIC "fleet/easypost/carrier-metadata/get/response"
WITH METHOD "GET"
WITH ENDPOINT "/v2/metadata/carriers?{value.json.query}"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# WRITE: CreateAddress changes data in EasyPost. Restrict who can publish to fleet/easypost/address/create.
# Create an address; verify / verify_strict run address verification (result in verifications)
ADD EVENT CreateAddress
WITH SOURCE_TOPIC "fleet/easypost/address/create"
WITH DESTINATION_TOPIC "fleet/easypost/address/create/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/addresses"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
ADD "Content-Type" "application/json"
WITH BODY "{value.json.body}"
# WRITE: CreateAndVerifyAddress changes data in EasyPost. Restrict who can publish to fleet/easypost/address/create-verify.
# Create and strictly verify an address in one call; fails with ADDRESS.VERIFY.FAILURE when it cannot be verified
ADD EVENT CreateAndVerifyAddress
WITH SOURCE_TOPIC "fleet/easypost/address/create-verify"
WITH DESTINATION_TOPIC "fleet/easypost/address/create-verify/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/addresses/create_and_verify"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
ADD "Content-Type" "application/json"
WITH BODY "{value.json.body}"
# Verify an existing address
ADD EVENT VerifyAddress
WITH SOURCE_TOPIC "fleet/easypost/address/verify"
WITH DESTINATION_TOPIC "fleet/easypost/address/verify/response"
WITH METHOD "GET"
WITH ENDPOINT "/v2/addresses/{value.json.id}/verify"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# One address by id
ADD EVENT GetAddress
WITH SOURCE_TOPIC "fleet/easypost/address/get"
WITH DESTINATION_TOPIC "fleet/easypost/address/get/response"
WITH METHOD "GET"
WITH ENDPOINT "/v2/addresses/{value.json.id}"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# WRITE: CreateParcel changes data in EasyPost. Restrict who can publish to fleet/easypost/parcel/create.
# Create a parcel (inches and ounces, or a predefined_package)
ADD EVENT CreateParcel
WITH SOURCE_TOPIC "fleet/easypost/parcel/create"
WITH DESTINATION_TOPIC "fleet/easypost/parcel/create/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/parcels"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
ADD "Content-Type" "application/json"
WITH BODY "{value.json.body}"
# WRITE: CreateCustomsInfo changes data in EasyPost. Restrict who can publish to fleet/easypost/customs-info/create.
# Customs declaration (with customs items) for international shipments
ADD EVENT CreateCustomsInfo
WITH SOURCE_TOPIC "fleet/easypost/customs-info/create"
WITH DESTINATION_TOPIC "fleet/easypost/customs-info/create/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/customs_infos"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
ADD "Content-Type" "application/json"
WITH BODY "{value.json.body}"
# WRITE: CreateShipment changes data in EasyPost. With service + carrier_accounts in the body it is a One-Call Buy and PURCHASES postage. Restrict who can publish to fleet/easypost/shipment/create.
# Create a shipment and get its rates (no charge). Adding service + carrier_accounts makes it a One-Call Buy that purchases postage
ADD EVENT CreateShipment
WITH SOURCE_TOPIC "fleet/easypost/shipment/create"
WITH DESTINATION_TOPIC "fleet/easypost/shipment/create/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/shipments"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
ADD "Content-Type" "application/json"
WITH BODY "{value.json.body}"
# One shipment (rates, selected_rate, postage_label, tracker, fees)
ADD EVENT GetShipment
WITH SOURCE_TOPIC "fleet/easypost/shipment/get"
WITH DESTINATION_TOPIC "fleet/easypost/shipment/get/response"
WITH METHOD "GET"
WITH ENDPOINT "/v2/shipments/{value.json.id}"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# Shipments, newest first; page_size (max 100), before_id / after_id, start_datetime / end_datetime; has_more
ADD EVENT ListShipments
WITH SOURCE_TOPIC "fleet/easypost/shipment/list"
WITH DESTINATION_TOPIC "fleet/easypost/shipment/list/response"
WITH METHOD "GET"
WITH ENDPOINT "/v2/shipments?{value.json.query}"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# PURCHASE: BuyShipment spends money on a production key. Use a test key first; restrict who can publish to fleet/easypost/shipment/buy.
# Buy the label for one rate (adds tracking_code, postage_label, tracker); optional insurance (USD string) and end_shipper_id
ADD EVENT BuyShipment
WITH SOURCE_TOPIC "fleet/easypost/shipment/buy"
WITH DESTINATION_TOPIC "fleet/easypost/shipment/buy/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/shipments/{value.json.id}/buy"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
ADD "Content-Type" "application/json"
WITH BODY "{value.json.body}"
# Convert a bought PNG label to ZPL, EPL2 or PDF (label_zpl_url / label_epl2_url / label_pdf_url)
ADD EVENT ConvertShipmentLabel
WITH SOURCE_TOPIC "fleet/easypost/label/convert"
WITH DESTINATION_TOPIC "fleet/easypost/label/convert/response"
WITH METHOD "GET"
WITH ENDPOINT "/v2/shipments/{value.json.id}/label?{value.json.query}"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# Regenerate the rates of an unbought shipment (no body, no charge)
ADD EVENT RerateShipment
WITH SOURCE_TOPIC "fleet/easypost/rate/rerate"
WITH DESTINATION_TOPIC "fleet/easypost/rate/rerate/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/shipments/{value.json.id}/rerate"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# SmartRate: rates with time-in-transit percentiles for the shipment
WITH BODY ""
ADD EVENT GetShipmentSmartRates
WITH SOURCE_TOPIC "fleet/easypost/smartrate/get"
WITH DESTINATION_TOPIC "fleet/easypost/smartrate/get/response"
WITH METHOD "GET"
WITH ENDPOINT "/v2/shipments/{value.json.id}/smartrate"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# PURCHASE: InsureShipment spends money on a production key. Use a test key first; restrict who can publish to fleet/easypost/shipment/insure.
# Insure a bought shipment for amount (USD); charges the insurance fee
ADD EVENT InsureShipment
WITH SOURCE_TOPIC "fleet/easypost/shipment/insure"
WITH DESTINATION_TOPIC "fleet/easypost/shipment/insure/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/shipments/{value.json.id}/insure"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
ADD "Content-Type" "application/json"
WITH BODY "{value.json.body}"
# WRITE: RefundShipment changes data in EasyPost. Restrict who can publish to fleet/easypost/shipment/refund.
# Request a refund of an unused label (refund_status submitted; USPS refunds take up to 15 days)
ADD EVENT RefundShipment
WITH SOURCE_TOPIC "fleet/easypost/shipment/refund"
WITH DESTINATION_TOPIC "fleet/easypost/shipment/refund/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/shipments/{value.json.id}/refund"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# One rate by id
WITH BODY ""
ADD EVENT GetRate
WITH SOURCE_TOPIC "fleet/easypost/rate/get"
WITH DESTINATION_TOPIC "fleet/easypost/rate/get/response"
WITH METHOD "GET"
WITH ENDPOINT "/v2/rates/{value.json.id}"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# Stateless rates for addresses and a parcel without creating a shipment (beta API under /beta)
ADD EVENT EstimateRates
WITH SOURCE_TOPIC "fleet/easypost/rate/estimate"
WITH DESTINATION_TOPIC "fleet/easypost/rate/estimate/response"
WITH METHOD "POST"
WITH ENDPOINT "/beta/rates"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
ADD "Content-Type" "application/json"
WITH BODY "{value.json.body}"
# Delivery date predictions by carrier and service for a ZIP pair and ship date
ADD EVENT SmartRateDeliverBy
WITH SOURCE_TOPIC "fleet/easypost/smartrate/deliver-by"
WITH DESTINATION_TOPIC "fleet/easypost/smartrate/deliver-by/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/smartrate/deliver_by"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
ADD "Content-Type" "application/json"
WITH BODY "{value.json.body}"
# WRITE: CreateTracker changes data in EasyPost. Restrict who can publish to fleet/easypost/tracker/create.
# Track a package not bought through EasyPost (EZ... test codes simulate statuses with a test key); trackers may be billed in production
ADD EVENT CreateTracker
WITH SOURCE_TOPIC "fleet/easypost/tracker/create"
WITH DESTINATION_TOPIC "fleet/easypost/tracker/create/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/trackers"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
ADD "Content-Type" "application/json"
WITH BODY "{value.json.body}"
# One tracker (status, est_delivery_date, tracking_details, carrier_detail)
ADD EVENT GetTracker
WITH SOURCE_TOPIC "fleet/easypost/tracker/get"
WITH DESTINATION_TOPIC "fleet/easypost/tracker/get/response"
WITH METHOD "GET"
WITH ENDPOINT "/v2/trackers/{value.json.id}"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# Trackers, newest first; filter by tracking_code and carrier
ADD EVENT ListTrackers
WITH SOURCE_TOPIC "fleet/easypost/tracker/list"
WITH DESTINATION_TOPIC "fleet/easypost/tracker/list/response"
WITH METHOD "GET"
WITH ENDPOINT "/v2/trackers?{value.json.query}"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# Many trackers by tracking_codes in one call (POST, read only)
ADD EVENT RetrieveTrackerBatch
WITH SOURCE_TOPIC "fleet/easypost/tracker/batch"
WITH DESTINATION_TOPIC "fleet/easypost/tracker/batch/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/trackers/batch"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
ADD "Content-Type" "application/json"
WITH BODY "{value.json.body}"
# WRITE: CreateOrder changes data in EasyPost. With service + carrier_accounts in the body it is a One-Call Buy and PURCHASES postage. Restrict who can publish to fleet/easypost/order/create.
# Multi-parcel order (one shipment per carton) with rates; only some carriers (FedEx, UPS, DHLExpress, ...); service + carrier_accounts make it a One-Call Buy
ADD EVENT CreateOrder
WITH SOURCE_TOPIC "fleet/easypost/order/create"
WITH DESTINATION_TOPIC "fleet/easypost/order/create/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/orders"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
ADD "Content-Type" "application/json"
WITH BODY "{value.json.body}"
# PURCHASE: BuyOrder spends money on a production key. Use a test key first; restrict who can publish to fleet/easypost/order/buy.
# Buy every shipment of an order for one carrier + service
ADD EVENT BuyOrder
WITH SOURCE_TOPIC "fleet/easypost/order/buy"
WITH DESTINATION_TOPIC "fleet/easypost/order/buy/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/orders/{value.json.id}/buy"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
ADD "Content-Type" "application/json"
WITH BODY "{value.json.body}"
# One order with its shipments and rates
ADD EVENT GetOrder
WITH SOURCE_TOPIC "fleet/easypost/order/get"
WITH DESTINATION_TOPIC "fleet/easypost/order/get/response"
WITH METHOD "GET"
WITH ENDPOINT "/v2/orders/{value.json.id}"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# WRITE: CreateBatch changes data in EasyPost. Restrict who can publish to fleet/easypost/batch/create.
# Group shipments into a batch (end-of-shift label run)
ADD EVENT CreateBatch
WITH SOURCE_TOPIC "fleet/easypost/batch/create"
WITH DESTINATION_TOPIC "fleet/easypost/batch/create/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/batches"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
ADD "Content-Type" "application/json"
WITH BODY "{value.json.body}"
# One batch (state, status counts, label_url, scan_form)
ADD EVENT GetBatch
WITH SOURCE_TOPIC "fleet/easypost/batch/get"
WITH DESTINATION_TOPIC "fleet/easypost/batch/get/response"
WITH METHOD "GET"
WITH ENDPOINT "/v2/batches/{value.json.id}"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# Batches, newest first
ADD EVENT ListBatches
WITH SOURCE_TOPIC "fleet/easypost/batch/list"
WITH DESTINATION_TOPIC "fleet/easypost/batch/list/response"
WITH METHOD "GET"
WITH ENDPOINT "/v2/batches?{value.json.query}"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# WRITE: AddBatchShipments changes data in EasyPost. Restrict who can publish to fleet/easypost/batch-shipment/add.
# Add shipments to a batch
ADD EVENT AddBatchShipments
WITH SOURCE_TOPIC "fleet/easypost/batch-shipment/add"
WITH DESTINATION_TOPIC "fleet/easypost/batch-shipment/add/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/batches/{value.json.id}/add_shipments"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
ADD "Content-Type" "application/json"
WITH BODY "{value.json.body}"
# WRITE: RemoveBatchShipments changes data in EasyPost. Restrict who can publish to fleet/easypost/batch-shipment/remove.
# Remove shipments from a batch
ADD EVENT RemoveBatchShipments
WITH SOURCE_TOPIC "fleet/easypost/batch-shipment/remove"
WITH DESTINATION_TOPIC "fleet/easypost/batch-shipment/remove/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/batches/{value.json.id}/remove_shipments"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
ADD "Content-Type" "application/json"
WITH BODY "{value.json.body}"
# PURCHASE: BuyBatch spends money on a production key. Use a test key first; restrict who can publish to fleet/easypost/batch/buy.
# Buy every shipment in a batch (asynchronous; batch.updated events; no body)
ADD EVENT BuyBatch
WITH SOURCE_TOPIC "fleet/easypost/batch/buy"
WITH DESTINATION_TOPIC "fleet/easypost/batch/buy/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/batches/{value.json.id}/buy"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# WRITE: CreateBatchLabel changes data in EasyPost. Restrict who can publish to fleet/easypost/batch-label/create.
# One merged label file (PDF, EPL2 or ZPL) for a bought batch (asynchronous; label_url)
WITH BODY ""
ADD EVENT CreateBatchLabel
WITH SOURCE_TOPIC "fleet/easypost/batch-label/create"
WITH DESTINATION_TOPIC "fleet/easypost/batch-label/create/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/batches/{value.json.id}/label"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
ADD "Content-Type" "application/json"
WITH BODY "{value.json.body}"
# WRITE: CreateBatchScanForm changes data in EasyPost. Restrict who can publish to fleet/easypost/batch-scan-form/create.
# ScanForm (manifest) for a bought batch (no body)
ADD EVENT CreateBatchScanForm
WITH SOURCE_TOPIC "fleet/easypost/batch-scan-form/create"
WITH DESTINATION_TOPIC "fleet/easypost/batch-scan-form/create/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/batches/{value.json.id}/scan_form"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# WRITE: CreateScanForm changes data in EasyPost. Restrict who can publish to fleet/easypost/scan-form/create.
# ScanForm (manifest) for bought shipments of one carrier and from address, for the driver to scan at pickup
WITH BODY ""
ADD EVENT CreateScanForm
WITH SOURCE_TOPIC "fleet/easypost/scan-form/create"
WITH DESTINATION_TOPIC "fleet/easypost/scan-form/create/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/scan_forms"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
ADD "Content-Type" "application/json"
WITH BODY "{value.json.body}"
# One ScanForm (status, form_url, tracking_codes)
ADD EVENT GetScanForm
WITH SOURCE_TOPIC "fleet/easypost/scan-form/get"
WITH DESTINATION_TOPIC "fleet/easypost/scan-form/get/response"
WITH METHOD "GET"
WITH ENDPOINT "/v2/scan_forms/{value.json.id}"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# ScanForms, newest first
ADD EVENT ListScanForms
WITH SOURCE_TOPIC "fleet/easypost/scan-form/list"
WITH DESTINATION_TOPIC "fleet/easypost/scan-form/list/response"
WITH METHOD "GET"
WITH ENDPOINT "/v2/scan_forms?{value.json.query}"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# WRITE: CreatePickup changes data in EasyPost. Restrict who can publish to fleet/easypost/pickup/create.
# Request a carrier pickup window for a bought shipment (or batch); reply carries pickup_rates
ADD EVENT CreatePickup
WITH SOURCE_TOPIC "fleet/easypost/pickup/create"
WITH DESTINATION_TOPIC "fleet/easypost/pickup/create/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/pickups"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
ADD "Content-Type" "application/json"
WITH BODY "{value.json.body}"
# PURCHASE: BuyPickup spends money on a production key. Use a test key first; restrict who can publish to fleet/easypost/pickup/buy.
# Buy one pickup rate by carrier + service (not by id); confirmation in the reply
ADD EVENT BuyPickup
WITH SOURCE_TOPIC "fleet/easypost/pickup/buy"
WITH DESTINATION_TOPIC "fleet/easypost/pickup/buy/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/pickups/{value.json.id}/buy"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
ADD "Content-Type" "application/json"
WITH BODY "{value.json.body}"
# WRITE: CancelPickup changes data in EasyPost. Restrict who can publish to fleet/easypost/pickup/cancel.
# Cancel a scheduled pickup (no body)
ADD EVENT CancelPickup
WITH SOURCE_TOPIC "fleet/easypost/pickup/cancel"
WITH DESTINATION_TOPIC "fleet/easypost/pickup/cancel/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/pickups/{value.json.id}/cancel"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# One pickup (status, confirmation, pickup_rates)
WITH BODY ""
ADD EVENT GetPickup
WITH SOURCE_TOPIC "fleet/easypost/pickup/get"
WITH DESTINATION_TOPIC "fleet/easypost/pickup/get/response"
WITH METHOD "GET"
WITH ENDPOINT "/v2/pickups/{value.json.id}"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# Pickups, newest first
ADD EVENT ListPickups
WITH SOURCE_TOPIC "fleet/easypost/pickup/list"
WITH DESTINATION_TOPIC "fleet/easypost/pickup/list/response"
WITH METHOD "GET"
WITH ENDPOINT "/v2/pickups?{value.json.query}"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# WRITE: CreateRefund changes data in EasyPost. Restrict who can publish to fleet/easypost/refund/create.
# Refund labels by carrier + tracking codes
ADD EVENT CreateRefund
WITH SOURCE_TOPIC "fleet/easypost/refund/create"
WITH DESTINATION_TOPIC "fleet/easypost/refund/create/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/refunds"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
ADD "Content-Type" "application/json"
WITH BODY "{value.json.body}"
# Events (tracker.updated, batch.updated, scan_form.updated, refund.successful, ...) for webhook catch-up; result is not included
ADD EVENT ListEvents
WITH SOURCE_TOPIC "fleet/easypost/event/list"
WITH DESTINATION_TOPIC "fleet/easypost/event/list/response"
WITH METHOD "GET"
WITH ENDPOINT "/v2/events?{value.json.query}"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# One event (description, status, pending_urls, completed_urls)
ADD EVENT GetEvent
WITH SOURCE_TOPIC "fleet/easypost/event/get"
WITH DESTINATION_TOPIC "fleet/easypost/event/get/response"
WITH METHOD "GET"
WITH ENDPOINT "/v2/events/{value.json.id}"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# Webhook delivery attempts of one event (request body, response code, URL)
ADD EVENT ListEventPayloads
WITH SOURCE_TOPIC "fleet/easypost/event-payload/list"
WITH DESTINATION_TOPIC "fleet/easypost/event-payload/list/response"
WITH METHOD "GET"
WITH ENDPOINT "/v2/events/{value.json.id}/payloads"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# WRITE: CreateWebhook changes data in EasyPost. Restrict who can publish to fleet/easypost/webhook/create.
# Register a webhook URL for this key's mode (a test key registers a test-mode webhook); every event is sent to every webhook
ADD EVENT CreateWebhook
WITH SOURCE_TOPIC "fleet/easypost/webhook/create"
WITH DESTINATION_TOPIC "fleet/easypost/webhook/create/response"
WITH METHOD "POST"
WITH ENDPOINT "/v2/webhooks"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
ADD "Content-Type" "application/json"
WITH BODY "{value.json.body}"
# Webhooks of this mode (url, disabled_at)
ADD EVENT ListWebhooks
WITH SOURCE_TOPIC "fleet/easypost/webhook/list"
WITH DESTINATION_TOPIC "fleet/easypost/webhook/list/response"
WITH METHOD "GET"
WITH ENDPOINT "/v2/webhooks"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# One webhook
ADD EVENT GetWebhook
WITH SOURCE_TOPIC "fleet/easypost/webhook/get"
WITH DESTINATION_TOPIC "fleet/easypost/webhook/get/response"
WITH METHOD "GET"
WITH ENDPOINT "/v2/webhooks/{value.json.id}"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
# WRITE: UpdateWebhook changes data in EasyPost. Restrict who can publish to fleet/easypost/webhook/update.
# Re-enable a disabled webhook and / or replace webhook_secret and custom_headers (custom_headers is replaced as a whole)
ADD EVENT UpdateWebhook
WITH SOURCE_TOPIC "fleet/easypost/webhook/update"
WITH DESTINATION_TOPIC "fleet/easypost/webhook/update/response"
WITH METHOD "PATCH"
WITH ENDPOINT "/v2/webhooks/{value.json.id}"
WITH HEADERS
ADD "Accept" "application/json"
ADD "User-Agent" "Coreflux-Broker"
ADD "Authorization" "Basic {value.json.basic}"
ADD "Content-Type" "application/json"
WITH BODY "{value.json.body}"
Webhook receiver template
# EasyPost webhook receiver — REST_API route (server mode, beta, companion of EasypostRestApi.lot)
# Receives EasyPost webhook Events (tracker, batch, scan form, refund, insurance, report, payment, invoice, claim)
# and publishes each JSON body on MQTT, so tracking updates reach the broker without polling. Opt-in: deploying it
# opens an HTTP port.
# Docs: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/easypost
#
# Set up: create one webhook per API key mode with CreateWebhook (or Dashboard > Webhooks and Events): the production
# key -> https://<public host>/easypost/webhooks, the test key -> https://<public host>/easypost/webhooks/test.
# EasyPost sends every Event of that mode to every webhook (no per-type filter); route by "description"
# (tracker.updated, batch.updated, ...) in an action. Delivery is at least once, so handle duplicates (Event id).
# EasyPost needs HTTPS with a publicly trusted certificate: put a reverse proxy or tunnel in front of SERVER_PORT 9014.
# Answer 2xx; endpoints that keep failing are disabled (re-enable with UpdateWebhook). 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.
# This route checks neither, so verify them in the proxy (or keep the URL secret and restrict source addresses).
DEFINE ROUTE EasyPostWebhookReceiver WITH TYPE REST_API
ADD REST_API_CONFIG
WITH ENABLE_SERVER "true"
WITH SERVER_PORT '9014'
WITH DEFAULT_CONTENT_TYPE "application/json"
# production-mode events: tracker.created / tracker.updated, batch.created / batch.updated, scan_form.created / scan_form.updated, refund.successful, insurance.*, report.*, payment.*, shipment.invoice.*, claim.* (POST https://<public host>/easypost/webhooks)
ADD EVENT WebhookEvents
WITH ENDPOINT "/easypost/webhooks"
WITH METHOD "POST"
WITH DESTINATION_TOPIC "fleet/easypost/webhook/received"
# test-mode events (webhook created with a test API key; EZ... test tracking codes trigger tracker events) (POST https://<public host>/easypost/webhooks/test)
ADD EVENT TestWebhookEvents
WITH ENDPOINT "/easypost/webhooks/test"
WITH METHOD "POST"
WITH DESTINATION_TOPIC "fleet/easypost/webhook-test/received"
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
CreateShipmenteach or one multi-parcelCreateOrder(only some carriers support orders). - Labels are PNG by default;
ConvertShipmentLabelconverts a bought label to ZPL, EPL2 or PDF, or setoptions.label_formatwhen you create the shipment.CreateBatchLabelbuilds one merged file for a bought batch. - Asynchronous objects: batch buy and batch labels, scan forms and refunds finish later; follow them with
GetBatch/GetScanFormor thebatch.updated,scan_form.updatedandrefund.successfulwebhook Events. - Refunds:
RefundShipmentandCreateRefundwork on unused labels; USPS refunds can take up to 15 days (refund.successful). - Trackers are created automatically when a label is bought. Standalone
CreateTrackercalls 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.
ListEventsandListEventPayloadshelp to catch up after downtime. - Errors:
{"error": {"code", "message", "errors"}}.401 UNAUTHORIZEDmeans no credentials,403 APIKEY.INACTIVEa wrong or disabled key,404 NOT_FOUNDan unknown id or path,422a validation error (for exampleSHIPMENT.POSTAGE.EXISTSwhen buying twice). Rate-limited calls answer429.
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/buyshowsPOST /v2/shipments(create),GET /shipments/:id/labelshows a plain retrieve,POST /trackers/batchshowsPOST /v2/trackers,POST /orders/:id/buyshowsPOST /v2/orders,POST /pickups/:id/buyand/cancelshowPOST /v2/pickups,GET /events/:event_id/payloadsshows the single-payload URL, and the EndShipper page labels its create callPOST /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, not401; only a request withoutAuthorizationgets401 UNAUTHORIZED. Without auth, an unknown method or path answers404 NOT_FOUND, so the unauthenticated probe tells existing calls apart. - The rate estimate API is
POST /beta/rates, outside/v2, so the connector base ishttps://api.easypost.comand each endpoint carries/v2or/beta. - The docs describe HTTP Basic (key as username, empty password); the official client libraries send
Authorization: Bearer <key>. The auth layer parses both (same403for a fake key); the template uses Basic as documented. GetRate(GET /v2/rates/{id}) has no docs page; it is the official client’srate.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.

