Connect Bubble to AfterShip using the API Connector with a single `aftership-api-key` header marked Private. This is the simplest shipping integration in Bubble — no OAuth, no Base64 encoding, no token refresh. Register tracking numbers via POST /trackings, display shipment status and checkpoint history via GET calls, and optionally receive real-time status updates in a Backend Workflow webhook. The free plan caps new tracking registrations at around 50 per month.
| Fact | Value |
|---|---|
| Tool | AfterShip |
| Category | Productivity |
| Method | Bubble API Connector |
| Difficulty | Beginner |
| Time required | 1–2 hours |
| Last updated | July 2026 |
The Easiest Shipping Integration for Your Bubble App
AfterShip aggregates parcel tracking from 1,100+ carriers into one API, making it the fastest way to add shipping visibility to a Bubble app. The auth model could not be simpler: one API key in one custom header, marked Private in Bubble's API Connector. No OAuth, no Base64, no token refresh.
The two core flows are: registering a tracking number when an order ships (POST /trackings — done once per shipment) and reading the current status and location history (GET /trackings — done whenever a customer or admin views the order). AfterShip normalizes carrier-specific status codes into consistent `tag` values: Pending, InfoReceived, InTransit, OutForDelivery, Delivered, Exception, and FailedAttempt.
The free plan caps new registrations at around 50 per month. GET requests against already-registered numbers are unlimited on all plans. Design your Bubble app to register tracking numbers only when orders ship — not on page load — to avoid burning the free tier on re-registrations.
Integration method
Bubble's API Connector runs all AfterShip calls server-side. Place your AfterShip API key in a single `aftership-api-key` header marked Private. One header authenticates every AfterShip call in the group.
Prerequisites
- An AfterShip account — free tier available at app.aftership.com
- Your AfterShip API key from the AfterShip dashboard (Settings → API Keys)
- Bubble app on any plan for GET and POST call flows; a paid Bubble plan for Backend Workflow webhook receipt
- Bubble's API Connector plugin installed (Plugins tab → Add plugins → search 'API Connector' → Install)
- At least one real tracking number to test with — AfterShip cannot return useful data for placeholder tracking numbers
Step-by-step guide
Get Your AfterShip API Key
Sign up or log in at app.aftership.com. In the left sidebar, click 'Settings', then click 'API Keys' in the settings navigation. If you have not yet created an API key, click 'Generate New API Key' (or 'Create API Key' depending on your AfterShip dashboard version). Give it a name like 'Bubble Integration'. Copy the API key string. It will look like a long alphanumeric string. Unlike some APIs, AfterShip does not separate a 'key ID' from a 'secret' — this single string is everything you need. Note your AfterShip plan. The free plan allows approximately 50 new tracking registrations per month. This refers to new POST /trackings calls — registering a tracking number for the first time. Status check calls (GET) against already-registered numbers are free and unlimited on all plans. If your Bubble app ships more than 50 orders per month, you will need the Essential plan or higher.
1# AfterShip API key location:2# app.aftership.com → Settings → API Keys → Create API Key34# API key format: a long alphanumeric string5# Example: at_ab1cd2ef3gh4ij5kl6mn7op8qr9st67# Base URL for all calls:8https://api.aftership.com/v4910# Plan limits (verify at aftership.com/pricing):11# Free: ~50 new trackings/month12# Essential: ~1,000 shipments/month13# Pro+: higher limitsPro tip: Keep this API key secure. Anyone with the key can register tracking numbers to your AfterShip account and consume your monthly quota. Treat it like a password.
Expected result: You have an AfterShip API key copied and saved. You know your current plan's monthly tracking registration limit.
Configure the API Connector with the AfterShip API Key Header
In your Bubble app, go to the Plugins tab in the left sidebar. Click the API Connector plugin (install it if needed: Plugins → Add plugins → search 'API Connector' → Install). Click 'Add another API'. Name it 'AfterShip'. Click 'Add shared headers / parameters'. Add a header with: - Key: `aftership-api-key` (lowercase, hyphenated — the exact header name AfterShip expects) - Value: your API key string - Tick the 'Private' checkbox The header name must be exactly `aftership-api-key`. Using `Authorization`, `x-api-key`, `api-key`, or any other variation returns a 401 with no indication of which header name AfterShip expects. Set the API root URL to `https://api.aftership.com/v4`. The 'Private' checkbox ensures this header value is never transmitted to or accessible from the browser. Because Bubble's API Connector runs server-side, CORS is also not a concern.
1{2 "api_name": "AfterShip",3 "root_url": "https://api.aftership.com/v4",4 "shared_headers": [5 {6 "key": "aftership-api-key",7 "value": "<private — your aftership api key>",8 "private": true9 }10 ]11}Pro tip: The header name is `aftership-api-key` in all lowercase with hyphens. A common typo is adding spaces or capitalizing 'AfterShip' in the header name. If you get a 401 after confirming the key value is correct, double-check the header name character by character.
Expected result: The AfterShip API group appears in the API Connector with the `aftership-api-key` Private header configured. You are ready to add individual call definitions.
Initialize the Connection and Set Up a Get Tracking Status Call
Add your first call inside the AfterShip API group. Click 'Add another call'. Name it 'Get Couriers'. Set Method to GET and path to `/couriers`. Click 'Initialize call'. If your API key is correct, you will receive a list of active couriers in your AfterShip account. This confirms the auth header is working. Now add the primary read call. Click 'Add another call'. Name it 'Get Tracking'. Set Method to GET and path to `/trackings/{slug}/{tracking_number}`. In the path: - `{slug}` is AfterShip's carrier code (e.g., 'usps', 'ups', 'fedex', 'dhl') — you can get valid slugs from GET /couriers - `{tracking_number}` is the actual parcel tracking number For the 'Use as' setting, select 'Data' — this call returns a single tracking object you can bind to page elements. For initialization, provide a real tracking number you have already registered in AfterShip (or register one first using the POST /trackings call in the next step, then come back to initialize this GET call). The response includes a `data.tracking` object with key fields: - `tag` (text) — normalized status: Pending, InfoReceived, InTransit, OutForDelivery, Delivered, Exception - `status_summary` (text) — human-readable status description - `checkpoints` (array) — full location and status history - `expected_delivery` (date) - `courier_tracking_link` (URL) — the carrier's native tracking page
1// GET /trackings/{slug}/{tracking_number}2// Example: GET /trackings/usps/940011189922393786109034// Response structure (data.tracking object)5{6 "data": {7 "tracking": {8 "id": "abcdef1234567890",9 "tracking_number": "9400111899223937861090",10 "slug": "usps",11 "tag": "InTransit",12 "status_summary": "Package in transit",13 "expected_delivery": "2026-07-11",14 "courier_tracking_link": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223937861090",15 "checkpoints": [16 {17 "created_at": "2026-07-09T18:30:00+00:00",18 "city": "Louisville",19 "state": "KY",20 "country_name": "United States",21 "message": "Arrived at Post Office",22 "tag": "InTransit"23 },24 {25 "created_at": "2026-07-09T10:00:00+00:00",26 "city": "Austin",27 "state": "TX",28 "country_name": "United States",29 "message": "Shipping Label Created",30 "tag": "InfoReceived"31 }32 ]33 }34 }35}Pro tip: The `tag` field is AfterShip's normalized status — use it in Bubble conditions to show different UI states (green badge for Delivered, amber for InTransit, red for Exception). The carrier's raw status message is in `checkpoints[].message` and may vary widely by carrier.
Expected result: The Get Couriers call confirms your API key works. The Get Tracking call initializes with a real tracking number and Bubble detects the tracking object fields including tag, status_summary, and the checkpoints array.
Register New Tracking Numbers via POST /trackings
Add a new call. Name it 'Create Tracking'. Set Method to POST and path to `/trackings`. Set 'Use as' to 'Action' — this is triggered by a workflow event (an order being marked as shipped), not a data source. Add a Content-Type header at the call level: `application/json`. In the body, construct a JSON object with the required fields: - `tracking_number` (string) — the parcel tracking number (required) - `slug` (string) — AfterShip's carrier code; optional if you want AfterShip to auto-detect the carrier - `title` (string) — optional; a label for this tracking (e.g., the order number) - `customer_name` (string) — optional; shown in AfterShip's dashboard - `customer_email` (string) — optional; AfterShip can send tracking notifications to this email Click 'Initialize call'. For the test, provide a real tracking number. AfterShip will register it and return the tracking object (same structure as the GET response, but with initial status 'Pending' or 'InfoReceived' since the carrier hasn't updated yet). In Bubble's workflow editor, wire the Create Tracking action to a workflow that runs when an order's status changes to 'Shipped'. Pass the order's tracking number field as the `tracking_number` parameter. Optionally pass the order number as `title` so you can identify the shipment in AfterShip's dashboard. AfterShip returns error code 4009 ('Tracking already exists') if you register the same tracking number twice. Handle this in your Bubble workflow: treat 4009 as a non-error and fetch the existing tracking via GET instead of treating it as a failure.
1// POST /trackings — request body2{3 "tracking": {4 "tracking_number": "<dynamic: tracking_number>",5 "slug": "<dynamic: carrier_slug_optional>",6 "title": "<dynamic: order_number>",7 "customer_name": "<dynamic: customer_name>",8 "customer_email": "<dynamic: customer_email>"9 }10}1112// Success response (201 Created)13{14 "meta": { "code": 201, "message": "Created" },15 "data": {16 "tracking": {17 "id": "newtrackingid123",18 "tracking_number": "9400111899223937861090",19 "slug": "usps",20 "tag": "Pending",21 "created_at": "2026-07-09T20:00:00+00:00"22 }23 }24}2526// Error: tracking already registered27{28 "meta": { "code": 4009, "message": "Tracking already exists." }29}Pro tip: If you leave `slug` empty, AfterShip attempts to auto-detect the carrier from the tracking number format. This works reliably for major carriers (USPS, UPS, FedEx) but may produce wrong results for regional or international carriers. Passing the slug explicitly is more reliable.
Expected result: Triggering the Create Tracking workflow action from a Bubble 'Order Shipped' event registers the tracking number with AfterShip. AfterShip begins polling the carrier for status updates. The tracking appears in AfterShip's dashboard with initial status 'Pending' or 'InfoReceived'.
Display Shipment Checkpoints in a Repeating Group
The `checkpoints` array in the AfterShip tracking response contains the full shipment history — every location scan and status update from the carrier. To display this in Bubble, set up a Repeating Group that shows each checkpoint entry. In Bubble's design editor, add a Repeating Group. Set the 'Type of content' to the 'Get Tracking' API call result. Bubble will have detected the `checkpoints` array during initialization — set the data source to use the checkpoints from the current page's tracking data. Inside the Repeating Group, add elements for: - Checkpoint date and time (from `created_at` — format it using Bubble's date formatting to '10:30 AM, July 9, 2026') - Location (combine `city`, `state`, `country_name`) - Status message (from `message`) - A status indicator dot colored by the `tag` field (green for Delivered, amber for InTransit, grey for InfoReceived, red for Exception) For the main tracking status at the top of the page (outside the Repeating Group), bind text elements directly to the `tag` and `status_summary` fields of the tracking response. Use Bubble's conditional formatting to show different colors or icons based on the `tag` value. This setup gives you a branded tracking page that is functionally equivalent to what most e-commerce platforms display — without requiring any carrier-specific integration logic.
1// AfterShip status tags and their meanings:2// Pending → tracking registered, no carrier update yet3// InfoReceived → carrier received shipment info4// InTransit → package is moving between facilities5// OutForDelivery → package is on delivery vehicle today6// Delivered → package delivered7// FailedAttempt → delivery attempted, no one home8// Exception → delay, damage, or other issue9// Expired → carrier has stopped updating (90+ days)1011// Bubble conditional formatting example:12// When tag = 'Delivered' → background green13// When tag = 'Exception' → background red14// When tag contains 'InTransit' OR 'OutForDelivery' → background amberPro tip: Use the `tag` field for conditional formatting logic in Bubble — it is normalized across all carriers. The `message` field contains raw carrier text that varies in format and language by carrier. Always use `tag` for programmatic decisions and `message` for display text.
Expected result: A Bubble Repeating Group shows the shipment checkpoint history with dates, locations, and status messages. The main tracking status at the top of the page shows the current tag with color-coded formatting.
Set Up a Backend Workflow for AfterShip Webhook Notifications
AfterShip can push real-time tracking updates to your Bubble app via webhooks, eliminating the need to poll GET /trackings on a schedule. When a package status changes, AfterShip sends a POST request to your Bubble Backend Workflow endpoint. In your Bubble app, go to Settings → API → enable 'This app exposes a Workflow API'. Go to Backend Workflows → New API Workflow. Name it 'AfterShip Webhook'. Under 'Detect request data', click the option to detect parameters from an incoming request. Your Backend Workflow public URL will be: `https://YOUR_APP_NAME.bubbleapps.io/api/1.1/wf/aftership-webhook`. In AfterShip's dashboard, go to Settings → Notifications → Webhooks. Click 'Add Webhook'. Enter your Bubble Backend Workflow URL. Select the event types: 'Tracking update' covers all status changes. Save and use AfterShip's 'Send test notification' button to fire a test payload. In the Backend Workflow, after detecting the request data, add an action: find the matching Bubble order record by the incoming `tracking_number` field, then update the order's status and tracking tag fields with the values from the webhook payload. RapidDev has built webhook-driven tracking systems in Bubble for e-commerce clients with hundreds of daily shipments. If you need help with the webhook data mapping or order status sync logic, book a free scoping call at rapidevelopers.com/contact. Note: Backend Workflows are unavailable on Bubble's Free plan. Without webhooks, poll GET /trackings periodically using 'Recurring events' — but stay mindful of the AfterShip rate limit of approximately 10 requests per second on the free plan.
1// AfterShip webhook payload (tracking update event)2{3 "event": "tracking_update",4 "msg": {5 "id": "abcdef1234567890",6 "tracking_number": "9400111899223937861090",7 "slug": "usps",8 "tag": "Delivered",9 "status_summary": "Delivered, Front Door/Porch",10 "expected_delivery": null,11 "checkpoints": [12 {13 "created_at": "2026-07-11T14:22:00+00:00",14 "city": "Beverly Hills",15 "state": "CA",16 "country_name": "United States",17 "message": "Delivered, Front Door/Porch",18 "tag": "Delivered"19 }20 ]21 }22}2324// Bubble Backend Workflow endpoint:25// https://YOUR_APP.bubbleapps.io/api/1.1/wf/aftership-webhookPro tip: Register your PUBLISHED Bubble app URL as the AfterShip webhook target, not the preview URL. Webhooks sent to a preview URL will fail silently after you publish the app to production.
Expected result: AfterShip sends webhook events to the Bubble Backend Workflow endpoint when a tracked shipment's status changes. The Bubble Backend Workflow updates the matching order record with the new status tag and checkpoint data.
Common use cases
Order Tracking Page for Customers
Build a branded order tracking page inside your Bubble app where customers enter their tracking number or see their shipment status automatically. AfterShip returns the current status tag, carrier name, and a full checkpoint history — all displayed in a clean Repeating Group.
How do I build a Bubble tracking page that shows a customer their shipment status, current location, and delivery history from AfterShip using the GET /trackings endpoint?
Copy this prompt to try it in Bubble
Admin Shipment Dashboard
Display all registered trackings in a Bubble admin panel with status filtering. Show which orders are in transit, out for delivery, delivered, or experiencing exceptions. Trigger alerts when the status tag changes to 'Exception' or 'FailedAttempt'.
How do I fetch all AfterShip trackings and display them in a Bubble Repeating Group filtered by status tag (InTransit, Delivered, Exception)?
Copy this prompt to try it in Bubble
Automatic Tracking Registration on Order Fulfillment
When a staff member marks an order as shipped in Bubble and enters the tracking number, a workflow automatically calls POST /trackings to register it with AfterShip. From that point, AfterShip monitors the carrier and the app shows live status without any additional manual steps.
How do I trigger a POST /trackings call to AfterShip automatically when a Bubble workflow sets an order's status to 'Shipped' and saves the tracking number?
Copy this prompt to try it in Bubble
Troubleshooting
401 Unauthorized on every API call
Cause: The most common cause is a wrong header name. AfterShip requires the header name `aftership-api-key` (all lowercase, hyphenated). Using `Authorization`, `x-api-key`, `API-Key`, or any other variation returns 401 with no indication of which header name is expected.
Solution: In the API Connector, verify the shared header key is exactly `aftership-api-key` — all lowercase, with hyphens. Check for any leading or trailing spaces in the value field. If the key value itself might be wrong, regenerate it in AfterShip Settings → API Keys.
POST /trackings returns error code 4009 'Tracking already exists'
Cause: The same tracking number was registered with AfterShip before. AfterShip does not allow duplicate tracking registrations.
Solution: This is not a failure — the tracking is already in AfterShip and monitoring the package. In your Bubble workflow, add a condition that checks if the result code is 4009 and, if so, fetches the existing tracking via GET /trackings/{slug}/{tracking_number} instead of treating it as an error. For idempotent order fulfillment workflows, always check for 4009 and handle gracefully.
GET /trackings returns 'Pending' status even after the package has shipped
Cause: AfterShip polls carriers on a schedule — it does not receive real-time pushes from all carriers. For some carriers, it can take a few hours after registration for the first carrier scan to appear. 'Pending' means AfterShip has registered the number but has not yet received carrier data.
Solution: Wait 2–4 hours after registration for most major carriers to update. If the package is still showing Pending after 24 hours, verify the tracking number is correct and the carrier slug matches the carrier actually handling the package. Some regional carriers have delayed polling schedules.
'There was an issue setting up your call' when initializing the GET /trackings call
Cause: The initialize call needs a tracking number that is already registered in AfterShip and has received at least one status update. Using an unregistered or all-Pending tracking number may return an insufficient response for Bubble to detect the full schema.
Solution: Register a real tracking number using POST /trackings first. Wait for AfterShip to receive at least one carrier status update (InTransit or later). Then return to the API Connector and initialize the GET /trackings call with that number — the response will include the full checkpoints array that Bubble needs to detect.
Free plan 50-tracking limit is exhausted before end of month
Cause: The app is registering tracking numbers on GET requests or page loads rather than only when an order ships. Or, tracking numbers are being re-registered on every order status change rather than once per shipment.
Solution: Move the POST /trackings call to a workflow that triggers only once — when an order status changes to 'Shipped' for the first time. Add a condition checking that the order does not already have an AfterShip tracking ID before calling POST. Once registered, use only GET calls to check status updates.
Best practices
- Mark the `aftership-api-key` header Private in the API Connector. This single configuration ensures the key is never exposed to the browser across all AfterShip calls in the group.
- Register tracking numbers with POST /trackings only once per shipment — typically when an order status changes to 'Shipped'. Avoid re-registering on page loads or status checks, which wastes your monthly tracking registration quota.
- Handle error code 4009 ('Tracking already exists') gracefully in your Bubble workflows. Treat it as a successful registration and fetch the existing tracking record rather than surfacing an error to the user.
- Use AfterShip's normalized `tag` field for programmatic logic in Bubble (conditional formatting, status filtering, notification triggers). Use the `message` field from checkpoints for display text only — it varies in format and language by carrier.
- Set up a Backend Workflow webhook endpoint for real-time tracking updates instead of polling GET /trackings repeatedly. Webhooks are more efficient, real-time, and well within AfterShip's rate limits compared to scheduled polling.
- On the free plan, design your app to assume GET calls are unlimited but POST calls are capped. Build a counter or use AfterShip's dashboard to monitor monthly registration usage before approaching the limit.
- Apply Bubble Privacy rules to any Data Type storing tracking data. Tracking information (addresses, delivery status) is tied to customer orders — restrict read access to the customer who placed the order and admins only.
- For the AfterShip webhook endpoint, register your published Bubble app URL, not the preview URL. Preview URLs change behavior after publication and will miss webhook events.
Alternatives
ShipStation is a full fulfillment platform — create labels, shop rates, manage orders across carriers. AfterShip is read-only post-shipment tracking. Choose ShipStation if you need to create shipping labels or manage orders; choose AfterShip if you only need to display tracking status to customers.
The UPS API provides direct UPS-only tracking and rating with OAuth 2.0 (significantly more complex to configure in Bubble). AfterShip covers UPS plus 1,100+ other carriers with one simple API key. Choose UPS API directly only for custom rate negotiation or UPS-specific features not available through AfterShip.
Shippo provides both label creation (with a test sandbox) and tracking across multiple carriers. AfterShip focuses purely on post-shipment tracking. Choose Shippo if your Bubble app needs to create labels and track shipments within one platform.
Frequently asked questions
Does AfterShip have a free plan?
Yes. AfterShip's free plan allows approximately 50 new tracking registrations per month. Status check calls (GET) against already-registered numbers are unlimited on all plans. If your app ships more than 50 orders per month, you will need the Essential plan or higher. Verify current pricing and limits at aftership.com/pricing.
What is the difference between registering a tracking number and checking its status?
Registering a tracking number (POST /trackings) is what consumes your monthly quota — you do this once when an order ships. Checking status (GET /trackings/{slug}/{number}) is unlimited on all plans. Design your Bubble app to POST only once per shipment and GET as often as needed for status display.
Do I need to know the carrier to register a tracking number?
No. You can leave the `slug` (carrier code) empty in the POST /trackings body, and AfterShip will attempt to auto-detect the carrier from the tracking number format. This works reliably for major carriers (USPS, UPS, FedEx, DHL) but may be less accurate for regional or international carriers. Passing the slug explicitly gives more reliable results.
Why does the tracking status show 'Pending' for a long time?
AfterShip polls carriers on a schedule rather than receiving instant pushes from all carriers. It typically takes 2–6 hours after label creation for the first carrier scan to appear, and some regional carriers update less frequently. 'Pending' is normal for recently registered tracking numbers. If a tracking number stays in 'Pending' for more than 24 hours, verify the tracking number is valid and the carrier slug is correct.
Can I use AfterShip webhooks without a paid Bubble plan?
No. Receiving AfterShip webhooks requires a Bubble Backend Workflow endpoint, which is only available on paid Bubble plans. On the Free plan, you can build tracking display using GET calls, but you cannot receive inbound webhook events. As an alternative on Free, use a scheduled recurring workflow that periodically fetches tracking status — but stay under AfterShip's rate limit of approximately 10 requests per second.
What does the AfterShip error code 4009 mean?
Error 4009 means 'Tracking already exists' — the same tracking number has already been registered with AfterShip under your account. This is not a failure. Handle it in your Bubble workflow by treating 4009 as a success and fetching the existing tracking record with GET /trackings instead of surfacing an error to the user.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation