# Ship24 Webhooks

Ship24 is a multi-carrier parcel tracking API. When a tracker you follow gets a new tracking event
(or a proof of delivery), Ship24 POSTs a JSON message to your Webhook URL. Webhooks are **not
signed**. Ship24 allocates a random Webhook Secret to your account and sends it on every request as
`Authorization: Bearer <webhook secret>`; your endpoint authenticates the sender by comparing it
with the secret shown in your Ship24 dashboard.

## When to Use This Skill

* How do I receive Ship24 webhooks?
* How do I verify the `Authorization: Bearer` header on a Ship24 webhook?
* Does Ship24 sign webhook payloads? (No: use the shared Webhook Secret)
* How do I handle `tracking/events` and `tracking/pod` messages?
* How do I deduplicate Ship24 messages by `messageId`?
* Why does my Ship24 webhook endpoint return 401?

## Setup Note

Paste the Webhook Secret from your Ship24 dashboard.

Store just the secret (without `Bearer `) as `SHIP24_WEBHOOK_SECRET`. See
[references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/ship24-webhooks/references/setup.md).

## Verification (core)

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

function verifyShip24Request(headers, configured = process.env.SHIP24_WEBHOOK_SECRET) {
  if (!configured) return false;                              // never accept when unconfigured
  const secret = Buffer.from(configured.replace(/^bearer /i, ''), 'utf8'); // bare secret
  if (!secret.length) return false; // a configured value of just "Bearer " must not match
  const header = headers['authorization'];
  if (typeof header !== 'string' || header.slice(0, 7).toLowerCase() !== 'bearer ') return false;
  const received = Buffer.from(header.slice(7), 'latin1');    // scheme is case-insensitive
  return secret.length > 0 && received.length === secret.length &&
    crypto.timingSafeEqual(received, secret);
}

```

* Header: `Authorization: Bearer <secret>`. The `Bearer` scheme matches in any case (RFC 9110);
  the secret itself is compared exactly, never trimmed.
* Missing header, wrong scheme or mismatch: `401`. Body that isn't `{ "trackings": [...] }`: `400`.
* Python: `hmac.compare_digest` on **bytes** (encode both sides; `str` raises on non-ASCII).
* Read the body raw (`express.raw({ type: '*/*' })`) so any content type works, and parse it only
  after the check.

**What this does and doesn't give you.** A static shared credential authenticates the *sender*,
not the *body*, and gives **no replay protection** (no signature, no timestamp). Anyone who
captures one request, or learns the secret, can send you arbitrary tracking data. Use HTTPS only
and rotate the Webhook Secret if it leaks.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/ship24-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/ship24-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/ship24-webhooks/examples/fastapi/).

## Message Shape and Topics

The body is `{ "trackings": [ ... ] }`. Each item has `metadata` (`generatedAt`, `messageId`,
`topic`), `tracker`, `shipment`, `events` and `statistics`. Loop over `trackings[]` (it holds one
item unless you raised "Maximum number of updates per webhook message") and dispatch on
`metadata.topic`:

| `metadata.topic` | Sent When | Webhook URL |
| --- | --- | --- |
| `tracking/events` | A tracker has a new tracking event | Webhook URL |
| `tracking/pod` | A proof of delivery was retrieved or is unavailable (Proof of Delivery add-on) | Proof of Delivery Webhook URL (can be the same endpoint) |

* **Dedupe on `metadata.messageId`.** Ship24 retries a message that didn't get a 2xx, so you can
  see the same `messageId` twice.
* **Ordering isn't guaranteed.** Compare each event's `occurrenceDatetime` with what you already
  stored before changing a shipment's status.
* Respond with any `2xx`. Otherwise the request is retried up to 20 times with exponential backoff
  (seconds to hours). Proof of delivery messages always contain exactly one tracking.

## Environment Variables

```bash
SHIP24_WEBHOOK_SECRET=your_webhook_secret   # Ship24 dashboard > Webhook Secret (without "Bearer ")

```

## Local Development

Ship24 can't reach `localhost`. Tunnel with the Hookdeck CLI (no account required):

```bash
npx hookdeck-cli listen 3000 ship24 --path /webhooks/ship24

```

Use the printed URL as the Webhook URL in your Ship24 dashboard.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/ship24-webhooks/references/overview.md) - What Ship24 webhooks are, topics and payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/ship24-webhooks/references/setup.md) - Configure the Webhook URL and find the Webhook Secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/ship24-webhooks/references/verification.md) - Bearer secret check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/ship24-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests