# ClickPost Webhooks

ClickPost (shipment tracking for e-commerce) sends a JSON `POST` for each shipment scan: a flat
record with `waybill`, `cp_id`, `status`, `remark`, `location`, `timestamp`,
`clickpost_status_code`, `clickpost_status_description` and an `additional` object that includes
`latest_status`. Webhooks are **not signed**. With Simple Token authentication every request carries
`Authorization: Token <secret>`, and your endpoint authenticates the sender by comparing it with the
token you configured.

> Choose Simple Token authentication on the ClickPost webhook and paste the token.

## When to Use This Skill

* How do I receive ClickPost webhooks?
* How do I verify the `Authorization: Token` header on a ClickPost webhook?
* Does ClickPost sign webhook payloads? (No: it uses a shared token)
* How do I handle `clickpost_status_code` 8 (Delivered) or 6 (OutForDelivery)?
* Why does my ClickPost webhook endpoint return 401?

## Verification (core)

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

function safeEqual(a, b) { // hash both sides: equal-length buffers
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyClickPostRequest(headers, configured = process.env.CLICKPOST_WEBHOOK_SECRET) {
  if (!configured) return false;                  // never accept when unconfigured
  const secret = configured.toLowerCase().startsWith('token ') ? configured.slice(6) : configured;
  const header = headers['authorization'];
  if (!header || header.slice(0, 6).toLowerCase() !== 'token ') return false; // not "Bearer"
  return safeEqual(header.slice(6), secret);
}

```

* Header: `Authorization: Token <secret>`. The scheme is `Token`, **not** `Bearer`, and is matched
  case-insensitively. Store the **bare** secret in `CLICKPOST_WEBHOOK_SECRET`; if the stored value
  itself starts with `Token `, strip it. Don't trim or otherwise alter the secret.
* Missing header, wrong scheme or mismatch: `401`. A body that isn't a ClickPost scan object: `400`.
  Success is `200`/`201`/`202`/`204` within 5 seconds.
* Read the body raw (`express.raw({ type: '*/*' })`) and authenticate before parsing it.
* **A static shared credential authenticates the sender, not the body, and gives no replay
  protection** (no signature, no timestamp). Use HTTPS and rotate the token if it leaks.
* Only **Simple Token** is covered. ClickPost's dashboard also offers X-API-Key, Webhook-Key, Basic
  Auth and Dynamic Token; these are not covered by this skill or Hookdeck's ClickPost source.

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

## Status Codes

Dispatch on `clickpost_status_code`, an integer:

| Code | `clickpost_status_description` |
| --- | --- |
| 1 | OrderPlaced |
| 2 | PickupPending |
| 4 | PickedUp |
| 5 | InTransit |
| 6 | OutForDelivery |
| 8 | Delivered |
| 9 | FailedDelivery |
| 41 / 42 / 43 / 44 | RiderAllocationFailed / RiderAllocated / ReachedPickupLocation / ReachedDropLocation |
| 47 / 48 / 50 / 52 | PartialRTOOutForDelivery / PartialDelivered / PartialRTO / PartialRTODelivered |

The list is longer; see the [ClickPost docs](https://docs.clickpost.ai/docs/tracking-a-shipment-using-webhook).
Scans may arrive out of order and can be redelivered, so dedupe on `waybill` + scan
(e.g. `waybill`, `clickpost_status_code`, `timestamp`) and don't let an older scan overwrite a newer state.

## Environment Variables

```bash
CLICKPOST_WEBHOOK_SECRET=your-simple-token   # the bare token, without "Token "

```

## Local Development

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

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

```

Use the printed URL as the webhook URL in ClickPost.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/clickpost-webhooks/references/overview.md) - What ClickPost webhooks are, status codes and payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/clickpost-webhooks/references/setup.md) - Configure the webhook and Simple Token in ClickPost
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/clickpost-webhooks/references/verification.md) - Token check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/clickpost-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests