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 |
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.
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…