> ## Documentation Index
> Fetch the complete documentation index at: https://developers.cubbo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive signed HTTPS notifications when orders, shipments, inventory and replenishments change.

Cubbo sends an HTTPS `POST` to your endpoint when something changes on your account. Use webhooks instead
of polling list endpoints.

## Events

| Event | Feed | When it is sent |
| - | - | - |
| `orderUpdated` | Orders | The order is created or changes status: processing, hold and unhold, picking, cancelation, returns. |
| `orderFulfilled` | Orders | Labels are generated and the order leaves the warehouse. |
| `shippingUpdated` | Orders | The carrier reports a new tracking status for a package. Multi-package orders send one event per package. |
| `inventoryMovement` | Inventory | Available stock changes for one or more products. The payload is a snapshot of current stock, not a delta. |
| `replenishmentCreated` | Replenishments | A replenishment is created. |
| `replenishmentScheduled` | Replenishments | A receiving appointment for a replenishment is booked or canceled. |
| `replenishmentStatus` | Replenishments | A replenishment moves to processing, putaway, completed or canceled. |

The event name travels in the `X-Webhook-Event` header. Route on that header, not on the URL. The full payload
of each event is in the API reference under **Webhooks**.

> [!NOTE]
>
> `shippingUpdated` does not include the Cubbo order `id`. Correlate it with `order_number` and `store_id`.

## Set up your endpoint

Your endpoint must use `https` and a domain name. IP addresses are rejected.

Configure it in the Cubbo app: open **Integrations**, find your Open API integration and click
**Configure Webhooks**. There you turn on the orders, inventory and inventory replenishment webhooks, set their
URLs, add custom headers, test the URL and copy the shared secret. See [Set up your integration](https://developers.cubbo.com/guides/setup.md#4-configure-webhooks).

If your endpoint requires OAuth client credentials, ask your Cubbo contact to configure them.

The inventory feed only sends while the orders feed is enabled. The replenishments feed is independent.

### Connectivity test

When Cubbo checks your URL it sends this body, without signature headers and without `X-Webhook-Event`:

```json theme={"dark"}
{ "test": "cubbo_open_api_integration" }
```

Answer `2xx` within 10 seconds. Accept this ping even though it is unsigned, or the check fails.

## Headers

| Header | Value |
| - | - |
| `Content-Type` | `application/json` |
| `X-Webhook-Event` | The event name. |
| `X-Cubbo-Webhook-Message-Id` | UUID of this delivery attempt. It changes on every retry. |
| `isa` | Issued-at time, Unix seconds. |
| `exp` | Expiration time, Unix seconds. Always `isa` plus 300. |
| `X-Cubbo-Hmac-SHA256` | Signature, 64 lowercase hex characters. |
| `X-Cubbo-Idempotency-Key` | Stable key for deduplication. Sent on `shippingUpdated`, `inventoryMovement` and replenishment events. |
| `Authorization` | `Bearer <token>`, only when OAuth is configured for your endpoint. |

Static headers configured for your integration are added as well.

## Verify the signature

Every event is signed with HMAC-SHA256 using your webhook secret. Verify it before you trust the body.

**1. Read the raw body**

Use the exact bytes you received. Do not parse and re-serialize the JSON: key order and spacing must match.

**2. Build the signed string**

Join four values with a literal dot: message ID, `isa`, `exp` and the raw body.

```text theme={"dark"}
<X-Cubbo-Webhook-Message-Id>.<isa>.<exp>.<raw body>
```

**3. Compute and compare**

Compute the HMAC-SHA256 of that string. Use the secret as a plain UTF-8 string, without decoding it.
Compare the lowercase hex digest with `X-Cubbo-Hmac-SHA256` in constant time.

**4. Check expiration**

Reject the request if the current time is past `exp`. Allow a few seconds of clock skew.

```js Node.js theme={"dark"}
const crypto = require("crypto");

function verifyCubboWebhook(rawBody, headers, secret) {
  const msgId = headers["x-cubbo-webhook-message-id"];
  const isa = headers["isa"];
  const exp = headers["exp"];
  const given = headers["x-cubbo-hmac-sha256"] || "";

  if (Math.floor(Date.now() / 1000) > Number(exp) + 30) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${msgId}.${isa}.${exp}.${rawBody}`, "utf8")
    .digest("hex");

  return given.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(given));
}
```

```python Python theme={"dark"}
import hashlib, hmac, time

def verify_cubbo_webhook(raw_body: bytes, headers, secret: str) -> bool:
    msg_id = headers["X-Cubbo-Webhook-Message-Id"]
    isa = headers["isa"]
    exp = headers["exp"]
    given = headers.get("X-Cubbo-Hmac-SHA256", "")

    if time.time() > int(exp) + 30:
        return False

    signed = f"{msg_id}.{isa}.{exp}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, given)
```

All feeds of one integration use the same secret. When Cubbo renews it, the new secret applies to every
delivery from that moment, including retries of older events.

## Respond and retry

Answer with any `2xx` status as soon as you have stored the event, then process it asynchronously. Slow
responses risk a timeout and a retry.

| Your response | What Cubbo does |
| - | - |
| `2xx` | Marks the event as delivered. |
| `429`, `5xx`, timeout or network error | Retries with exponential backoff. |
| Any other `4xx` | Does not retry. The event is not sent again. |

| Feed | Retries | Retry window |
| - | - | - |
| Orders | Up to 14 | About 26 hours |
| Inventory and replenishments | Up to 5 | About 10 minutes |

Inventory and replenishment events are delivered one at a time per integration, in order. A failing delivery
holds back the events behind it until it succeeds or runs out of retries.

> [!WARNING]
>
> Do not answer `400` or `401` while your endpoint is being deployed or misconfigured. Cubbo treats those as
> final and drops the event. Answer `503` instead so the event is retried.

## Handle duplicates

Retries resend the same body, but with a new message ID, `isa`, `exp` and signature. Never deduplicate on
`X-Cubbo-Webhook-Message-Id`.

| Event | Deduplicate on |
| - | - |
| `orderUpdated`, `orderFulfilled` | `id`, `status` and `updated_at` together. |
| `shippingUpdated` | `X-Cubbo-Idempotency-Key`. When it is absent, use `order_number` and `status`. |
| `inventoryMovement` | `X-Cubbo-Idempotency-Key`. Also skip any event whose `sequence` is not greater than the last one you processed for that store. |
| `replenishmentCreated`, `replenishmentScheduled`, `replenishmentStatus` | `X-Cubbo-Idempotency-Key`. |

Keep processed keys for at least 26 hours, the longest retry window. `sequence` values grow but can skip
numbers, so do not expect them to increase by one.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.