# Guide to Sendcloud Webhooks: Features and Best Practices

Sendcloud is a shipping platform for e-commerce. It creates labels across many carriers, tracks parcels and handles returns. Once a parcel is on its way, the useful information is in its status updates. Sendcloud webhooks push those updates to your application, so your order system stays current without polling the API.

This guide covers how Sendcloud webhooks are configured on an integration, how to verify the `Sendcloud-Signature` header, the five events and their payloads, Sendcloud's retry behaviour, and the best practices for running a Sendcloud receiver in production.

## What are Sendcloud webhooks?

A Sendcloud webhook is a URL set on one integration, typically your Sendcloud API integration. Sendcloud sends a JSON body to that URL when something happens to the integration itself, when a parcel created through it changes status, or when a return is created from one of its parcels.

Every request carries a `Sendcloud-Signature` header: the hex HMAC-SHA256 of the raw body, keyed with your integration's secret. The body's `action` field names the event.

Webhooks belong to the integration. You only receive events for parcels created through that integration and actions that belong to it, so a second integration needs its own webhook configuration.

## Sendcloud webhook features

| Feature | Details |
| --- | --- |
| Configuration | Settings > Integrations > Configure, "Webhook feedback enabled" and "Webhook url" |
| Scope | One integration; only its parcels and actions |
| Event field | `action` in the body |
| Signature header | `Sendcloud-Signature` |
| Scheme | HMAC-SHA256, lowercase hex |
| Signed content | The entire raw request body |
| Secret | The integration's Secret Key or Webhook Signature Key, depending on integration type |
| Replay window | None. The body has a `timestamp`, but it is not a separate signed header |
| Retries | Up to 10, exponential backoff from 5 minutes, capped at 1 hour between attempts |
| After retries | Failure logged in the connection issue log |
| Ordering | Not guaranteed; use `timestamp` |
| Testing | "Test API webhook" button |
| Source IPs | Not published |

## Common events

Sendcloud documents five events.

| `action` | Fires when |
| --- | --- |
| `parcel_status_changed` | A parcel created through this integration moves to a different status |
| `return_created` | A return is created from a parcel matching the integration |
| `integration_connected` | The integration is created |
| `integration_updated` | The integration is updated (shown as "Integration modified" in Sendcloud's reference) |
| `integration_deleted` | The integration is deleted |

Every payload has `action` and `timestamp`. Sendcloud describes `timestamp` as a Unix timestamp, and its example value, `1525271885993`, is in milliseconds, so check the scale against a real delivery before comparing it with other times.

`parcel_status_changed` is the event most integrations are built around. Its body adds `carrier_status_change_timestamp`, the time the status changed in the carrier's system, and a `parcel` object holding the full parcel: `id`, address fields, `tracking_number`, `tracking_url`, `carrier`, `is_return`, `order_number`, `external_order_id`, `shipment`, `date_updated` and more. The status is an object with an `id` and a `message`:

```json
"status": { "id": 1, "message": "Ready to send" }

```

There is no status `code` field. Use `parcel.status.id` to identify the status; the full list of IDs is available from Sendcloud's parcel statuses endpoint.

`return_created` carries a `data` object describing the return: `id`, `order_number`, `return_reason_id`, `refund_type`, `delivery_choice`, `items` and more. It fires for returns created through the API or the Return Portal, but not for returns created manually with the Return Form.

The three integration events carry an `integration` object with the integration's `id`, `shop_name`, `shop_url`, `system`, `webhook_active`, `webhook_url` and timestamps such as `last_updated_at`.

## Setting up Sendcloud webhooks

1. In Sendcloud, go to **Settings > Integrations** and click **Configure** on your Sendcloud API integration.
2. Tick **Webhook feedback enabled**.
3. Enter your endpoint in the **Webhook url** field.
4. Click **Save**.
5. Optionally click **Test API webhook** to send a test request to your endpoint.

Find the integration's **Secret Key**, or its **Webhook Signature Key** if your integration type has one, and store it server-side, for example as `SENDCLOUD_WEBHOOK_SECRET`.

If you run several integrations, for example an API integration and a shop plugin, repeat this for each one that creates parcels you need updates for.

## Securing Sendcloud webhooks

Compute an HMAC-SHA256 over the entire raw request body, exactly as received, using your secret. Encode it as hex and compare it with `Sendcloud-Signature` in constant time.

```javascript
const crypto = require('crypto');
const express = require('express');

function verifySendcloudSignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !secret) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const received = Buffer.from(signatureHeader.trim().toLowerCase(), 'utf8');
  const computed = Buffer.from(expected, 'utf8');
  return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}

const app = express();

app.post('/webhooks/sendcloud', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.SENDCLOUD_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  if (!verifySendcloudSignature(req.body, req.get('sendcloud-signature'), secret)) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // Queue the event for processing, keyed on event.action.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hashlib
import hmac

def verify_sendcloud_signature(raw_body: bytes, signature_header, secret: str) -> bool:
    if not signature_header or not secret:
        return False
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    received = signature_header.strip().lower().encode("utf-8")
    return hmac.compare_digest(received, expected.encode("utf-8"))

# In FastAPI: raw_body = await request.body()
# verify_sendcloud_signature(raw_body, request.headers.get("sendcloud-signature"),
#                            os.environ["SENDCLOUD_WEBHOOK_SECRET"])

```

Sendcloud publishes a test vector: the body `{"key": "value"}` (with the space after the colon) and the secret `secretkey` give `1eed4b3d41f4653ac64fd56f1bf1cbfd349e4482cbc11dff7134bd93e5da4b0a`.

Verification fails if you use the wrong key. Some integration types sign with the Secret Key and others with a separate Webhook Signature Key, so check which one your integration shows.

> **Make Sendcloud webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies the `Sendcloud-Signature` header, accepts each parcel update as it arrives, and queues it for your service with full delivery history.

## Sendcloud webhook limitations and pain points

### Out-of-order updates

**The Problem:** A parcel shows an older status after a newer one has already arrived.

**Why It Happens:** Sendcloud says webhooks can arrive out of order, especially after your application has been unreachable and retries catch up.

**Workarounds:**

* Store the latest `timestamp` per parcel and ignore any update older than it.
* When in doubt, fetch the parcel from Sendcloud's API before updating customer-facing status.

**How Hookdeck Can Help:** Hookdeck records every delivery with its arrival time and body, so you can see the order in which updates came in and replay them if your state needs rebuilding.

### Retries spread over hours

**The Problem:** After a short outage, parcel updates trickle in for hours.

**Why It Happens:** Sendcloud retries up to 10 times, starting 5 minutes after the failure and backing off to an hour between attempts. After the last one it stops and logs the failure in the connection issue log.

**Workarounds:**

* Keep the endpoint highly available and quick to respond.
* After an outage, check the connection issue log and reconcile affected parcels from the API.

**How Hookdeck Can Help:** Hookdeck keeps accepting Sendcloud's requests while your service is down, then [retries](/docs/retries) towards your service as soon as it recovers, without waiting for Sendcloud's backoff.

### Webhooks are per integration

**The Problem:** Parcels created in one channel never produce webhooks.

**Why It Happens:** Each integration only sends events for its own parcels. A shop plugin's parcels are not reported through the API integration's webhook.

**Workarounds:**

* Configure a webhook on every integration that creates parcels you care about, and use a separate path per integration so you know which secret to use.

**How Hookdeck Can Help:** Each integration can point at its own Hookdeck source with its own secret, and all of them can feed one destination.

### No delivery ID

**The Problem:** A retried update is processed twice.

**Why It Happens:** Sendcloud sends no delivery or event ID.

**Workarounds:**

* Key idempotency on `action`, the parcel `id`, `parcel.status.id` and `timestamp`. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

**How Hookdeck Can Help:** Hookdeck's [deduplication](/docs/deduplication) can drop repeats based on the body fields you choose.

## Best practices

### Verify against the raw body

Sendcloud stresses hashing the body exactly as received. Mount `express.raw()` on the route, use `await request.text()` in Next.js, or `await request.body()` in FastAPI.

### Route on `action`, then on status ID

Branch on `action` first. For `parcel_status_changed`, map `parcel.status.id` to your own states rather than parsing the `message` text.

### Treat return parcels separately

`parcel.is_return` tells you when an update concerns a return. Return parcels only send updates if the outgoing shipment was created through the API integration, so returns from other channels need another source of truth.

### Acknowledge fast and process asynchronously

Customer emails and order updates involve external calls. Verify, enqueue and return `200`. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

Sendcloud signs every webhook with a hex HMAC-SHA256 of the raw body in `Sendcloud-Signature`, keyed with your integration's Secret Key or Webhook Signature Key. Verify the raw bytes, route on `action`, order updates by `timestamp`, and configure a webhook on every integration whose parcels you track.

[Hookdeck Event Gateway](https://hookdeck.com) sits in front of your receiver, verifies each Sendcloud request, and holds parcel updates until your service has processed them, so an outage does not leave you waiting on Sendcloud's hourly retries.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Sendcloud webhooks reliably in minutes.