Gareth Wilson Gareth Wilson

Guide to ClickPost Webhooks: Features and Best Practices

Published


ClickPost webhooks keep your systems up to date with every shipment you track through it. ClickPost collects scans from your carrier partners, normalises them into one set of status codes, and posts each update to your endpoint, so you can notify customers, update order status and flag delivery problems without polling carrier APIs yourself.

This guide covers the two kinds of ClickPost tracking webhook, what each request contains, how to authenticate it with the Simple Token, how ClickPost retries failed deliveries, and the practices that keep a ClickPost receiver accurate.

What are ClickPost webhooks?

ClickPost gets status updates from carriers, by carrier webhooks where they exist and by polling where they don't. Whenever a new scan comes in, ClickPost sends a JSON POST to the URL you configured. There are two webhooks, configured and retriggered independently:

  • All Status fires "for every scan update, not only when the status changes". If a carrier reports several scans with the same status, you get several calls.
  • Selected Status fires only for the checkpoints you opt into in the dashboard, and adds notification_event_id to the additional object. If ClickPost receives several carrier notifications at the same time, only the latest is sent.

ClickPost doesn't sign webhook requests. Instead, each webhook carries a credential you configure, and your endpoint checks it. The options are a Simple Token, an x-api-key header, a webhook-key header, Basic Auth, or, for enterprise accounts, a Dynamic Token fetched from your own auth endpoint.

ClickPost webhook features

FeatureDetails
ConfigurationDashboard: Settings > Notifications > Webhooks, with All Status and Selected Status tabs
MethodPOST with a JSON body
TriggerEvery new carrier scan (All Status), or chosen checkpoints only (Selected Status)
Status fieldclickpost_status_code (integer), at the root and in additional.latest_status
AuthenticationSimple Token (Authorization: Token <token>), X-API-Key, Webhook-Key, Basic Auth or Dynamic Token. No signature
SuccessHTTP 200, 201, 202 or 204
Timeout5 seconds
RetriesUp to 6, at 30, 60, 90, 120, 150 and 180 minutes after the first failure
OrderingNot guaranteed
Deduplication keywaybill plus scan_id
Manual resendRetrigger from the dashboard, which sends the shipment's latest status
Source IPsNot published

Status codes and payloads

ClickPost's status vocabulary is numeric. Route on clickpost_status_code rather than on clickpost_status_description: ClickPost says the text fields are "subject to change". The common tracking codes are:

CodeDescription
1OrderPlaced
2, 3, 4, 25PickupPending, PickupFailed, PickedUp, OutForPickup
5InTransit
6OutForDelivery
8Delivered
9FailedDelivery
10Cancelled
11 to 15, 21Return to origin (RTO): requested, marked, out for delivery, delivered, failed, in transit
16, 17, 18, 20Lost, Damaged, ShipmentDelayed, ShipmentHeld

Quick-commerce shipments add codes from 40 upwards, such as 42 RiderAllocated and 48 PartialDelivered, and returns and exchanges have their own codes from 101. The full tables are on ClickPost's tracking status codes page. Selected Status webhooks also carry additional.notification_event_id, which is a separate numbering from clickpost_status_code.

Every body is a flat scan record plus an additional object. Here is ClickPost's All Status example, trimmed:

{
  "waybill": "SF49245NER",
  "cp_id": 9,
  "status": "OFD",
  "remark": "Shipment is Out for Delivery",
  "location": "DEL_GeetaColony",
  "timestamp": "2019-05-06T10:04:20Z",
  "clickpost_status_code": 6,
  "clickpost_status_description": "OutForDelivery",
  "additional": {
    "latest_status": {
      "clickpost_status_code": 6,
      "clickpost_status_description": "OutForDelivery",
      "clickpost_status_bucket": 4,
      "clickpost_status_bucket_description": "Out for delivery",
      "status": "OFD",
      "timestamp": "2019-05-06T10:04:20Z",
      "location": "DEL_GeetaColony",
      "remark": "Shipment is Out for Delivery",
      "reference_number": "WERA-9616974"
    },
    "is_rvp": false,
    "courier_partner_edd": "2020-03-25",
    "order_id": "ABC123"
  }
}

The root fields describe this scan. additional.latest_status describes the shipment as it stands after ClickPost's de-duplication, and ClickPost recommends it for customer-facing notifications. The full field list also includes account_code, scan_id, coordinates, rider details, NDR codes for failed deliveries and NPR codes for failed pickups. Note that timestamp is in the courier's timezone, not necessarily UTC.

ClickPost also warns against strict JSON schema validation: "new fields [are] added without prior notice".

Setting up ClickPost webhooks

  1. In the ClickPost dashboard, go to Settings, then Notifications and Webhooks.
  2. Choose the All Status tab, or Selected Status and pick the checkpoints you want.
  3. Enter your HTTPS endpoint URL.
  4. Choose Simple Token authentication and enter a long random token, for example the output of openssl rand -hex 32.
  5. Save. Configure the other tab separately if you need both.

With Hookdeck. Choose Simple Token authentication on the ClickPost webhook and paste the token. Create a Hookdeck source with the ClickPost type and paste only the token into the Token field. ClickPost sends it as Authorization: Token <token>, and Hookdeck handles the Token prefix. Use the source URL as the webhook URL in ClickPost. The source accepts POST only, which is the method ClickPost uses. Hookdeck's ClickPost source checks the Simple Token only, not the X-API-Key, Webhook-Key, Basic Auth or Dynamic Token options.

Securing ClickPost webhooks

Read the Authorization header, check that the scheme is Token (not Bearer), and compare the rest with your token in constant time. Do this before you parse the body.

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

const TOKEN = process.env.CLICKPOST_WEBHOOK_TOKEN; // the bare token, without "Token "

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

function verifyClickPost(authorization) {
  if (!TOKEN || typeof authorization !== 'string') return false;
  if (authorization.slice(0, 6).toLowerCase() !== 'token ') return false; // scheme is case-insensitive
  return safeEqual(authorization.slice(6), TOKEN);
}

const app = express();

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

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

  // Deduplicate on scan.waybill + scan.scan_id, queue the scan, then respond within 5 seconds.
  res.sendStatus(200);
});

The same check in Python:

import hmac
import os

TOKEN = os.environ.get("CLICKPOST_WEBHOOK_TOKEN", "")


def verify_clickpost(authorization) -> bool:
    if not TOKEN or not authorization:
        return False  # fail closed when unconfigured or missing
    if authorization[:6].lower() != "token ":
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    return hmac.compare_digest(authorization[6:].encode("utf-8"), TOKEN.encode("utf-8"))

A static token authenticates the sender, not the body. It can't detect a modified payload, and with no signed timestamp a captured request can be replayed and still pass. Use HTTPS, a long random token, and replace it in ClickPost and in your receiver together if it might have leaked.

Make ClickPost webhooks production-ready. Hookdeck Event Gateway checks the Simple Token on every scan, answers ClickPost straight away, and queues each update durably until your service has processed it.

ClickPost webhook limitations and pain points

A 5-second timeout and long gaps between retries

The Problem: A brief slowdown on your side delays a tracking update by half an hour or more.

Why It Happens: ClickPost waits 5 seconds for a response. A timeout or an unsuccessful status schedules a retry 30 minutes later, then at 60, 90, 120, 150 and 180 minutes after the first failure, and that is the last attempt. A customer-facing "out for delivery" message can arrive after the parcel has been delivered.

Workarounds:

  • Respond as soon as the token checks out, and do the work in the background.
  • After an outage, use the dashboard's retrigger, which sends each shipment's latest status.

How Hookdeck Can Help: Hookdeck answers ClickPost straight away and stores the update, so slow processing on your side never counts as a timeout. Hookdeck then delivers to your service with retries you control, and you can replay any stored scan.

Is 204 a success?

The Problem: Your endpoint returns 204 No Content and you see the same scan again.

Why It Happens: ClickPost's success table lists 200, 201, 202 or 204, but its retry rule says a retry is scheduled "if your endpoint does not return HTTP 200, 201, or 202".

Workarounds:

  • Return 200 to avoid the question.
  • Deduplicate anyway, because retries resend the same payload.

How Hookdeck Can Help: Hookdeck answers ClickPost with a 200 and lets your service respond however it likes.

Out of order and repeated scans

The Problem: A shipment shows "in transit" after it was delivered, or a customer gets the same message twice.

Why It Happens: Carriers report late, so "scans may arrive out of chronological order". All Status fires on every scan, including several with the same status, and retries repeat a payload while its latest_status may have moved on.

Workarounds:

  • Deduplicate on waybill plus scan_id. ClickPost says scan_id is globally unique and increases monotonically.
  • Compare timestamp (or scan_id) before you overwrite a shipment's state, and drive customer messages from additional.latest_status.

How Hookdeck Can Help: Hookdeck's deduplication can drop repeated deliveries based on waybill and scan_id before they reach your service.

A single shared token

The Problem: A wrong or rotated token turns every scan into a failed delivery.

Why It Happens: The Simple Token is a static credential set once per webhook tab, and nothing about it is tied to the request body.

Workarounds:

  • Rotate by accepting the old and new token in your receiver until ClickPost has the new one.
  • Use HTTPS, and keep the token out of logs.

How Hookdeck Can Help: Hookdeck's ClickPost source compares the token you paste. Hookdeck answers ClickPost with a 200 either way, then records a request without the right token as failed verification, so it never reaches your destination, and you can see exactly which Authorization value arrived.

Best practices

Route on codes, not text

Switch on clickpost_status_code. Treat clickpost_status_description, the raw status and remark as display text that can change.

Use the right status for the job

Use the root clickpost_status_code for scan history, and additional.latest_status.clickpost_status_code for anything a customer sees, as ClickPost recommends.

Parse permissively

Ignore unknown fields and expect null in nullable ones. ClickPost adds fields over time, and a strict schema will start rejecting valid scans.

Make the endpoint idempotent

ClickPost asks for this explicitly. Store waybill and scan_id before you act, and skip pairs you've seen. See our guide to webhook idempotency.

Acknowledge fast

Five seconds is a short window. Check the token, store the scan, return 200, and process afterwards. See why to process webhooks asynchronously.

Conclusion

ClickPost posts a JSON scan record for every carrier update (All Status) or for the checkpoints you choose (Selected Status), authenticated by a static credential rather than a signature. Use the Simple Token and check Authorization: Token <token>, route on clickpost_status_code, deduplicate on waybill and scan_id, and answer within 5 seconds, because a missed response costs a 30-minute wait.

Hookdeck Event Gateway sits in front of your receiver, checks the token, answers ClickPost at once, and keeps every scan until your service has processed it.

Get started with Hookdeck for free and handle ClickPost webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.