Agent skill

ClickPost Webhooks Skill

Receive and authenticate ClickPost shipment tracking webhooks. Use when setting up a ClickPost status webhook endpoint, verifying the Authorization: Token header, debugging 401s, or handling scan events by clickpost_status_code such as Delivered, OutForDelivery, PickedUp and InTransit.

Install this skill

npx skills add hookdeck/webhook-skills --skill 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)

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/, examples/nextjs/, examples/fastapi/.

Status Codes

Dispatch on clickpost_status_code, an integer:

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

The list is longer; see the ClickPost docs. 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

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):

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

Use the printed URL as the webhook URL in ClickPost.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 9, 2026

View on GitHub →