Shipping ClickPost Module — Vendor surface
Vendor-facing HTTP surface for per-vendor ClickPost integration — credentials, pickup address, courier map, parcel defaults, webhook secret, and the courier allow-list. ClickPost is one shipping provider…
Vendor-facing HTTP surface for per-vendor ClickPost integration — credentials, pickup address, courier partner map, parcel defaults, webhook secret, and the courier allow-list. ClickPost is one shipping provider plugin (see shipping.md for the provider-agnostic surface and order.md for the fulfillment dispatch).
Source:
api-modules/shipping-clickpost/src/controllers/vendor-clickpost-config.controller.ts.
Conventions
Authentication
Both endpoints require a Better-Auth bearer session with an active vendor.
Authorization: Bearer <session-token>The active vendor is resolved via resolveActiveVendorId(session). Sessions missing an active vendor are rejected with 403 Forbidden. There is no platform RBAC permission — vendor users are not platform staff.
Tenant scoping
Every read and write scopes to the active vendor's id. Config is persisted via VendorSettingsService under vendor.admin.shipping.clickpost.*, so writes are also subject to the registry's forAdmin: true flag — but the controller calls setMany(..., { allowAdminOnly: false }), so any key marked admin-only in the registry would reject with 403 (same rule as vendor/settings).
Response envelope
{
"data": <payload>,
"message": "Success",
"statusCode": 200,
"metadata": { /* optional */ }
}Error envelope
statusCode | errorCode examples |
|---|---|
| 400 | BAD_REQUEST, VALIDATION_ERROR |
| 401 | UNAUTHORIZED |
| 403 | FORBIDDEN (no active vendor, or payload includes a forAdmin: true key) |
| 500 | INTERNAL_SERVER_ERROR |
Domain types
ClickPostCourierEntry
Each entry in the courierMap records the ClickPost-side integer courier partner id and the account code for that courier. ClickPost's v3 API uses integer courier_partner_id values rather than string codes, and these values are per-account — obtain them from your ClickPost dashboard or account manager.
type ClickPostCourierEntry = {
cpId: number; // ClickPost integer courier_partner_id (from your ClickPost dashboard)
accountCode: string; // Account code for this courier (from ClickPost dashboard)
};ClickPostConfigResponse
type ClickPostConfigResponse = {
apiKey: string;
username: string;
webhookSecret: string;
pickupPincode: string; // Indian 6-digit
enabledCouriers: string[]; // courier string codes in the allow-list
/** Maps each enabled courier code to its ClickPost cpId + accountCode.
* Required for v3 fulfillment — a courier in enabledCouriers with no
* entry here will fail at ship-time with a clear error. */
courierMap: Record<string, ClickPostCourierEntry>;
/** Full pickup address used on the ClickPost create-order payload. */
pickupName: string;
pickupAddress: string;
pickupCity: string;
pickupState: string;
pickupPhone: string;
pickupEmail: string;
pickupTin: string; // TIN / GSTIN
/** Parcel dimensions. Weight in grams; length/breadth/height in
* centimetres. L/B/H are sent on every shipment (the v3 payload has
* no per-item dimensions); only weight falls back, used when the
* order's lines supply no weight. Null when not yet configured. */
defaultParcel: {
weight: number; // grams
length: number; // centimetres
breadth: number; // centimetres
height: number; // centimetres
} | null;
/** Fully-qualified URL the vendor should configure in ClickPost's
* webhook settings. Computed from PUBLIC_API_BASE_URL + the per-vendor
* route. Null when PUBLIC_API_BASE_URL isn't set on the API process —
* in that case vendor docs should fall back to manual instructions. */
webhookUrl: string | null;
};ClickPost config
Base path: /vendor/shipping/clickpost/config.
GET /vendor/shipping/clickpost/config — Get config
Returns the active vendor's ClickPost credentials, pickup address, courier map, parcel defaults, and the resolved webhook URL.
Response 200 — ClickPostConfigResponse.
{
"data": {
"apiKey": "cp_live_...",
"username": "acme-bakery",
"webhookSecret": "whsec_...",
"pickupPincode": "560001",
"enabledCouriers": ["bluedart", "delhivery"],
"courierMap": {
"bluedart": { "cpId": 12, "accountCode": "BD_ACME_001" },
"delhivery": { "cpId": 4, "accountCode": "DL_ACME_002" }
},
"pickupName": "Acme Bakery Warehouse",
"pickupAddress": "123 Industrial Area",
"pickupCity": "Bengaluru",
"pickupState": "Karnataka",
"pickupPhone": "9876543210",
"pickupEmail": "dispatch@acme-bakery.com",
"pickupTin": "29ABCDE1234F1Z5",
"defaultParcel": {
"weight": 500,
"length": 20,
"breadth": 15,
"height": 10
},
"webhookUrl": "https://api.example.com/webhooks/shipping/clickpost/01J9..."
},
"message": "Success",
"statusCode": 200
}Errors
| Status | Code | When |
|---|---|---|
| 403 | FORBIDDEN | No active vendor on session |
GET /vendor/shipping/clickpost/active-couriers — Active couriers
Calls ClickPost's fetch-accounts API with the vendor's saved API key and returns every active account with its cpId and accountCode. The code is derived from the account's courier name (uppercased, separators → _), falling back to a canonical spelling when one already exists for that courier — so activating a new courier partner at ClickPost makes it selectable here with no code change. The config UI uses this to populate the courier picker and prefill the courier map.
Returns an empty array — never an error — when the API key isn't saved yet or ClickPost is unreachable.
Response 200 — ActiveCourier[].
{
"data": [
{
"code": "DELHIVERY",
"courierName": "Delhivery",
"cpId": 4,
"accountCode": "DL_ACME_002"
}
],
"message": "Success",
"statusCode": 200
}Errors
| Status | Code | When |
|---|---|---|
| 403 | FORBIDDEN | No active vendor on session |
PATCH /vendor/shipping/clickpost/config — Update config
Partial update — fields not in the body are left untouched. The body is .strict() so unknown keys are rejected at the zod layer. The controller maps each field to a setting under vendor.admin.shipping.clickpost.* and writes via VendorSettingsService.setMany (audit row per changed key).
Body
{
"apiKey": "cp_live_...",
"username": "acme-bakery",
"webhookSecret": "whsec_...",
"pickupPincode": "560001",
"enabledCouriers": ["bluedart", "delhivery"],
"courierMap": {
"bluedart": { "cpId": 12, "accountCode": "BD_ACME_001" },
"delhivery": { "cpId": 4, "accountCode": "DL_ACME_002" }
},
"pickupName": "Acme Bakery Warehouse",
"pickupAddress": "123 Industrial Area",
"pickupCity": "Bengaluru",
"pickupState": "Karnataka",
"pickupPhone": "9876543210",
"pickupEmail": "dispatch@acme-bakery.com",
"pickupTin": "29ABCDE1234F1Z5",
"defaultParcel": {
"weight": 500,
"length": 20,
"breadth": 15,
"height": 10
}
}Credentials and courier list
| Field | Type | Constraints | Setting key |
|---|---|---|---|
apiKey | string? | Trimmed; 1..500 chars | clickpost.api_key |
username | string? | Trimmed; 1..200 chars | clickpost.username |
webhookSecret | string? | Trimmed; 32..500 chars | clickpost.webhook_secret |
pickupPincode | string? | Trimmed; exactly 6 digits (/^\d{6}$/) | clickpost.pickup_pincode |
enabledCouriers | string[]? | Each entry non-empty | clickpost.enabled_couriers |
enabledCouriersgates nothing on the server. It drives the config UI's courier picker only; no code path validates a shipment against it.resolveCourierandmethodsForVendorboth key offcourierMap, so a courier is usable once it has acourierMapentry — with or without anenabledCouriersentry.
Courier map — required for v3 fulfillment
| Field | Type | Notes | Setting key |
|---|---|---|---|
courierMap | Record<string, { cpId: number; accountCode: string }>? | One entry per enabled courier; cpId and accountCode are per-account values from your ClickPost dashboard | clickpost.courier_map |
Where to find cpId and accountCode: Log in to your ClickPost dashboard, go to Couriers (or contact your ClickPost account manager). The integer
cpId(courier_partner_id) andaccountCodeare specific to your ClickPost account — they cannot be inferred from the courier name.
Pickup address — required for v3 fulfillment
| Field | Type | Constraints | Setting key |
|---|---|---|---|
pickupName | string? | Trimmed; 1..120 chars | clickpost.pickup_name |
pickupAddress | string? | Trimmed; 1..500 chars | clickpost.pickup_address |
pickupCity | string? | Trimmed; 1..120 chars | clickpost.pickup_city |
pickupState | string? | Trimmed; 1..120 chars | clickpost.pickup_state |
pickupPhone | string? | Trimmed; 6..20 chars | clickpost.pickup_phone |
pickupEmail | string? | Valid email, max 200 chars | clickpost.pickup_email |
pickupTin | string? | Trimmed; 1..40 chars (TIN / GSTIN) | clickpost.pickup_tin |
Default parcel dimensions — required for v3 fulfillment
| Field | Type | Constraints | Setting key |
|---|---|---|---|
defaultParcel | { weight, length, breadth, height }? | Nested object; all four sub-fields must be positive integers. weight in grams; length, breadth, height in centimetres. | clickpost.default_parcel (stored as a JSON object under this single key) |
Not optional in practice, and not purely a fallback.
getCredentialsrejects a config without it.length/breadth/heightare sent on every shipment — the ClickPost v3 payload carries no per-item dimensions, so this is the only source. Onlyweightbehaves as a fallback (line.weight ?? defaultParcel.weight).
Response 200 — updated ClickPostConfigResponse (re-fetched after the write).
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Body fails zod (unknown key, short secret, bad pincode) |
| 403 | FORBIDDEN | No active vendor on session, or payload maps to a forAdmin: true key |
Fulfillment flow (ClickPost v3)
When a vendor marks a sub-order fulfilled with providerId="clickpost" and a courier method, the system:
- Validates that all required config keys are present —
api_key,username,pickup_*fields, and acourier_mapentry for the chosen courier. Missing keys are listed in the error response. - Builds the ClickPost v3 create-order payload:
pickup_info— from the vendor'spickup_*config fields.drop_info— from the order's shipping address.shipment_details— one item per order line carryingsku,description,quantity,price, andweight(fromorder_line.weightAtOrder, snapshotted at place-order; falls back todefaultParcel.weightwhen null). Totalweightis the sum of per-line weights, falling back todefaultParcel.weightwhen every line is null. Parcellength/breadth/heightcome fromdefaultParcelunconditionally — the v3 item shape has no dimension fields, so nothing per-line or per-variant contributes.cod_value— set to the sub-order total (in rupees; converted from internal integer subunits) whenpayment_method === "cod". Set to 0 for prepaid orders.courier_partner_id— the integercpIdfrom the vendor'scourier_mapfor the chosen courier.
- Calls
POST https://www.clickpost.in/api/v3/create-order/?username=<u>&key=<k>withContent-Type: application/json. - Handles the response:
- ClickPost always returns HTTP 200, even on errors. Success is signaled by
meta.successbeingtrueandmeta.statusbeing200,102, or202. - Status
200— synchronous success. AWB is atresult.waybill; label URL atresult.label. Both are stamped onto the sub-order immediately. - Status
102/202— async accepted. ClickPost has accepted the order but the AWB is not yet assigned. The AWB will arrive later via a ClickPost tracking webhook (see the webhooks doc). The sub-order is marked fulfilled, and the AWB/label will be populated when the webhook is received. meta.status 400(ormeta.success === false) — business error. The error message from ClickPost is surfaced to the vendor.
- ClickPost always returns HTTP 200, even on errors. Success is signaled by
- Stamps the AWB and label URL on
order_vendor(immediately on sync; via webhook on async).
Product variant fields used at fulfillment
Of the fields set in the product editor under Variants → Shipping/Customs, exactly one reaches ClickPost:
| Variant field | Type | Reaches ClickPost? |
|---|---|---|
weight | integer | Yes — grams. Snapshotted onto order_line.weightAtOrder at place-order for stable manifests, then sent as each item's weight and summed into the shipment total. |
length / breadth / height | integer | No — centimetres, but the v3 payload takes dimensions only from defaultParcel. |
countryOfOrigin | string | No |
midCode | string | No |
hsnCode | string | No — snapshotted onto order_line at place-order and used for GST invoicing, not shipping. |
CreateShipmentInput.items(built byOrderLineRepository.listForManifest) carries onlysku,description,quantity,unitPriceSubunits, andweight, so the customs fields have no path into the create-order call today. They remain meaningful for invoicing and for any future provider that accepts them — but setting them will not change a ClickPost manifest.
Set defaultParcel to realistic values for your typical parcel: its length / breadth / height are what every shipment declares, and its weight is the fallback when a line has no snapshotted weight.
Webhook receiver
The ClickPost webhook receiver itself is not on the vendor surface — it's an unauthenticated public endpoint that verifies the per-vendor webhookSecret from the request signature and lands rows on shipping_event. See the webhooks documentation under separated/webhooks/ for that surface.
Checkpoint times on those pushes are ClickPost's local wall clock suffixed Z; the receiver re-reads them in the platform-level admin.shipping.clickpost.webhook_timezone (default Asia/Kolkata). There is no per-vendor override.
Related modules
shipping— provider-agnostic config (enabledProviders, flat rate, tracking events).clickpostmust appear invendor.admin.shipping.enabled_providersfor these credentials to be used at fulfill time. Seeshipping.md.settings—vendor.admin.shipping.clickpost.*keys live in the vendor settings registry; this controller is a typed wrapper overvendor-settings. Seesettings.md.order—POST /vendor/orders/:id/fulfilledwithproviderId="clickpost"dispatches toClickPostProvider.createShipment()which reads these credentials.
Settings Module — Vendor surface
Vendor self-service for reading and writing the active vendor's own settings. Settings are organized by scope (admin for staff-facing config like shipping/tax, store for…
Shipping Module — Vendor surface
Vendor-facing HTTP surface for shipping configuration (flat customer-charge rate, free-above threshold, enabled providers) and per-sub-order tracking timeline. The vendor charge…