# Shippo

- Tool: Bubble
- Difficulty: Intermediate
- Time required: 1–2 hours
- Last updated: July 2026

## TL;DR

Connect Bubble to Shippo's multi-carrier shipping API using the API Connector with a Private Authorization header. The critical gotcha: Shippo requires `Authorization: Shippo <token>` — not Bearer. Use Bearer and you get HTTP 200 with an empty rates array and no error, burning hours of debugging time. With the correct header, Bubble can compare rates across 85+ carriers, generate shipping labels, and store tracking numbers entirely server-side.

## Shippo + Bubble: 85 carriers, one API, zero browser key exposure

Adding shipping to a Bubble app traditionally meant building separate integrations for each carrier — USPS, UPS, FedEx, DHL — each with different auth methods and response formats. Shippo eliminates that by providing a single API that normalizes rate data and label generation across 85+ carriers.

The integration flow follows Shippo's two-step model. First, a POST to `/shipments` submits address and parcel data and returns a list of available rates from all eligible carriers. Your Bubble app displays these rates in a Repeating Group so the user can choose their preferred carrier and service. Second, a POST to `/transactions` purchases the label for the selected rate — this is the step that charges your Shippo balance and returns a label URL and tracking number.

Bubble's API Connector handles both calls server-side, which matters for label purchases: the transaction never touches the browser, meaning the API token can't be intercepted, and purchases are logged through Bubble's workflow system.

The most common integration failure — and the one that trips up nearly every developer the first time — is using the wrong Authorization header format. Shippo requires `Authorization: Shippo <token>` with the literal word 'Shippo,' not 'Bearer.' Using Bearer returns an HTTP 200 with an empty rates array and no error message, making it look like the call worked but returned no data. The fix is a single word change in your shared header.

## Before you start

- A Shippo account at goshippo.com — the Free tier supports up to 25 shipments per month with Shippo branding on labels; higher volumes require a paid plan
- Shippo test tokens from the API section of your Shippo dashboard — test tokens (starting with `shippo_test_`) let you generate sample labels without spending money or triggering real shipments
- The API Connector plugin installed in your Bubble app (Plugins tab → Add plugins → search 'API Connector' by Bubble)
- Sender address information to use in POST /shipments — Shippo requires a complete `address_from` object including name, street, city, state, zip, and country

## Step-by-step guide

### 1. Get your Shippo API token and understand test vs live modes

Go to app.goshippo.com and log in to your Shippo account. In the left sidebar, click 'API' under the developer section. You'll see two types of tokens: Test tokens (beginning with `shippo_test_`) and Live tokens (beginning with `shippo_live_`). Always start with the test token during Bubble development — test mode lets you call POST /shipments and get sample rates, and POST /transactions to generate sample label PDFs, all without spending money or triggering real shipments with carriers.

Click the copy button next to your test token and save it. When your Bubble app goes to production, you'll swap this for the live token in the API Connector shared header — a single field change.

Understand the Shippo pricing model before going live: Shippo's Free tier allows up to 25 shipments per month with Shippo branding on the label. Above 25 shipments or to remove branding, you move to a pay-per-label model. For busy marketplaces and e-commerce stores, check goshippo.com/pricing for current per-label rates from $0.05 per label. There are no monthly fees on the pay-as-you-go plan.

Important: your Shippo balance must be funded before live label purchases (POST /transactions) will succeed. Add a payment method and prepay balance through the Shippo dashboard before switching to live tokens.

**Expected result:** You have a Shippo test token (shippo_test_...) copied and ready to use in Bubble's API Connector. You understand that test transactions generate sample labels without charging your balance.

### 2. Configure the API Connector with the correct Shippo authorization header

In your Bubble editor, click 'Plugins' in the left sidebar, then 'Add plugins.' Search for 'API Connector' (published by Bubble) and install it. Click 'API Connector' in your plugins list, then 'Add another API.' Name it 'Shippo.'

Set the 'API Root URL' to `https://api.goshippo.com`.

Under 'Shared headers,' click 'Add a shared header.' This is the most critical step: set the key to `Authorization` and the value to `Shippo YOUR_TEST_TOKEN_HERE` — replacing YOUR_TEST_TOKEN_HERE with your actual test token. Note carefully: the prefix is the word **Shippo** (with a capital S), followed by a space, followed by the token. This is NOT `Bearer`. If you use `Bearer`, Shippo returns HTTP 200 with an empty `rates` array and no error message — a completely silent failure that appears to work but returns no data.

Check the **'Private'** checkbox on this header. This ensures the token is server-side only and never appears in browser network requests.

Also add a Content-Type header: key `Content-Type`, value `application/json`. This is needed for the POST calls.

To test the connection, add a first call named 'List Carriers.' Set method to GET, path to `/carrier_accounts`. Under 'Use as,' select 'Data.' Click 'Initialize call.' A successful response lists your connected carrier accounts. If you see 'There was an issue setting up your call,' re-check the Authorization header — the most common issue is using 'Bearer' instead of 'Shippo.'

```
{
  "api_name": "Shippo",
  "base_url": "https://api.goshippo.com",
  "shared_headers": [
    {
      "key": "Authorization",
      "value": "Shippo <private: your_shippo_token>",
      "private": true,
      "note": "CRITICAL: Use 'Shippo' not 'Bearer' — Bearer returns silent empty rates"
    },
    {
      "key": "Content-Type",
      "value": "application/json"
    }
  ]
}
```

**Expected result:** The Shippo API Connector is configured with the correct `Shippo <token>` authorization header marked Private. The 'List Carriers' Initialize call succeeds and returns your carrier account data.

### 3. Add the POST /shipments call to fetch carrier rates

Add a second call to the Shippo API Connector. Click 'Add a call,' name it 'Get Rates,' and set the method to POST and the path to `/shipments`. Under 'Use as,' select 'Action' — this is a data-mutation call, not a read-only data source.

Set the body type to 'JSON body' and enter the following body structure. In Bubble's API Connector, mark each value that should be dynamic with angle brackets — you'll fill these in from your Bubble workflow at call time:

The key field to include is `async: false`. By default, Shippo processes shipment creation asynchronously and returns immediately without rates. Setting `async: false` forces Shippo to wait and include the `rates` array in the response. Without this, `rates` comes back empty and you have to poll a separate endpoint.

After entering the body, click 'Initialize call.' You'll need to provide real test addresses and parcel dimensions. Use a US address like 215 Clayton St, San Francisco, CA 94117 as the from address and a valid destination address. Use realistic parcel dimensions (weight in oz: 16 for 1lb, length/width/height in in). A successful initialize shows the response includes `object_id`, `status`, and a `rates` array with carrier options. Each rate has `amount`, `currency`, `provider` (carrier name), `servicelevel.name`, `estimated_days`, and crucially `object_id` — which you'll use to purchase the label in the next step.

```
{
  "call_name": "Get Rates",
  "method": "POST",
  "path": "/shipments",
  "use_as": "Action",
  "body": {
    "address_from": {
      "name": "<dynamic: Sender Name>",
      "street1": "<dynamic: Sender Street>",
      "city": "<dynamic: Sender City>",
      "state": "<dynamic: Sender State>",
      "zip": "<dynamic: Sender ZIP>",
      "country": "US"
    },
    "address_to": {
      "name": "<dynamic: Recipient Name>",
      "street1": "<dynamic: Recipient Street>",
      "city": "<dynamic: Recipient City>",
      "state": "<dynamic: Recipient State>",
      "zip": "<dynamic: Recipient ZIP>",
      "country": "US"
    },
    "parcels": [
      {
        "length": "<dynamic: Length inches>",
        "width": "<dynamic: Width inches>",
        "height": "<dynamic: Height inches>",
        "distance_unit": "in",
        "weight": "<dynamic: Weight oz>",
        "mass_unit": "oz"
      }
    ],
    "async": false
  }
}
```

**Expected result:** The 'Get Rates' call initializes successfully. Bubble detects the response fields including a `rates` array with carrier options, each having an `object_id`, `amount`, `provider`, and `servicelevel` fields.

### 4. Display carrier rates in a Repeating Group for user selection

Now build the rate selection UI. In your Bubble page, add an Input Group with fields for: recipient name, street address, city, state, ZIP. Add inputs for parcel dimensions (length, width, height in inches) and weight in ounces, or pre-populate these from your product/order data type.

Add a Button labeled 'Get Shipping Rates.' In its click workflow, add an action 'Run an API call' → choose 'Shippo — Get Rates' and map all the dynamic parameters from your input fields or the current order's data.

Add a Custom State to the page: name it `shippo_rates` with type 'text' (you'll store the JSON rates data here) or better, create a Thing in Bubble's database called 'Shipping Rate' with fields: `rate_id` (Text), `provider` (Text), `service_name` (Text), `price` (Number), `estimated_days` (Number), `shipment_id` (Text). In the workflow, after the Get Rates call, add a 'Create a list of Things' step for each item in the response rates array, mapping: `rate_id` to `rates item's object_id`, `provider` to `rates item's provider`, `service_name` to `rates item's servicelevel's name`, `price` to `rates item's amount` (as number), `estimated_days` to `rates item's estimated_days`.

Add a Repeating Group bound to 'Search for Shipping Rates' (showing only the most recent batch — add a constraint like `shipment_id = last shipment id` to avoid mixing old rate results). Inside the cell, show provider name, service name, price, and estimated days. Add a Radio Button or a 'Select' button per row that stores the current cell's Shipping Rate's `rate_id` to a page custom state `selected_rate_id`.

Add a 'Purchase Label' button below the Repeating Group, enabled only when `selected_rate_id is not empty`.

**Expected result:** Clicking 'Get Shipping Rates' populates a Repeating Group showing carrier options with prices and delivery estimates. The user can select one option, which stores the rate's object_id for the label purchase step.

### 5. Add the POST /transactions call to purchase the label

Add a third call to the Shippo API Connector. Click 'Add a call,' name it 'Purchase Label,' set method to POST, path to `/transactions`. Under 'Use as,' select 'Action.'

Set the body to:
- `rate`: the selected rate's `object_id` (dynamic)
- `label_file_type`: `"PDF"` (or `"PNG"` if you prefer image labels)
- `async`: `false` (same reason as before — synchronous response with the label URL included)

Click 'Initialize call' and provide a real rate `object_id` from a previous test shipment call as the test value. On success, the response includes `object_id` (transaction ID), `status` (should be `SUCCESS`), `tracking_number`, `tracking_url_provider` (a carrier tracking link), and `label_url` (direct URL to the PDF label).

In your 'Purchase Label' button workflow, add these steps in order:
1. Run API call 'Shippo — Purchase Label' with `rate` set to the `selected_rate_id` page state.
2. Make changes to the current Order thing: set `tracking_number` to `result's tracking_number`, `label_url` to `result's label_url`, `shipping_status` to 'Fulfilled'.
3. Show a success popup with the tracking number and a link to download the label PDF.
4. Send a confirmation notification (email or in-app) to the buyer with the tracking number.

For tracking webhooks (getting real-time tracking updates pushed to Bubble): create a Backend Workflow in Bubble's Settings → API section, enable the Workflow API, and register your Backend Workflow URL in Shippo's Webhook settings. Note: Backend Workflows require a paid Bubble plan.

```
{
  "call_name": "Purchase Label",
  "method": "POST",
  "path": "/transactions",
  "use_as": "Action",
  "body": {
    "rate": "<dynamic: selected rate object_id>",
    "label_file_type": "PDF",
    "async": false
  },
  "response_fields": [
    "object_id",
    "status",
    "tracking_number",
    "tracking_url_provider",
    "label_url",
    "messages"
  ]
}
```

**Expected result:** The 'Purchase Label' call returns a SUCCESS status with a tracking_number and label_url. The order record in Bubble's database is updated with the tracking number and label link, and the shipping status changes to 'Fulfilled'.

### 6. Store tracking data and set up Privacy rules for order data

After completing the label purchase workflow, ensure your Bubble database is correctly set up to store and protect shipping data. In the Data tab, open your Order data type. Verify you have these fields: `tracking_number` (Text), `label_url` (Text), `shipping_status` (Option Set or Text), `shippo_transaction_id` (Text), `carrier` (Text), and `estimated_delivery` (Date).

Privacy rules are critical here. Your order data contains sensitive customer information — addresses, names, and now tracking links. Go to Data tab → Privacy → click on 'Order.' Review the existing rules. At minimum: ensure 'Everyone else' cannot view any Order fields. Add a rule 'Current User' can view orders 'when Order's customer = Current User' to let buyers see their own orders. Add a separate rule for sellers: 'Current User' can view orders 'when Order's seller = Current User' to let sellers see orders they need to fulfill. Never expose order data through Bubble's Data API without explicit rules.

For label URLs: store the full Shippo label URL in Bubble's database. These URLs are persistent (unlike signed expiring URLs) and can be retrieved and displayed any time after purchase. Add a 'Download Label' link element in your seller dashboard bound to 'Current cell's Order's label_url.'

For tracking: store tracking numbers and build a tracking status workflow using the Shippo tracking endpoint GET `/tracks/{carrier}/{tracking_number}` — this returns the latest status. Or display tracking via the `tracking_url_provider` link (opens the carrier's native tracking page). The latter saves WU on Bubble since you're not making repeated API calls for status updates.

**Expected result:** Order records store tracking numbers, label URLs, and fulfillment status. Privacy rules ensure only the relevant buyer and seller can see each order's data. The seller dashboard shows a 'Download Label' link for each fulfilled order.

## Best practices

- Always use `async: false` in both POST /shipments and POST /transactions request bodies. Shippo's async default returns empty results immediately and processes in the background — synchronous mode ensures your Bubble workflow receives complete rate and label data in a single response.
- Mark the Authorization header as Private in Bubble's API Connector. The Shippo token authorizes real money transactions — label purchases. It must never appear in browser network requests, client-side JavaScript, or visible page elements.
- Use test tokens (shippo_test_...) for all development and staging work. Test transactions generate real-looking sample labels without charging your balance or triggering actual carrier pickups. Switch to the live token only when going to production.
- Set Bubble Data Privacy rules on your Order and Shipping Rate data types. By default, all Things in Bubble are readable by any logged-in user via the Data API. Add rules scoping order visibility to the buyer and seller involved in each transaction.
- Generate separate tokens per application in the Shippo dashboard. If you have multiple Bubble apps using Shippo, use a separate token for each. This makes it easy to revoke access for a specific app without affecting others.
- Store both the tracking_number and the label_url in your Order data type after a successful transaction. The label URL is persistent — it doesn't expire — so users can re-download it any time. The tracking number lets you build a status lookup workflow.
- Monitor WU consumption for shipping workflows in Bubble's Logs tab. Each POST /shipments and POST /transactions call consumes WU. If users frequently abandon after getting rates without purchasing, consider adding rate results to Bubble's database with a time-to-live field instead of running fresh API calls every session.

## Use cases

### In-App Shipping Label Generator for Marketplace Sellers

Let sellers on your Bubble marketplace generate shipping labels without leaving your platform. When an order is placed, the buyer's address populates automatically. The seller clicks 'Get Rates,' sees carrier options with prices in a Repeating Group, selects their preferred service, and clicks 'Buy Label.' The label URL and tracking number are saved to the order record in Bubble's database.

Prompt example:

```
Build a Bubble order fulfillment page where sellers see pending orders, click 'Get Rates' to fetch live Shippo carrier rates for each order's address, select a rate from a repeating group, click 'Purchase Label,' and the tracking number and label PDF URL are saved to the order record automatically.
```

### Shipping Rate Calculator for E-Commerce Checkout

Add a real-time shipping rate estimator to your Bubble checkout flow. As the buyer enters their address, trigger a POST /shipments call with your standard box dimensions and display carrier options with live prices before order confirmation. The selected carrier rate is stored with the order and used for label purchase after payment is confirmed.

Prompt example:

```
Create a Bubble checkout step that calls Shippo's POST /shipments endpoint when the shipping address is entered, displays returned carrier rates in a radio-button repeating group, and stores the chosen rate object_id with the order to use for label generation after payment.
```

### Bulk Label Generation for Order Batches

For high-volume sellers, add a 'Process Batch' button that loops through unfulfilled orders and generates Shippo labels for each using the same carrier and service level. Use Bubble's 'Schedule workflow on a list' to process each order sequentially, POST /transactions for each, and mark orders as 'Shipped' when the label URL is returned.

Prompt example:

```
Build a Bubble admin page where a seller selects multiple unfulfilled orders using checkboxes, chooses a carrier service from a dropdown, clicks 'Generate Labels,' and Bubble runs a scheduled workflow that creates a Shippo transaction for each order, saves the label URL to the order, and marks it as fulfilled.
```

## Troubleshooting

### POST /shipments returns HTTP 200 but the rates array is empty

Cause: This is the most common Shippo bug in Bubble. There are two possible causes: (1) the Authorization header uses `Bearer` instead of `Shippo` — Bearer returns a valid-looking response with empty rates and no error, or (2) `async` is not set to `false` in the POST body, so Shippo processes rates asynchronously and doesn't include them in the immediate response.

Solution: Check your shared Authorization header value — it must be exactly `Shippo <your_token>` with a capital S and a space. If it says `Bearer <token>`, change it to `Shippo <token>`. Also verify the POST body includes `"async": false`. If both fixes are applied and rates are still empty, check that the addresses are valid US addresses with correct state codes and ZIP formats — invalid addresses return an empty rates array without an explicit address validation error.

### Initialize call returns 'There was an issue setting up your call' or a 401 Unauthorized error

Cause: The API token is incorrect, expired, or incorrectly formatted. A 401 can also appear if the Authorization header value accidentally includes extra spaces or quotation marks around the token string.

Solution: Go to your Shippo dashboard → API and copy the test token again fresh. In Bubble's API Connector, click on the Shippo API → edit the shared Authorization header. Clear the value field completely and re-type `Shippo ` (with a trailing space), then paste the token. Avoid copy-pasting from a document that may have added smart quotes. The Private checkbox should be checked. Click Initialize call again with valid test address data.

### POST /transactions returns status ERROR with a 'balance' message

Cause: Your Shippo live account does not have sufficient prepaid balance to purchase the label. This only occurs in live mode — test tokens never charge balance.

Solution: Log into app.goshippo.com → Billing and add funds to your Shippo account. Labels are purchased by deducting from your prepaid balance. Alternatively, confirm you are using a test token (shippo_test_...) rather than the live token during development — test transactions do not deduct real money.

### Rate object_id rejected with 'invalid rate' when purchasing label

Cause: Shippo rate object IDs expire after 7 days. If the user selected a rate but didn't proceed to purchase for more than 7 days (or if you're reusing a rate ID from an old test), the transaction will fail with an invalid rate error.

Solution: The rate selection and label purchase should happen in the same user session. If your app allows users to return later to complete a purchase, re-run the POST /shipments call to generate fresh rates when they return to the checkout step, rather than storing and reusing old rate IDs.

### Tracking webhooks from Shippo are not reaching the Bubble Backend Workflow

Cause: Backend Workflows (API Workflows) are only available on paid Bubble plans. If the app is on a free plan, the Backend Workflow section doesn't appear and Bubble cannot receive inbound webhook POSTs from Shippo.

Solution: Upgrade to at least a Bubble Starter plan to enable Backend Workflows. After upgrading, go to Settings → API → enable 'This app exposes a Workflow API,' create a new Backend Workflow to receive Shippo webhooks, and register the endpoint URL `https://yourapp.bubbleapps.io/api/1.1/wf/{workflow_name}` in Shippo's Webhooks settings. As a free-plan alternative, use polling: add a 'Refresh Tracking' button that calls GET /tracks/{carrier}/{tracking_number} on demand.

## Frequently asked questions

### Why does using Bearer in the Authorization header return empty rates with no error?

Shippo's API accepts the Authorization header but checks the prefix. A `Bearer` prefix causes Shippo to reject the authentication silently — it returns HTTP 200 with an empty `rates` array instead of a 401 error. This makes the bug nearly invisible. The correct format is `Shippo <token>` with the word Shippo (capital S) as the prefix.

### Do I need a paid Bubble plan for the Shippo integration?

The core rate comparison and label generation flow — API Connector with POST /shipments and POST /transactions — works on Bubble's free plan. A paid Bubble plan is required only if you want to receive real-time tracking webhooks from Shippo (which needs Backend Workflows) or if you want to schedule automatic tracking status syncs using scheduled Backend Workflows.

### Can I test the integration without buying real labels?

Yes. Use a Shippo test token (starts with `shippo_test_`) in the API Connector shared header. In test mode, POST /shipments returns realistic rate data using test carrier accounts, and POST /transactions generates sample label PDFs with fake tracking numbers. No real labels are created and no money is charged. Switch to a live token when you're ready for production.

### What happens if I call POST /shipments with async: true (or omit the field)?

Shippo's default is async: true. In async mode, Shippo processes the shipment asynchronously and returns immediately with a shipment object_id but an empty rates array. You'd need to poll the GET /shipments/{id} endpoint repeatedly until the status changes to VALID and rates are populated. This polling pattern is complex to build in Bubble and wastes WU. Always set async: false for synchronous rate retrieval.

### How do I handle international shipments to non-US addresses?

Shippo supports international shipping. For non-US destinations, add `address_to.country` with the 2-letter ISO country code. International shipments also require customs declarations — add a `customs_declaration` object to the POST /shipments body with item descriptions, values, HS codes, and shipper signature. Not all carriers in Shippo support all international routes — the rates response will show only eligible options.

### Can Bubble receive tracking updates automatically when a package status changes?

Yes, through Shippo webhooks and Bubble Backend Workflows (paid plans). Shippo sends webhook POSTs to your Backend Workflow URL when tracking status changes. Create a Backend Workflow in Bubble's Settings → API section, enable the Workflow API, and register the workflow's endpoint URL (`https://yourapp.bubbleapps.io/api/1.1/wf/{name}`) in Shippo's Webhooks settings. On free Bubble plans, use a 'Refresh Tracking' button that triggers a GET /tracks call on demand instead.

---

Source: https://www.rapidevelopers.com/bubble-integrations/shippo
© RapidDev — https://www.rapidevelopers.com/bubble-integrations/shippo
