> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developer.shipbob.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developer.shipbob.com/_mcp/server.

# Webhooks

ShipBob webhooks automatically notify your app when key events occur, allowing you to keep your data in sync.

## Getting Started

Create a [webhook](/api/webhooks/create-subscription) to subscribe to an event and define a **subscription URL**.

When the event occurs, ShipBob sends a `POST` request to your **subscription URL** with relevant data.

Your system should return a `2XX` response to confirm successful receipt.

If no `2XX` response is received, ShipBob **retries delivery** using an **exponential backoff strategy**.

***

## Key Concepts

* **Subscription**: A request to receive webhook notifications at a specified URL.
* **Subscription URL**: The endpoint where webhook data is sent.
* **Event**: A trigger that generates webhook data (e.g., an order being shipped).
* **Topic**: The category of event data being sent.
* **Payload**: The actual event data sent in the webhook.
* **Response**: A `2XX` HTTP response is required to confirm webhook receipt.

***

## Common Use Cases

* ✅ **Real-time Order Tracking** - Use `order.shipped` for first tracking availability and `order.shipment.tracking.updated` for downstream carrier status changes.
* ✅ **Shipment Mutation Sync** - Use `order.shipment.address.updated`, `order.shipment.line_item.added`, `order.shipment.line_item.removed`, and `order.shipment.line_item.updated` to keep shipment data in sync after creation.
* ✅ **Service-Level Change Detection** - Monitor `order.shipment.shipping_service.updated` to track shipping service changes and rollback events.
* ✅ **Inventory Risk Monitoring** - Monitor `order.shipment.exception` events to detect and respond to stock shortages.
* ✅ **Receiving Workflow Automation** - Use `wro.created`, `wro.updated`, `wro.completed`, `wro.box.arrived`, `wro.box.scanned`, and `wro.box.stowed` to automate inbound inventory workflows.
* ✅ **Return Lifecycle Notifications** - Use `return.created`, `return.updated`, and `return.completed` to keep your returns state synchronized.
* ✅ **Billing Event Reconciliation** - Use `billing.charge.created`, `billing.refund.created`, and `billing.credit.created` to reconcile invoices and credits in near real time.

***

## Webhook Topics & Events

| **Topics**                                | **Description**                                                                                                                                                                                     | **Scopes Required**                  |
| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `order.shipped`                           | Fires when a shipping label is purchased and printed. If the order is split into multiple shipments, this fires when all the labels in the shipments have been printed.                             | `orders_read`                        |
| `order.shipment.tracking.updated`         | Fires when a shipment tracking status is updated.                                                                                                                                                   | `orders_read` or `fulfillments_read` |
| `order.shipment.delivered`                | Fires when a shipment is delivered to the customer.                                                                                                                                                 | `orders_read` or `fulfillments_read` |
| `order.shipment.exception`                | Fires when a shipment is moved to exception status (e.g., out-of-stock items).                                                                                                                      | `orders_read` or `fulfillments_read` |
| `order.shipment.on_hold`                  | Fires when a shipment moves to **On-Hold** status due to missing information (e.g., address issue, payment issue).                                                                                  | `orders_read` or `fulfillments_read` |
| `order.shipment.cancelled`                | Fires when a shipment is canceled. Does **not** fire for orders manually created on the dashboard.                                                                                                  | `orders_read` or `fulfillments_read` |
| `order.shipment.address.updated`          | Fires when the shipping address on an existing shipment is changed. Suppressed when the write would not change any address field.                                                                   | `orders_read` or `fulfillments_read` |
| `order.shipment.line_item.added`          | Fires when a new line item is added to an existing shipment.                                                                                                                                        | `orders_read` or `fulfillments_read` |
| `order.shipment.line_item.removed`        | Fires when an existing line item is removed from a shipment.                                                                                                                                        | `orders_read` or `fulfillments_read` |
| `order.shipment.line_item.updated`        | Fires when an existing line item on a shipment has its quantity changed.                                                                                                                            | `orders_read` or `fulfillments_read` |
| `order.shipment.shipping_service.updated` | Fires when the shipping service on an existing shipment is changed, including when a ShipBob-paid upgrade is rolled back to the original service. Does not fire for ShipBob-paid upgrade additions. | `orders_read` or `fulfillments_read` |
| `return.created`                          | Fires when a return is created.                                                                                                                                                                     | `returns_read`                       |
| `return.updated`                          | Fires when a return is updated.                                                                                                                                                                     | `returns_read`                       |
| `return.completed`                        | Fires when a return is completed.                                                                                                                                                                   | `returns_read`                       |
| `wro.created`                             | Fires when a Warehouse Receiving Order (WRO) has been created.                                                                                                                                      | `receiving_read`                     |
| `wro.updated`                             | Fires when a Warehouse Receiving Order (WRO) is updated, such as status change or modifications to shipping details.                                                                                | `receiving_read`                     |
| `wro.completed`                           | Fires when a Warehouse Receiving Order (WRO) is completed.                                                                                                                                          | `receiving_read`                     |
| `wro.box.arrived`                         | Fires when a WRO box arrives at a ShipBob fulfillment center.                                                                                                                                       | `receiving_read`                     |
| `wro.box.scanned`                         | Fires when a WRO box is scanned at a ShipBob fulfillment center.                                                                                                                                    | `receiving_read`                     |
| `wro.box.stowed`                          | Fires when a WRO box is stowed into inventory at a ShipBob fulfillment center.                                                                                                                      | `receiving_read`                     |
| `billing.charge.created`                  | Fires when a billing charge is recorded.                                                                                                                                                            | `billing_read`                       |
| `billing.refund.created`                  | Fires when a previously recorded charge is refunded.                                                                                                                                                | `billing_read`                       |
| `billing.credit.created`                  | Fires when a credit is applied to a merchant's account.                                                                                                                                             | `billing_read`                       |

| **Topic (1.0, 2.0)** | **Description**                                                                                                                                                         | **Scopes Required**                  |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `order_shipped`      | Fires when a shipping label is purchased and printed. If the order is split into multiple shipments, this fires when all the labels in the shipments have been printed. | `orders_read`                        |
| `shipment_delivered` | Fires when a shipment is delivered to the customer.                                                                                                                     | `orders_read` or `fulfillments_read` |
| `shipment_exception` | Fires when a shipment is moved to exception status (e.g., out-of-stock items).                                                                                          | `orders_read` or `fulfillments_read` |
| `shipment_onhold`    | Fires when a shipment moves to **On-Hold** status due to missing information (e.g., address issue, payment issue).                                                      | `orders_read` or `fulfillments_read` |
| `shipment_cancelled` | Fires when a shipment is canceled. Does **not** fire for orders manually created on the dashboard.                                                                      | `orders_read` or `fulfillments_read` |

***

## Webhook Headers

ShipBob sends webhook notifications with the following headers:

```json
{
  "x-webhook-topic": "order.shipment.on_hold",
  "webhook-timestamp": "1755884852",
  "webhook-signature": "v1,5BO0uMAu1XYGkAJOpZb0Piel11YaChVEZCpXY6mwUMA",
  "webhook-id": "msg_31eW7bWRi16lL0Ajjik68kfyeUh",
  "content-type": "application/json"
}
```

```json
{
  "shipbob-subscription-id": "1582",
  "shipbob-topic": "order_shipped",
  "content-type": "application/json"
}
```

## Verifying Signatures

Each webhook call includes three headers used for verification:

* **webhook-id**: The unique message identifier for the webhook message. This identifier is unique across all messages, but will be the same when the same webhook is being resent (e.g. due to a previous failure)
* **webhook-timestamp**: Unix timestamp in seconds (seconds since epoch).
* **webhook-signature**: A space-delimited list of signatures, where each signature value is base64-encoded and may include a version prefix.

### Constructing the signed content

The content to sign is composed by concatenating the id, timestamp and payload, separated by the full-stop character `(.)`. In code, it will look something like:

```js
const signedContent = `${webhook_id}.${webhook_timestamp}.${body}`;
```

Where `body` is the raw request body. The signature is sensitive to any changes, so even a small change in the body will cause the signature to be completely different. This means that you should not change the body in any way before verifying.

### Determining the expected signature

ShipBob uses HMAC with SHA-256 to sign the webhooks.

To calculate the expected signature, you should HMAC the `signed_content` from above using the base64-decoded bytes of your signing secret as the key. Specifically, take the portion after the `whsec_` prefix, base64-decode it, and use the decoded bytes as the HMAC key. For example, given the secret `whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw`, you would base64-decode `MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw` and use the resulting bytes as the key.

For example, this is how you can decode the secret and calculate the signature in Node.js:

```js
const crypto = require('crypto');

const signedContent = `${webhook_id}.${webhook_timestamp}.${body}`;
const secret = "whsec_5WbX5kEWLlfzsGNjH64I8lOOqUB6e8FH";

// Take the portion after `whsec_`, base64-decode it, and use the resulting bytes as the HMAC key.
const decodedSecret = Buffer.from(secret.split('_')[1], "base64");

const signature = crypto
  .createHmac('sha256', decodedSecret)
  .update(signedContent)
  .digest('base64');

console.log(signature);

```

This generated signature should match one of the signatures sent in the `webhook-signature` header.

### Understanding the webhook-signature header

The `webhook-signature` header is composed of a list of space-delimited signatures and their corresponding version identifiers. The signature list is most commonly of length one, though there could be any number of signatures. For example:

```
v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=
```

Be sure to remove the version prefix (e.g. `v1,`) before verifying the signature.

Security Note: It is recommended to use a constant-time string comparison method in order to prevent timing attacks.

***

## Webhook Payloads

✨ **The best way to see examples of webhook payloads** is to create a webhook in the **ShipBob Dashboard** by going to:\
**Integrations → Webhooks → Create new subscription**.

1. **Add your subscription URL** — this is the endpoint on your server where ShipBob will send the webhook data.
2. **Select a topic** — choose an event type such as `order.shipped` or `return.completed`.
3. **Click “Send example”** — ShipBob will immediately send a **sample JSON payload** to your URL so you can preview the structure.

This approach makes it easy to **test your integration**, **validate your endpoint**, and **understand the exact payload format** without waiting for a real event to occur.

<img src="https://files.buildwithfern.com/ship.docs.buildwithfern.com/c9e80192f6845af8149f34a3a8675afdd0ef03b03a1e28d0844e69f05c3a1f3f/docs/assets/images/test-webhook-payloads.png" alt="Webhooks payloads" title="Webhooks payloads" />

***

## Retry Schedule

Each message is attempted based on the following schedule, where each period is started following the failure of the preceding attempt: 

* Immediately 
* 5 seconds 
* 5 minutes 
* 30 minutes 
* 2 hours 
* 5 hours 
* 10 hours 
* 10 hours (in addition to the previous) 

If an endpoint is removed or disabled delivery attempts to the endpoint will be disabled as well. 

For example, an attempt that fails three times before eventually succeeding will be delivered roughly 35 minutes and 5 seconds following the first attempt. 

**Indicating successful delivery** 

The way to indicate that a webhook has been processed is by returning a 2xx (status code 200-299) response to the webhook message within a reasonable time-frame (15s). Any other status code, including 3xx redirects are treated as failures. 

**Failed delivery handling** 

After the conclusion of the above attempts the message will be marked as Failed for this endpoint, and the webhook sender's account will get email notification for notifying them of this error. 

**Manual retries** 

You can also use the application portal to manually retry each message at any time, or automatically retry ("Recover") all failed messages starting from a given date.

<img src="https://files.buildwithfern.com/ship.docs.buildwithfern.com/025fb90ef7ee4b9a69f70a93b567fe486c745296dd1b4f6e3ed3a4a5ca409753/docs/assets/images/webhooks-manual-retries.png" alt="Webhooks Manual Retries Pn" title="Webhooks Manual Retries Pn" />

## Disabling failing endpoints

If all attempts to a specific endpoint fail for a period of 5 days, the endpoint will be disabled and an email will be sent to the account owner. The clock only starts after multiple deliveries fail within a 24-hour span, with at least 12 hours difference between the first and the last failure.

***

## Static Source IP Addresses

In case your webhook receiving endpoint is behind a firewall or NAT, you may need to allow traffic from static IP addresses.

This is the full list of IP addresses that webhooks may originate from.

```
44.228.126.217
50.112.21.217
52.24.126.164
54.148.139.208
2600:1f24:64:8000::/56
```

***

## Best Practices

✅ **Use HTTPS** - Subscription URLs **must** support SSL. Use RequestBin for testing if needed.

✅ **Implement Redundancy** - Webhooks **may be delayed or lost**. Use `GET` endpoints to periodically reconcile data.

✅ **Retry Handling** - Events **may arrive out of order** due to retries—handle them as independent updates.

✅ **Use Idempotency** - Store webhook event `id`s and discard duplicates to prevent redundant processing.

✅ **Logging & Monitoring** - Log webhook requests and responses to diagnose issues.

***

## Troubleshooting Guide

Yes, you can view webhook logs in the ShipBob dashboard by going to **Integrations** > **Webhooks**. Then, click into your webhook and you will be able to see logs at the bottom of the page.

![Tracking received webhook](https://files.buildwithfern.com/ship.docs.buildwithfern.com/e4e2dfbaf7cdefae480963e05e60858fb1d2596b97df21c8775f343c089ada02/docs/assets/images/webhook-tracking-received.png)

* Ensure your **subscription URL is correct** and **publicly accessible**.
* Confirm your app **`returns a 2XX response`** to ShipBob’s `POST` request.
* Verify that your app **`has the correct webhooks_read or webhooks_write permissions`**.

- Ensure your system **processes events efficiently** and responds within **5 seconds**.
- Return a `2XX` response **before** doing heavy processing.

* Use timestamps from the payload to **sort events properly**.
* Handle events **individually**, rather than relying on strict order.

- Webhooks **are not guaranteed to be sent only once**.
- Implement **idempotency checks**: Store received event IDs and ignore duplicates.