# ShipStation

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

## TL;DR

Connect Bubble to ShipStation using the API Connector with HTTP Basic auth — Base64-encode your API key and secret together, place the result in a Private Authorization header, and all calls run server-side. Key flows: display orders in a Bubble dashboard, shop carrier rates at checkout, and receive shipment status webhooks in a Backend Workflow. Critical reminder: ShipStation has no sandbox — label creation charges a real carrier account immediately.

## ShipStation as the Fulfillment Layer for Your Bubble E-Commerce App

Most Bubble e-commerce apps use Stripe for payments and Bubble's own database for order records — but ShipStation sits in the middle of the physical fulfillment layer, handling which carrier to use, what it will cost, and how to print the label. The ShipStation API lets your Bubble app expose shipping data directly to store operators without requiring them to log into the ShipStation dashboard.

The most important thing to know before building: ShipStation has no test or sandbox environment. The moment you call the label creation endpoint (POST /shipments/createlabel), you are spending real money on a real carrier transaction. During development, use rate-shopping calls (POST /carriers/listrates) freely — those are read-only against ShipStation's rate tables — and save label creation testing for a controlled go-live moment.

ShipStation's auth model is HTTP Basic: you generate an API key and API secret in ShipStation's account settings, combine them as `apiKey:apiSecret`, Base64-encode the result, and send it as `Basic [encoded]` in every Authorization header. In Bubble, this one header marked Private authenticates every ShipStation call you add to the API Connector — no per-call auth tokens, no token refresh, no OAuth complexity.

The 40 requests-per-minute rate limit is shared across your entire ShipStation account. During development with multiple team members triggering calls, this limit is easier to hit than it looks. Cache order data in Bubble's database rather than calling ShipStation on every page load.

## Before you start

- A ShipStation account with at least a Starter plan ($9.99/month) — API credentials are available on all paid plans
- Your ShipStation API Key and API Secret from Account Settings → API Settings
- Bubble app on any plan for read flows (GET /orders, GET /carriers); a paid Bubble plan is required for Backend Workflow webhook endpoints
- Bubble's API Connector plugin installed (Plugins tab → Add plugins → search 'API Connector' → Install)
- A Base64 encoding tool to encode your `apiKey:apiSecret` string (use an online encoder or your browser's console: `btoa('apiKey:apiSecret')`)

## Step-by-step guide

### 1. Generate ShipStation API Credentials

In ShipStation, click your account name in the top-right corner and select 'Account Settings'. In the left sidebar, click 'API Settings' (under the Integrations section). If you do not see an API key yet, click 'Generate API Keys'. ShipStation will show you an API Key and an API Secret — copy both immediately. The API Secret is shown only once in some account configurations; if you navigate away before copying it, you may need to regenerate.

Now you need to Base64-encode the combination `apiKey:apiSecret` with a colon separating them. Open your browser's developer console (right-click → Inspect → Console tab) and type: `btoa('YOUR_API_KEY:YOUR_API_SECRET')` replacing the values with your actual credentials. Copy the resulting Base64 string — it will look like a long block of characters ending in `==`.

IMPORTANT: You are encoding the key and secret TOGETHER with a colon between them. Encoding them separately produces a completely different string and will result in 401 Unauthorized on every API call.

```
// In browser console — Base64-encode apiKey:apiSecret together
btoa('abc123apikey:xyz789apisecret')
// Returns something like: 'YWJjMTIzYXBpa2V5Onh5ejc4OWFwaXNlY3JldA=='

// The Authorization header value will be:
// Basic YWJjMTIzYXBpa2V5Onh5ejc4OWFwaXNlY3JldA==

// DO NOT encode separately:
btoa('abc123apikey') + ':' + btoa('xyz789apisecret') // WRONG — produces 401
```

**Expected result:** You have a Base64-encoded string representing your `apiKey:apiSecret`. It is ready to be used as the Authorization header value prefixed with 'Basic '.

### 2. Configure the API Connector with Private Basic Auth

In your Bubble app, go to the Plugins tab. Click the API Connector plugin to open its settings. Click 'Add another API'. Name it 'ShipStation'.

Under shared headers, click 'Add shared headers / parameters'. Add a header with:
- Key: `Authorization`
- Value: `Basic [YOUR_BASE64_STRING]` — replace [YOUR_BASE64_STRING] with the encoded string from Step 1
- Tick the 'Private' checkbox on this header row

The 'Private' checkbox ensures this header value is never sent to or accessible from the browser. All ShipStation calls under this API group inherit this auth header automatically.

Set the API root URL to `https://ssapi.shipstation.com`.

Also add a `Content-Type: application/json` header (this one does not need to be Private) to ensure POST/PUT call bodies are parsed correctly by ShipStation's servers.

```
{
  "api_name": "ShipStation",
  "root_url": "https://ssapi.shipstation.com",
  "shared_headers": [
    {
      "key": "Authorization",
      "value": "Basic <private — Base64(apiKey:apiSecret)>",
      "private": true
    },
    {
      "key": "Content-Type",
      "value": "application/json"
    }
  ]
}
```

**Expected result:** The ShipStation API group appears in the API Connector with the Authorization header configured and the Private checkbox ticked. You are ready to add individual API calls.

### 3. Set Up Order Display and Carrier Data Calls

Add your first call inside the ShipStation API group. Click 'Add another call'. Name it 'Get Account'. Set Method to GET and path to `/accounts/list`. Click 'Initialize call' — if auth is correct, you will receive account information. This confirms your credentials work.

Now add the primary read call. Click 'Add another call'. Name it 'Get Orders'. Set Method to GET and path to `/orders`. Add query parameters:
- `orderStatus` with a default value of `awaiting_shipment`
- `pageSize` with a default value of `100`

Set 'Use as' to 'Data'. Click 'Initialize call'. You should receive a JSON response with an `orders` array. Bubble auto-detects the fields within each order object — order number, status, customer details, line items (as a nested array), and weight.

Create a Bubble Data Type called 'ShipStationOrder' with fields matching the order properties you need: `order_number` (text), `order_status` (text), `customer_name` (text), `order_total` (number), `ship_to_city` (text), `ship_to_state` (text), `tracking_number` (text).

Add a third call. Name it 'Get Carriers'. Set Method to GET and path to `/carriers`. Set 'Use as' to 'Data'. Initialize. This call returns the list of enabled carriers (USPS, UPS, FedEx, etc.) and their service codes — you will need these codes when calling listrates.

```
// GET /orders — sample response structure
{
  "orders": [
    {
      "orderId": 987654321,
      "orderNumber": "ORDER-1001",
      "orderStatus": "awaiting_shipment",
      "customerName": "Jane Smith",
      "orderDate": "2026-07-09T14:30:00.0000000",
      "orderTotal": 89.99,
      "shipTo": {
        "name": "Jane Smith",
        "street1": "123 Main St",
        "city": "Austin",
        "state": "TX",
        "postalCode": "78701",
        "country": "US"
      },
      "weight": { "value": 16, "units": "ounces" },
      "items": [
        { "name": "Blue Widget", "quantity": 2, "unitPrice": 44.99 }
      ]
    }
  ],
  "total": 1,
  "page": 1,
  "pages": 1
}
```

**Expected result:** The Get Account, Get Orders, and Get Carriers calls all initialize successfully. A Repeating Group bound to Get Orders shows pending ShipStation orders in the Bubble editor preview.

### 4. Add Carrier Rate Shopping as an Action Call

Rate shopping (fetching carrier prices for a specific shipment) is a write-style API call in ShipStation — it uses POST, not GET. Add a new call. Name it 'List Rates'. Set Method to POST and path to `/carriers/listrates`. Set 'Use as' to 'Action' (not Data, because this is triggered by a user action, not a data source).

In the body section, add a JSON body with the required fields. Use dynamic placeholders for the values you will pass from the Bubble workflow:
- `carrierCode`: the carrier to quote (e.g., 'stamps_com' for USPS, 'ups' for UPS)
- `serviceCode`: optional — leave empty to get all rates for that carrier
- `fromPostalCode`: your warehouse/fulfillment zip code
- `toState`, `toCountry`, `toPostalCode`, `toCity`: destination address
- `weight`: object with `value` and `units` ('ounces' or 'grams')
- `dimensions`: optional object with `length`, `width`, `height`, `units`

Click 'Initialize call'. Since this is a POST, you need to provide sample values for all fields to get a test response. Use your real carrier code and a real destination zip code — ShipStation's rate engine requires valid address data.

In the Bubble workflow editor, trigger the List Rates action from a button click on the checkout page. Pass the current customer's shipping address fields as the dynamic values. Display the returned rates (each with `serviceCode`, `serviceName`, `shipmentCost`) in a Bubble Dropdown or Repeating Group for the customer to select.

```
// POST /carriers/listrates — request body
{
  "carrierCode": "stamps_com",
  "serviceCode": null,
  "packageCode": "package",
  "fromPostalCode": "78701",
  "toState": "CA",
  "toCountry": "US",
  "toPostalCode": "90210",
  "toCity": "Beverly Hills",
  "weight": { "value": 16, "units": "ounces" },
  "dimensions": { "length": 10, "width": 8, "height": 4, "units": "inches" }
}

// Response — array of rate options
[
  {
    "serviceName": "USPS Priority Mail",
    "serviceCode": "usps_priority_mail",
    "shipmentCost": 8.95,
    "otherCost": 0.00
  },
  {
    "serviceName": "USPS First Class Mail",
    "serviceCode": "usps_first_class_mail",
    "shipmentCost": 4.20,
    "otherCost": 0.00
  }
]
```

**Expected result:** The List Rates action call initializes successfully and returns an array of carrier rate options. Triggering it from a Bubble workflow on the checkout page populates a rate selector for the customer.

### 5. Configure a Backend Workflow Endpoint for ShipStation Webhooks

ShipStation can notify your Bubble app when key events occur: a new order arrives (ORDER_NOTIFY), an order ships (SHIP_NOTIFY), an item is ordered (ITEM_ORDER_NOTIFY). To receive these events, you need a Backend Workflow API endpoint.

In your Bubble app, go to the Settings tab → API section. Enable 'This app exposes a Workflow API'. Then go to the Backend Workflows tab. Click 'New API Workflow'. Name it 'ShipStation Webhook'. Under 'Detect request data', click 'Click here to add parameters' — but for webhooks, it's easier to use 'Detect request data' by sending a test event.

Once the Backend Workflow is created, Bubble generates a public URL. The URL format is: `https://YOUR_APP_NAME.bubbleapps.io/api/1.1/wf/shipstation-webhook`. Copy this URL — this is what you will register in ShipStation.

In ShipStation, go to Account Settings → Integrations → Webhooks. Click 'Add a Webhook'. Choose the event type (start with SHIP_NOTIFY for tracking updates), paste your Bubble Backend Workflow URL in the Target URL field, and save.

ShipStation will send a test payload to confirm the endpoint. In your Bubble Backend Workflow, add an action to create or update a ShipStationOrder Data Type record: find the order by order number (from the webhook payload), update the tracking_number and order_status fields.

RapidDev's team has configured webhook pipelines like this for dozens of Bubble e-commerce apps — including complex multi-carrier setups with fallback logic. If the webhook setup feels complex for your use case, reach out for a free scoping call at rapidevelopers.com/contact.

Note: Backend Workflows are unavailable on Bubble's Free plan. You must be on a paid plan to expose API workflow endpoints. Also register your PUBLISHED app URL, not the preview URL — ShipStation webhooks fired to a preview URL will fail after you publish.

```
// ShipStation SHIP_NOTIFY webhook payload (sample)
{
  "resource_url": "https://ssapi.shipstation.com/orders?orderStatus=shipped&modifyDateStart=2026-07-09T00:00:00",
  "resource_type": "SHIP_NOTIFY"
}

// To get the full shipment details, call the resource_url
// GET resource_url → returns updated order list with tracking numbers

// Bubble Backend Workflow endpoint URL format:
// https://YOUR_APP.bubbleapps.io/api/1.1/wf/shipstation-webhook

// ShipStation webhook event types:
// ORDER_NOTIFY — new order received
// ITEM_ORDER_NOTIFY — item ordered
// SHIP_NOTIFY — shipment created (most useful)
// ORDER_STATUS_CHANGE — order status updated
```

**Expected result:** The Backend Workflow endpoint is registered in ShipStation. Sending a test order through ShipStation triggers the webhook, and the Bubble Backend Workflow updates the matching order record with tracking information.

### 6. Test the Integration and Verify Logs

In Bubble's Logs tab (left sidebar), click 'Workflow logs' to monitor all API calls in real time. Test each flow:

1. Order display: Navigate to the page with the ShipStation orders Repeating Group. Verify orders appear with correct data. In the debugger, step through the workflow to confirm the API call returns a 200 response and Bubble correctly maps the response fields to the Data Type.

2. Rate shopping: On the checkout page, fill in a test shipping address and trigger the List Rates workflow. Verify the rate options appear in the UI. Check the Logs tab to confirm the POST /carriers/listrates returned a 200.

3. Webhook receipt: In ShipStation, use the 'Test Webhook' button in the Webhooks settings to send a test payload to your Bubble Backend Workflow endpoint. In Bubble's Logs → Server logs, verify the workflow executed and the data was processed.

If you see a 401 on any call, re-check the Base64 encoding of your credentials. Verify the string format is `apiKey:apiSecret` with a colon, encoded together. If the rate limit (40 req/min) is being hit during testing, add throttling or switch to caching fetched data in a Data Type rather than calling ShipStation on every trigger.

```
# Common ShipStation error responses

# 401 — wrong credentials (usually bad Base64 encoding)
{"ExceptionMessage": "Authorization has been denied for this request.",
 "ExceptionType": "System.UnauthorizedAccessException"}

# 429 — rate limit exceeded (40 req/min)
{"ExceptionMessage": "Too many requests",
 "ExceptionType": "System.Web.HttpException"}

# 404 — resource not found (wrong orderId or endpoint path)
{"ExceptionMessage": "The resource you requested was not found."}
```

**Expected result:** All three flows work: orders display correctly, rate shopping returns carrier options, and webhooks update order records. The integration is ready for a controlled production launch.

## Best practices

- Mark the Authorization header Private in the API Connector at the shared-header level. All ShipStation calls under this API group inherit the setting, so you only need to configure it once.
- ShipStation has no sandbox — never connect the label creation endpoint (POST /shipments/createlabel) to a workflow trigger that users can reach during development or testing. Accidental label creation charges real carrier fees.
- Cache ShipStation order data in a Bubble Data Type rather than calling GET /orders on every page load. The 40 req/min rate limit is shared across your account, and an order list rarely changes more than a few times per hour.
- Register webhook endpoints using your published Bubble app URL, never the preview URL. ShipStation webhooks sent to a preview URL will silently fail after you publish the app.
- Backend Workflows for webhook receipt require a paid Bubble plan. Without webhooks, your app must poll GET /orders to detect status changes — budget for polling frequency within the 40 req/min limit.
- Use GET /carriers at app initialization to dynamically populate carrier options rather than hardcoding carrier codes. Carrier availability changes when accounts are connected or disconnected in ShipStation.
- Set up Bubble Privacy rules on any Data Type that stores ShipStation order data (customer addresses, order totals). By default, all logged-in users in a Bubble app can query all records unless Privacy rules restrict access.
- In Bubble's Logs tab, monitor Server logs regularly during development to catch 401/429 errors before they surface in production. ShipStation's error messages include the HTTP status code and a brief explanation.

## Use cases

### Order Fulfillment Dashboard

Pull pending ShipStation orders into a Bubble admin panel where warehouse staff can view order details, customer information, and line items without accessing ShipStation directly. Filter by status (awaiting_shipment, on_hold, shipped) to create prioritized work queues.

Prompt example:

```
How do I build a Bubble Repeating Group showing ShipStation orders filtered by status 'awaiting_shipment', including customer name, order number, and total weight?
```

### Live Carrier Rate Shopping at Checkout

When a customer reaches the shipping step of a Bubble checkout flow, trigger a POST /carriers/listrates call with the package dimensions and destination. Display the carrier options and prices back to the customer in real time so they can choose their preferred service level.

Prompt example:

```
How do I call ShipStation's listrates endpoint from a Bubble workflow and display the returned carrier options in a dropdown for the customer to select?
```

### Real-Time Shipment Status Updates via Webhooks

Configure a Bubble Backend Workflow endpoint to receive ShipStation's SHIP_NOTIFY webhook. When a package ships, ShipStation calls the endpoint, and Bubble automatically updates the order record with the tracking number and carrier information, notifying the customer via email.

Prompt example:

```
How do I set up a Bubble Backend Workflow to receive ShipStation's SHIP_NOTIFY webhook and update the matching order record with the tracking number?
```

## Troubleshooting

### 401 Unauthorized on every API call even with credentials entered

Cause: The most common cause is incorrect Base64 encoding. The API key and secret must be encoded TOGETHER in the format `apiKey:apiSecret` — a colon separating them, then the entire string Base64-encoded. Encoding them separately and concatenating produces a different string.

Solution: In your browser console, run `btoa('YOUR_API_KEY:YOUR_API_SECRET')` with a colon between them. Copy the result and update the Authorization header value in the API Connector to `Basic [result]`. Make sure there is a space between 'Basic' and the encoded string.

```
// Correct
btoa('apiKey123:apiSecret456')
// Result: 'YXBpS2V5MTIzOmFwaVNlY3JldDQ1Ng=='
// Header: Basic YXBpS2V5MTIzOmFwaVNlY3JldDQ1Ng==

// Incorrect (separate encoding)
btoa('apiKey123') + ':' + btoa('apiSecret456') // WRONG
```

### Rate limit errors (429) appearing frequently during development

Cause: The 40 req/min rate limit is shared across your entire ShipStation account. Multiple team members making calls during development, or a Repeating Group that refreshes on every page interaction, can exhaust the limit quickly.

Solution: Cache fetched order data in a Bubble Data Type and refresh on a scheduled interval (every few minutes) rather than on every user action. During development, coordinate with team members to avoid simultaneous heavy testing. Use the 'pageSize=25' parameter on GET /orders to reduce response size and call frequency.

### ShipStation webhooks are not arriving at the Bubble Backend Workflow

Cause: Two common causes: (1) the webhook is registered with the Bubble preview URL instead of the published app URL, or (2) Backend Workflows are not enabled (requires a paid Bubble plan).

Solution: In ShipStation webhook settings, verify the registered URL matches your published Bubble app URL (`https://YOUR_APP.bubbleapps.io/api/1.1/wf/...`), not the preview URL. In Bubble Settings → API, confirm 'This app exposes a Workflow API' is enabled. If you are on the Free plan, upgrade to a paid plan to unlock Backend Workflows.

### 'There was an issue setting up your call' when initializing the List Rates POST call

Cause: POST calls in Bubble's API Connector need sample request body values that produce a real successful response for initialization. Using placeholder or empty values in the body causes ShipStation to return a 400 error, blocking initialization.

Solution: During initialization only, provide real values: use your actual warehouse postal code for `fromPostalCode`, a valid US destination zip code for `toPostalCode`, and a real carrier code (check GET /carriers to find your enabled carrier codes). Once initialized with a real response, replace the static values with Bubble dynamic field references.

### Label creation call is charging real money unexpectedly

Cause: POST /shipments/createlabel was triggered during testing or development. ShipStation has no sandbox — every label creation call immediately charges the connected carrier account.

Solution: Remove or disable any label creation calls during development. Use rate-shopping (POST /carriers/listrates) freely for testing the integration flow. Only wire the label creation call to a production-gated workflow trigger, and add a confirmation step before the user can trigger it.

## Frequently asked questions

### Is it safe to store the ShipStation API key and secret in Bubble's API Connector?

Yes — as long as the 'Private' checkbox is ticked on the Authorization header. Bubble's Private headers are stored encrypted server-side and are never transmitted to the browser or accessible through client-side workflows. This is the correct and secure approach for HTTP Basic auth credentials.

### Does ShipStation have a sandbox or test mode?

No. ShipStation does not offer a sandbox environment or test mode. Every API call hits your live ShipStation account. Rate quotes (POST /carriers/listrates) are safe to call freely during development. Label creation (POST /shipments/createlabel) immediately charges a real carrier — avoid this call until you are ready for production.

### Do I need a paid Bubble plan to use ShipStation webhooks?

Yes. Receiving ShipStation webhook events requires a Bubble Backend Workflow endpoint, which is only available on paid Bubble plans. On the Free plan, you can build read-only flows (order display, rate shopping) but cannot receive inbound webhook events. Without webhooks, you must poll GET /orders to detect status changes.

### Why is my Base64 Authorization header returning 401?

The most common mistake is encoding the API key and API secret separately instead of together. The correct format is `btoa('apiKey:apiSecret')` — the key and secret joined by a colon, encoded as a single string. The resulting value in the header should be `Basic [encoded_string]` with a space between 'Basic' and the encoded credentials.

### How do I display real-time shipment tracking in my Bubble app?

Set up a Backend Workflow endpoint that receives ShipStation's SHIP_NOTIFY webhook. When a package ships, ShipStation sends a notification to your endpoint containing a `resource_url`. Add a second API Connector call in the Backend Workflow that fetches this URL to get the full shipment details including tracking number. Store the tracking number in your ShipStationOrder Data Type and display it on the order detail page.

### What ShipStation plan do I need for API access?

API access is available on all paid ShipStation plans, starting with the Starter plan at $9.99/month (50 shipments). The plan limits apply to shipments processed, not API calls. If your app will process more than 50 shipments per month, plan accordingly — verify current pricing and limits at shipstation.com.

---

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