# Guide to Ship24 Webhooks: Features and Best Practices

Ship24 webhooks push parcel tracking updates to your system as couriers report them. Ship24 tracks shipments across many carriers through one API, and once you create a Tracker for a shipment, every new event the courier reports, such as in transit, out for delivery or delivered, can reach your order system or customer notifications straight away instead of on your next poll.

This guide covers how Ship24 webhooks are set up and authenticated, the tracking and proof-of-delivery messages and their shape, how grouping, ordering and retries behave, and the practices that keep a Ship24 receiver reliable.

## What are Ship24 webhooks?

Ship24 sends webhook messages as soon as a valid Webhook URL is configured in your dashboard. From then on, it pushes any new tracking events found by your Trackers to that URL as JSON `POST` requests. Each message contains the entire event, so your system can use the data without calling the API.

Ship24 does not sign webhooks. It allocates a random **Webhook Secret** to your account and sends it with every request as `Authorization: Bearer <secret>`. Your endpoint compares the value with the one shown in your dashboard.

## Ship24 webhook features

| Feature | Details |
| --- | --- |
| Configuration | Webhook URL in the Ship24 dashboard. Proof of Delivery needs a separate URL on the same page |
| Method and format | `POST`, JSON body |
| Authentication | `Authorization: Bearer <Webhook Secret>`, allocated per account |
| Signature and timestamp | None |
| Topics | `tracking/events` and `tracking/pod`, in `metadata.topic` |
| Grouping | One update per message by default. Grouping, on request, adds about 15 minutes of delay |
| Ordering | Not guaranteed |
| Success response | Any `2xx`. The response body does not matter |
| Retries | Up to 20, with exponential backoff from a few seconds to a few hours |
| Resend | All messages for a tracker, through the API's resend webhooks endpoint |
| Testing | **Test your integration** in the dashboard sends dummy tracking events |
| Source IPs | `54.161.7.2`. Dashboard test sends come from elsewhere |

## Tracking and proof-of-delivery messages

Every message has a `trackings` array. By default on recent accounts it holds exactly one item, and that item's `events` array holds exactly one event. Here is Ship24's example, with the parts it elides marked:

```json
{
  "trackings": [
    {
      "metadata": {
        "generatedAt": "2025-03-04T17:13:35.000Z",
        "messageId": "356a7f93-3ce5-4b49-b560-156537283df9",
        "topic": "tracking/events"
      },
      "tracker": {
        "trackerId": "26148317-7502-d3ac-44a9-546d240ac0dd",
        "trackingNumber": "9400115901047177598206",
        "shipmentReference": "c6e4fef4-a816-b68f-4024-3b7e4c5a9f81",
        "clientTrackerId": "3fa99515-3ca0-4901-85bb-056ee016799b",
        "isSubscribed": true,
        "createdAt": "2025-03-01T03:16:22.000Z"
      },
      "shipment": { "shipmentId": "f4f888d7-d140-423f-9a48-e0689d27e098", "...": "..." },
      "events": [
        {
          "eventId": "ee8ebe96-4eae-4a91-9a99-8f3afa6a0f46",
          "status": "Delivered to the addressee",
          "occurrenceDatetime": "2025-03-04T17:12:57",
          "...": "..."
        }
      ]
    }
  ]
}

```

`metadata.messageId` identifies the message, `tracker` identifies the Tracker and its tracking number, and `occurrenceDatetime` is when the event happened. Some fields belong to add-on options and are absent when you are not subscribed to them, so your parser must tolerate missing keys.

With the **Proof of Delivery** add-on, Ship24 asks the courier for a proof once a shipment is delivered, and sends one message with topic `tracking/pod` to your Proof of Delivery Webhook URL. Its `data.status` is `found` or `unavailable`. When found, `data.content` holds either text, such as a signatory name, or a `downloadUrl` for a PDF or image. The download URL is valid for 7 days. Proof-of-delivery messages are never grouped.

> **Inspect Ship24 webhooks as they arrive.** Set your Webhook URL to a [Hookdeck Inspect](https://inspect.hookdeck.com) URL and use **Test your integration** to see a message's headers and body, with no account or setup required.

## Setting up Ship24 webhooks

1. In the Ship24 dashboard, set your **Webhook URL** to your HTTPS endpoint.
2. If you subscribe to Proof of Delivery, set the **Proof of Delivery Webhook URL** on the same page. It can be the same endpoint or a different one. Without it, no proof-of-delivery messages are sent.
3. Copy the **Webhook Secret** shown in the dashboard and store it server-side, for example as `SHIP24_WEBHOOK_SECRET`.
4. Use **Test your integration** to send a message with dummy tracking events.

To route through Hookdeck, create a Hookdeck source with the **Ship24** type and use its URL as the Webhook URL, and as the Proof of Delivery Webhook URL if you use it. The source has one field, **Webhook Secret**: paste the Webhook Secret from your Ship24 dashboard. Paste only the secret. Ship24 sends it as `Bearer <secret>` in the `Authorization` header, and Hookdeck matches the `Bearer ` scheme itself. The source accepts `POST` only, which is the method Ship24 uses.

If you restrict inbound traffic by IP on your own endpoint, set it up after testing: dashboard test sends do not come from `54.161.7.2`.

## Securing Ship24 webhooks

Read the `Authorization` header, check that the scheme is `Bearer` (case-insensitively), and compare the rest with your Webhook Secret in constant time.

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

const WEBHOOK_SECRET = process.env.SHIP24_WEBHOOK_SECRET; // the secret only, no "Bearer "

function verifyBearer(header) {
  if (!WEBHOOK_SECRET || typeof header !== 'string') return false;
  const match = /^bearer (.+)$/i.exec(header);
  if (!match) return false;
  const received = Buffer.from(match[1], 'utf8');
  const expected = Buffer.from(WEBHOOK_SECRET, 'utf8');
  // timingSafeEqual throws on unequal lengths, so check first.
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

app.post('/webhooks/ship24', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyBearer(req.get('authorization'))) {
    return res.sendStatus(401);
  }

  let message;
  try {
    message = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }

  for (const tracking of message.trackings || []) {
    // Route on tracking.metadata.topic; dedupe on tracking.metadata.messageId.
  }
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac
import os

WEBHOOK_SECRET = os.environ.get("SHIP24_WEBHOOK_SECRET", "")  # the secret only

def verify_bearer(header) -> bool:
    if not WEBHOOK_SECRET or not header:
        return False
    scheme, _, token = header.partition(" ")
    if scheme.lower() != "bearer":
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    return hmac.compare_digest(token.encode("utf-8"), WEBHOOK_SECRET.encode("utf-8"))

```

The Webhook Secret is static. It authenticates the sender, not the body, and with no timestamp a captured request can be replayed. Serve the endpoint over HTTPS and keep the secret out of logs. Allowlisting `54.161.7.2` adds a second check, but the secret is what authenticates a request.

> **Make Ship24 webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the Webhook Secret, answers Ship24 straight away, and queues every tracking update durably before your service sees it.

## Ship24 webhook limitations and pain points

### Updates arrive out of order

**The Problem:** A shipment shows as in transit after it has been delivered.

**Why It Happens:** Ship24 does not guarantee order. A retried message can arrive after newer ones, and couriers can report events for one shipment at different times.

**Workarounds:**

* Before changing a shipment's status, compare the event's `occurrenceDatetime` with the latest one you already have, as Ship24 recommends, and ignore older events.
* Store every event, but derive the current status from the latest by occurrence time.

**How Hookdeck Can Help:** Hookdeck keeps every message with its full body, so when a status looks wrong you can see the order things actually arrived in and replay them after fixing your logic.

### Retries mean duplicates

**The Problem:** The same tracking event is processed twice, and a customer gets two "delivered" emails.

**Why It Happens:** When Ship24 does not receive a `2xx`, it retries the message up to 20 times, with delays growing from a few seconds to a few hours.

**Workarounds:**

* Store each `metadata.messageId`, or the event's `eventId`, before acting on it, and skip repeats.
* Make notifications to customers idempotent per shipment and status.

**How Hookdeck Can Help:** Hookdeck [deduplication](/docs/deduplication) can drop repeated messages before they reach your service, based on `messageId` or `eventId`.

### Proof-of-delivery files expire

**The Problem:** A proof of delivery you meant to keep is gone when someone asks for it.

**Why It Happens:** The `downloadUrl` in a `tracking/pod` message is valid for 7 days, and proofs are only delivered by webhook, not through the tracking API.

**Workarounds:**

* Download the file and store it on your side as soon as the message arrives.
* If your service was down, use the API's resend webhooks endpoint for the tracker while the link is still valid.

**How Hookdeck Can Help:** Hookdeck stores every `tracking/pod` message and retries delivery to your service on a [schedule](/docs/retries) you control, so a brief outage does not leave you without the link.

### Grouped messages need different handling

**The Problem:** A handler that reads only `trackings[0]` drops updates.

**Why It Happens:** Accounts with grouping enabled can receive several updates, for several shipments, in one message, and Ship24 waits about 15 minutes to group them.

**Workarounds:**

* Always loop over `trackings`, even on a default account.
* Keep the default of one update per message unless the volume of requests is a real problem.

**How Hookdeck Can Help:** Hookdeck [transformations](/docs/transformations) can reshape a message before it reaches your service, and every original message is kept for inspection.

## Best practices

### Store the secret, not the header

Keep only the secret in your configuration and match the `Bearer ` scheme in code, the same way the Hookdeck source does.

### Tolerate missing fields

Add-on fields are absent when you are not subscribed to them. Use optional access throughout and never require a field the core payload does not guarantee.

### Make handlers idempotent

Key stored events on `eventId` and messages on `messageId`. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Acknowledge fast and process asynchronously

Verify, store, return `200`, and do the work afterwards. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

Ship24 pushes tracking updates and proofs of delivery as JSON `POST` requests with your account's Webhook Secret in an `Authorization: Bearer` header and no signature. Check the scheme and compare the secret in constant time over HTTPS, loop over `trackings`, compare `occurrenceDatetime` before changing status, deduplicate retried messages, and store proof-of-delivery files before their links expire.

[Hookdeck Event Gateway](https://hookdeck.com) checks the Webhook Secret, answers Ship24 immediately and keeps every message until your service has processed it, so an outage on your side does not mean missed deliveries.

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