Shipping ClickPost Module — Vendor surface
Vendor-facing HTTP surface for per-vendor ClickPost integration — credentials, pickup pincode, webhook secret, and the courier allow-list. ClickPost is one shipping provider…
Vendor-facing HTTP surface for per-vendor ClickPost integration — credentials, pickup pincode, 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
ClickPostConfigResponse
type ClickPostConfigResponse = {
apiKey: string;
username: string;
webhookSecret: string;
pickupPincode: string; // Indian 6-digit
enabledCouriers: string[]; // courier ids inside ClickPost
/** 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 pincode, courier allow-list, and the resolved webhook URL.
Response 200 — ClickPostConfigResponse.
{
"data": {
"apiKey": "cp_live_...",
"username": "acme-bakery",
"webhookSecret": "whsec_...",
"pickupPincode": "560001",
"enabledCouriers": ["bluedart", "delhivery"],
"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"]
}| 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; 8..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 |
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 |
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/payouts, 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…