# EasyPost

Source: https://developers.swell.is/app-guides/easypost

The EasyPost app shows your customers live EasyPost carrier rates at checkout, and buys shipping labels from your EasyPost account, when you choose **Buy EasyPost label** on an order or automatically when an order is paid or submitted. After a label is bought, the app fulfills the order in Swell with the carrier and tracking number, and keeps the tracking status on the order until the parcel is delivered.

Rates and labels come from the carrier accounts in your EasyPost account, your own or the ones EasyPost provides, and labels are charged to your EasyPost account.

## How it works

| When | What the app does |
| --- | --- |
| A customer enters a shipping address at checkout | Asks EasyPost for rates for one parcel and shows each carrier service as a shipping option. |
| You choose Buy EasyPost label on an order | Buys a label from your EasyPost account for the service the customer chose, or the cheapest, and creates a Swell shipment with the carrier and tracking number. |
| An order is paid or submitted, if you choose automatic labels | Buys the label the same way, without anyone choosing Buy EasyPost label. |
| You choose Void EasyPost label on an order | Asks EasyPost to refund the label and cancels the Swell shipment the app created. |
| Every hour | Refreshes the tracking status of up to 20 labels that haven't been delivered. |
| EasyPost sends a tracking update, if you add the webhook | Saves the new tracking status on the order. |

## Before you begin

- An EasyPost account with at least one carrier account, your own or one EasyPost provides.
- A payment method in EasyPost for production labels. Labels bought with a test key aren't charged.
- Shipping weights on your products, so rates and labels have the right weight.

## Set up

Install EasyPost from the Swell App Store, and set it up in your test environment before the live one.

**1. Copy an API key**

In EasyPost, go to **Account Settings > API Keys** and copy a key. Use the test key, which starts with `EZTK`, in your Swell test environment, and the production key, which starts with `EZAK`, in live.

**2. Add it to the app**

In Swell, go to **Apps > EasyPost > Settings** and enter the **API key**. On the **Ship-from address** tab, enter the address your parcels ship from: at least the street, city, postal code and country code, and the state code where carriers need it, such as in the US and Canada. Some carriers need a phone number to buy a label. On the **Default parcel** tab, check the box size and default weight. On the **Labels** tab, choose when to **Buy labels**. Click **Save settings**.

**3. Test the connection**

Choose **Actions > Test connection**. The message says whether EasyPost accepted the key and flags a missing ship-from address. With a production key, it also lists your carrier accounts. While **Webhook secret** is empty, it also suggests setting one.

**4. Turn on checkout rates**

Go to **Settings > Shipping**, turn on **EasyPost** and click **Save changes**. Rates appear at checkout only after this step.

**5. Place a test order**

Add an item to a cart and enter a shipping address. EasyPost rates appear among the shipping options. Choose one, then place and pay the order. If **Buy labels** is set to **Automatically when an order is paid**, the app buys the label. Otherwise, open the order and choose **Actions > Buy EasyPost label**. The order's **EasyPost** tab shows **Label purchased**, and the order gets a shipment with the tracking number.

**6. Go live**

Settings are saved separately for each environment. In the live environment, repeat steps 2 to 4 with your production key. If you use the webhook, add a production webhook for live, as described under **Webhook for tracking updates**.

## Using EasyPost

### Rates at checkout

When a cart's shipping address or items change, the app asks EasyPost for rates from your ship-from address to the customer's, for one parcel with the default dimensions. The weight is the sum of the items' shipping weights, or the default weight when no item has one. Each carrier service becomes a shipping option, named like **USPS Priority** or **UPS Ground**, with the estimated delivery days. The cheapest option is listed first.

- Limit the rates with **Carrier accounts** and **Services**, and add a **Markup (%)** and a **Handling fee** to each rate.
- A rate is shown only when EasyPost quotes it in the cart's currency.
- Your other shipping services, Swell's own or other apps', stay at checkout alongside EasyPost's.

### Buying labels

By default, a label is bought only when you open an order and choose **Actions > Buy EasyPost label**. To buy labels automatically, set **Buy labels** to **Automatically when an order is paid** or **Automatically when an order is submitted**. Automatic labels are charged to your EasyPost account as soon as the order is paid or submitted.

To buy a label, the app creates an EasyPost shipment with the shipping and ship-from addresses, one parcel with the default dimensions, and the order number as the reference. **Label service** decides which rate it buys:

- **Service the customer chose, else the cheapest**: the EasyPost service the customer chose at checkout, from the cheapest carrier account that offers it, as at checkout. If EasyPost no longer offers it, or the order was shipped with another shipping service, the cheapest rate.
- **Only the service the customer chose**: the EasyPost service the customer chose. If it isn't offered, or the order was shipped with another shipping service, no label is bought.
- **Always the cheapest**: the cheapest rate, whatever the customer chose.

The cheapest rate is chosen among the services allowed by **Services**. The label costs EasyPost's price at the time it's bought, without your markup or handling fee, and the order's **EasyPost** tab shows it as **Postage**.

After buying, the app creates a Swell shipment for everything still to ship, with the carrier, service and tracking number, and saves the label link and tracking number on the order's **EasyPost** tab. With **Create Swell shipment when a label is bought** off, the label and tracking are saved on the tab but the order isn't fulfilled.

Canceled orders, orders with nothing to ship and orders that already have a label are skipped. If a purchase is interrupted after EasyPost bought the label, the next try uses that label instead of buying another.

If buying fails, the order's **EasyPost** tab shows **Error** with the reason. When EasyPost has a server error or doesn't answer in time, Swell tries an automatic purchase again. Other errors, such as an invalid address or no rates, aren't retried. To try again, choose **Actions > Buy EasyPost label**.

### Customs

When an order ships to another country than your ship-from address, its label includes a customs declaration with one line for each item: the product name, quantity, line total in the order's currency, weight and SKU. The contents type is merchandise, and an undeliverable parcel is returned. The declaration is signed with the **Customs signer**, or the ship-from name when it's empty.

To declare a product's **HS code** and **Country of origin**, enter them on the product's **EasyPost** tab. The country of origin is a 2-letter country code, such as `PT`. Without an HS code, none is declared, and without a country of origin, your ship-from country is used.

### Voiding labels

To void a label, open the order and choose **Actions > Void EasyPost label**. The app asks EasyPost to refund the label. Unless the carrier rejects the refund, the app cancels the Swell shipment it created for the label, if any, and clears the tracking, and the order shows **Label voided** with the **Refund** status, **Submitted** or **Refunded**. You can then buy a new label. When the carrier rejects the refund, the label and its Swell shipment stay, and **Refund** shows **Rejected**.

### Tracking

Every hour, the app refreshes up to 20 labels that haven't been delivered, those checked longest ago first. For each, it saves the tracking status, the latest scan with its location, the estimated delivery and the tracking page on the order's **EasyPost** tab. To refresh right away, choose **Actions > Refresh tracking now** in the app settings.

A label is no longer refreshed once its tracking status is **Delivered**, **Returned to sender**, **Failure**, **Cancelled** or **Error**, or 60 days after it was bought.

### Webhook for tracking updates

The webhook is optional. With it, EasyPost sends tracking updates as carriers scan the parcel, and the app saves them on the order right away. Without it, tracking is refreshed every hour.

**1. Choose a webhook secret**

In **Apps > EasyPost > Settings**, on the **Webhook** tab, enter a long random string as the **Webhook secret** and click **Save settings**. Keep a copy to enter in EasyPost.

**2. Build the webhook URL**

Start from the **Webhook URL** on the same tab:

```text
https://{store-id}:{public-key}@{store-id}.swell.store/functions/easypost/easypost-webhook
```

Replace `{store-id}`, in both places, with your store ID, the subdomain of your `swell.store` address, and `{public-key}` with a Swell public key from **Developer > API keys**: the test key for the test environment, the live key for live. Never use a secret key here.

**3. Add it in EasyPost**

In EasyPost, go to **Account Settings > Webhooks** and add a webhook with the URL and the same webhook secret. EasyPost webhooks belong to a mode: add a test webhook with the test-key URL for your test API key, and a production webhook with the live-key URL for your production key.

With a secret, the app rejects events that aren't signed with it. Without one, it accepts unsigned events. Either way, it loads the tracker from EasyPost with your API key before saving anything, so an event's content is never used as is.

### In the dashboard

| Where | What it's for |
| --- | --- |
| Apps > EasyPost > Settings | The API key, ship-from address, checkout rates, default parcel, labels and webhook settings, with Test connection and Refresh tracking now. |
| Settings > Shipping | Turns EasyPost rates at checkout on or off. |
| The order list | An EasyPost column with each order's status: Label purchased, Label voided or Error. |
| An order's EasyPost tab | The status, EasyPost shipment ID, carrier, service, tracking number, tracking status and details, estimated delivery, tracking page, label link, postage and currency, and the refund status and last error when there are any. |
| An order's Actions menu | Buy EasyPost label, or Void EasyPost label once the order has a label. |
| A product's EasyPost tab | The HS code and country of origin declared on customs forms. |

## Settings

Settings are in **Apps > EasyPost > Settings** and are saved separately for each environment.

| Setting | Description | Default |
| --- | --- | --- |
| API key | From EasyPost > Account Settings > API Keys. Use a test key (EZTK…) in test and a production key (EZAK…) in live. Masked on the settings page. Required. | None |
| Enabled | Turns off checkout rates, label buying, tracking and the webhook without uninstalling the app. Test connection still works. | On |
| Ship-from address | Name, company, street, street line 2, city, state or province code, postal code, 2-letter country code, phone and email. Street, city, postal code and country are needed for rates and labels. The company is used as the name when the name is empty. | None |
| Show EasyPost rates at checkout | Turns checkout rates off while labels and tracking keep working. | On |
| Carrier accounts | Optional. EasyPost carrier account IDs, starting with ca_, separated by commas. Used for rates and labels. | All carrier accounts |
| Services | Optional. Carrier and service pairs to offer, such as USPS:Priority and UPS:Ground, separated by commas. Also limits the cheapest rate for labels. | All services |
| Markup (%) | Added to each rate at checkout. | 0 |
| Handling fee | A flat amount added to each rate after the markup, in the checkout currency. | 0 |
| Show estimated delivery days | Shows the estimated delivery under each option. | On |
| Default parcel | The box size (in or cm) used for every rate and label, and the default weight (lb, oz, kg or g) used when no item has a shipping weight. | 10 × 8 × 4 in, 1 lb |
| Buy labels | Only with Buy label on the order page, Automatically when an order is paid, or Automatically when an order is submitted. Labels are charged to your EasyPost account. | Only with Buy label on the order page |
| Label service | Service the customer chose, else the cheapest; Only the service the customer chose; or Always the cheapest. | Service the customer chose, else the cheapest |
| Label format | PDF, PNG or ZPL for thermal printers. | PDF |
| Create Swell shipment when a label is bought | Fulfills the Swell order with the carrier and tracking number. When off, the label and tracking are still saved on the EasyPost tab. | On |
| Customs signer | The name that signs customs declarations for international labels. | The ship-from name |
| Webhook secret | Optional. The secret EasyPost signs webhook events with. When set, unsigned events are rejected. Masked on the settings page. | None |
| Webhook URL | Read only. The URL pattern to add in EasyPost, with your store ID and a public key filled in. | The URL pattern |

## Limitations

- Rates and labels use one parcel with the default dimensions. Multi-parcel and split shipments aren't supported, and an order has one label at a time.
- Rates aren't reserved. A label costs EasyPost's price when it's bought, which can differ from the rate the customer paid.
- EasyPost has 8 seconds to return rates. If it fails or takes longer, checkout shows only your other shipping services.
- Rate requests don't include customs details, so some international rates may be missing at checkout. Labels include a customs declaration with each product's HS code and country of origin when you've entered them. If a carrier needs more, buying fails with EasyPost's error on the order.
- Canceling or changing a Swell order doesn't void its label or change the EasyPost shipment. Void the label yourself.
- Carriers refund only unused labels, within their own time limits. The refund status is saved when you void the label, and later changes in EasyPost aren't shown.
- Without the webhook, tracking can be up to an hour old, and each hourly refresh covers up to 20 labels.
- **Test connection** lists carrier accounts only with a production key, because EasyPost lists them only for production keys.
- Items without a shipping weight add nothing to the parcel weight.

## Troubleshooting

Errors from buying a label are shown in **Error** on the order's **EasyPost** tab, and actions show their result in a message. The app also logs its errors. In the Swell dashboard, the logs are under **Developer > Console > Logs**.

### EasyPost rates don't appear at checkout

Check that **EasyPost** is turned on in **Settings > Shipping** and saved, that **Show EasyPost rates at checkout** and **Enabled** are on, and that the ship-from address has a street, city, postal code and country code. Rates in another currency than the cart's, and services not listed in **Services**, aren't shown. Run **Test connection** to check the key. With a production key, it also lists your carrier accounts.

### A label isn't bought automatically

Check that **Buy labels** is set to buy automatically when the order is paid, or submitted, and that **Enabled** is on. Canceled orders and orders with nothing to ship are skipped. If buying failed, the reason is in **Error** on the order's **EasyPost** tab. To try again, choose **Actions > Buy EasyPost label**.

### "Fill in the ship-from address in the app settings"

Enter the street, city, postal code and country code on the **Ship-from address** tab of the app settings, click **Save settings**, and buy the label again.

### "Missing shipping address"

The order has no shipping country. Add the shipping address to the order, then choose **Actions > Buy EasyPost label**.

### "The order is not shipped with an EasyPost service"

**Label service** is set to **Only the service the customer chose**, and the customer chose another shipping service. To buy the cheapest EasyPost rate for these orders, set **Label service** to **Service the customer chose, else the cheapest**.

### "The service the customer chose (…) is no longer offered by EasyPost"

EasyPost didn't quote the customer's service when the label was bought, for example after your carrier accounts changed. To buy the cheapest rate instead, set **Label service** to **Service the customer chose, else the cheapest**, then choose **Actions > Buy EasyPost label**.

### "EasyPost returned no rates for this order"

No carrier quoted the parcel, or no rate matches **Services**. The message also lists up to 5 carrier messages from EasyPost. Check the addresses, the default parcel, the items' weights and your carrier accounts.

### "EasyPost did not respond within …"

EasyPost took too long. Swell tries automatic purchases again, and if EasyPost bought the label before the time ran out, the next try uses it instead of buying another. To try again yourself, choose **Actions > Buy EasyPost label**.

### "EasyPost rejected the request"

**Test connection** shows this when EasyPost doesn't accept the API key. Check that you copied the whole key, and that it's the test key in the test environment and the production key in live.

### "The carrier rejected the refund"

Carriers refund only unused labels, within their own time limits. The label and its Swell shipment stay as they are.

### Tracking isn't updated

Wait for the next hourly refresh, or choose **Actions > Refresh tracking now** in the app settings. When more than 20 labels are waiting, the message says so, and you can run it again. Labels bought more than 60 days ago aren't refreshed. If you use the webhook, check that it's in the same mode, test or production, as your API key.

### "Invalid webhook signature"

EasyPost's webhook gets a `401` response with this message when **Webhook secret** is set in Swell but the event wasn't signed with the same secret. Enter the same secret in the app settings and in the EasyPost webhook. Tracking keeps refreshing every hour in the meantime.

### "Function app.easypost.easypost-webhook not found"

EasyPost's webhook gets a `404` response with this message when the URL has no public key, a key from another store, or a key for an environment where the app isn't installed. Check the `{public-key}` part of the URL.

## Uninstalling

Uninstalling the app removes EasyPost rates from checkout and from **Settings > Shipping**, stops buying labels and refreshing tracking, and removes the app's settings page, the **EasyPost** tab, column and order actions, and the **EasyPost** tab on products. The webhook stops working, so EasyPost's deliveries to it fail. Uninstalling applies to the environment you are in, so remove the app from the test and live environments separately.

The EasyPost values saved on your orders and products are kept, and come back if you reinstall the app. Labels, shipments and webhooks in your EasyPost account stay there, and Swell shipments the app created stay on your orders. If you added the webhook, delete it in EasyPost under **Account Settings > Webhooks**.

## For developers

| Function | Runs on | What it does |
| --- | --- | --- |
| shipping-rates | after:order.shipping, for the easypost shipping extension | Gets EasyPost rates and adds them to the shipping options. |
| easypost-order-events | order.paid and order.submitted | Buys the label on the event chosen in Buy labels. |
| refresh-tracking | Every hour | Refreshes the trackers of up to 20 labels that haven't been delivered. |
| easypost-webhook | A public POST route | Receives EasyPost tracker events and saves the tracker on its order. |
| test-connection | The Test connection action | Checks the API key and lists the carrier accounts of a production key. |
| refresh-tracking-now | The Refresh tracking now action | Runs the tracking refresh now. |
| buy-label | The Buy EasyPost label order action | Buys the label for one order. |
| void-label | The Void EasyPost label order action | Refunds the order's label and cancels the Swell shipment the app created. |

The app stores its fields on orders under `$app.easypost`: `easypost_status`, `easypost_shipment_id`, `easypost_tracker_id`, `easypost_tracking_code`, `easypost_tracking_url`, `easypost_label_url`, `easypost_carrier`, `easypost_service`, `easypost_label_cost`, `easypost_label_currency`, `easypost_label_date`, `easypost_refund_status`, `easypost_swell_shipment_id`, `easypost_tracking_status`, `easypost_tracking_details`, `easypost_tracking_date`, `easypost_eta`, `easypost_checked_at` and `easypost_error`. On products, it stores `easypost_hs_code` and `easypost_origin_country` under `$app.easypost`.

Rate IDs are `easypost_` plus the EasyPost carrier and service in lowercase, such as `easypost_usps_priority`, and each option also carries `easypost_carrier` and `easypost_service`. The label step matches the order's `shipping.service` against these IDs and buys the cheapest of the matching rates.

Each rate request and each label creates an EasyPost shipment with `POST /shipments`. Labels are bought with `POST /shipments/{id}/buy` and voided with `POST /shipments/{id}/refund`, and tracking is read with `GET /trackers/{id}`. Weights are sent in ounces and dimensions in inches, with one decimal. Swell item weights are line totals in the store's weight unit.

A label purchase gets 5 seconds to create the shipment and 4 to buy it. The EasyPost shipment ID is saved on the order before buying, so a retry finds a label bought by an interrupted run. EasyPost server errors, timeouts and Swell errors are rethrown so the platform retries the model event. EasyPost 4xx errors are saved on the order and not retried.

Customs declarations use `contents_type: merchandise`, `eel_pfc: NOEEI 30.37(a)` and `non_delivery_option: return`. Each item's `value` is its line total with the order's `currency`, and `hs_tariff_number` and `origin_country` come from the product's `easypost_hs_code` and `easypost_origin_country`. Swell shipments get the EasyPost carrier in lowercase as `carrier`, the readable service name as `service`, and the EasyPost shipment ID and tracking page in `notes`.

Each tracking refresh reads up to 20 orders sorted by `easypost_checked_at`, whose `easypost_label_date` is in the last 60 days, or whose order date is for labels bought before that field existed, and stops starting new ones after about 6 seconds. Tracker updates older than `easypost_tracking_date` are skipped, and a tracker whose shipment isn't the order's label is ignored.

The webhook handles `tracker.created` and `tracker.updated` and acknowledges other events. With a secret, it checks `X-Hmac-Signature`, which is `hmac-sha256-hex=` plus the HMAC-SHA256 of the raw body, keyed with the secret normalized to NFKD. It answers right away, then loads the tracker with `GET /trackers/{id}` before saving it.

## Support

Swell builds and maintains the EasyPost app. For help with the app, email support@swell.is. For questions about your EasyPost account, carriers or labels, contact EasyPost.
