> ## Documentation Index
> Fetch the complete documentation index at: https://docs.coreflux.org/llms.txt
> Use this file to discover all available pages before exploring further.

# EasyPost

> Client-mode REST API connector for EasyPost multi-carrier shipping: verify addresses, create shipments and compare rates, buy labels (and convert them to ZPL for line printers), multi-parcel orders, batches, scan forms, carrier pickups, insurance and refunds, trackers and events, with HTTP Basic auth (API key as username); companion server connector for EasyPost webhooks (tracker updates to MQTT)

Technical Preview

## Overview

[EasyPost](https://www.easypost.com) 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.

> [!WARNING]
>
> **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) |

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)

| 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) |

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.

> [!WARNING]
>
> 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

> [!WARNING]
>
> 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`:

```bash theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
printf '%s:' "$EASYPOST_API_KEY" | base64
```

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

| 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):

```json fleet/easypost/shipment/create theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "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>"
      ]
    }
  }
}
```

Buy the label for one rate (**purchase** with a production key):

```json fleet/easypost/shipment/buy theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "basic": "<base64 of '<api key>:' (Hub secret EASYPOST_BASIC)>",
  "id": "shp_...",
  "body": {
    "rate": {
      "id": "rate_..."
    }
  }
}
```

Convert the bought label to ZPL for a Zebra printer on the line:

```json fleet/easypost/label/convert theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "basic": "<base64 of '<api key>:' (Hub secret EASYPOST_BASIC)>",
  "id": "shp_...",
  "query": "file_format=ZPL"
}
```

Track an inbound package bought elsewhere (with a test key, `EZ1000000001`-style codes simulate statuses and send test webhooks):

```json fleet/easypost/tracker/create theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "basic": "<base64 of '<api key>:' (Hub secret EASYPOST_BASIC)>",
  "body": {
    "tracker": {
      "tracking_code": "EZ1000000001",
      "carrier": "USPS"
    }
  }
}
```

End of shift: batch the day's shipments, then buy them, print one merged label file and a scan form:

```json fleet/easypost/batch/create theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "basic": "<base64 of '<api key>:' (Hub secret EASYPOST_BASIC)>",
  "body": {
    "batch": {
      "shipments": [
        {
          "id": "shp_..."
        },
        {
          "id": "shp_..."
        }
      ]
    }
  }
}
```

Register the webhook receiver:

```json fleet/easypost/webhook/create theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "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 into `logistics/shipments/<shipment id>` messages (or `logistics/trackers/<tracking code>` for trackers without a shipment), and track inbound packages. Replace the `<...>` placeholders.

> [!WARNING]
>
> `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.

```lot theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# 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:

```python theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# 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

```lot theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# 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

```lot theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# 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 `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

> [!WARNING]
>
> **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.

## Related

* [REST API connectors](/latest/lot-language/routes/rest-api-routes)
* [REST API apps catalog](/latest/lot-language/routes/rest-api-apps/index)
* [EasyPost API docs](https://docs.easypost.com)
* [EasyPost webhooks](https://docs.easypost.com/docs/webhooks)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.