Supercommerce API Docs
Vendor API

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

statusCodeerrorCode examples
400BAD_REQUEST, VALIDATION_ERROR
401UNAUTHORIZED
403FORBIDDEN (no active vendor, or payload includes a forAdmin: true key)
500INTERNAL_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 200ClickPostConfigResponse.

{
  "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

StatusCodeWhen
403FORBIDDENNo 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 200ActiveCourier[].

{
  "data": [
    {
      "code": "DELHIVERY",
      "courierName": "Delhivery",
      "cpId": 4,
      "accountCode": "DL_ACME_002"
    }
  ],
  "message": "Success",
  "statusCode": 200
}

Errors

StatusCodeWhen
403FORBIDDENNo 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

FieldTypeConstraintsSetting key
apiKeystring?Trimmed; 1..500 charsclickpost.api_key
usernamestring?Trimmed; 1..200 charsclickpost.username
webhookSecretstring?Trimmed; 32..500 charsclickpost.webhook_secret
pickupPincodestring?Trimmed; exactly 6 digits (/^\d{6}$/)clickpost.pickup_pincode
enabledCouriersstring[]?Each entry non-emptyclickpost.enabled_couriers

enabledCouriers gates nothing on the server. It drives the config UI's courier picker only; no code path validates a shipment against it. resolveCourier and methodsForVendor both key off courierMap, so a courier is usable once it has a courierMap entry — with or without an enabledCouriers entry.

Courier map — required for v3 fulfillment

FieldTypeNotesSetting key
courierMapRecord<string, { cpId: number; accountCode: string }>?One entry per enabled courier; cpId and accountCode are per-account values from your ClickPost dashboardclickpost.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) and accountCode are specific to your ClickPost account — they cannot be inferred from the courier name.

Pickup address — required for v3 fulfillment

FieldTypeConstraintsSetting key
pickupNamestring?Trimmed; 1..120 charsclickpost.pickup_name
pickupAddressstring?Trimmed; 1..500 charsclickpost.pickup_address
pickupCitystring?Trimmed; 1..120 charsclickpost.pickup_city
pickupStatestring?Trimmed; 1..120 charsclickpost.pickup_state
pickupPhonestring?Trimmed; 6..20 charsclickpost.pickup_phone
pickupEmailstring?Valid email, max 200 charsclickpost.pickup_email
pickupTinstring?Trimmed; 1..40 chars (TIN / GSTIN)clickpost.pickup_tin

Default parcel dimensions — required for v3 fulfillment

FieldTypeConstraintsSetting 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. getCredentials rejects a config without it. length / breadth / height are sent on every shipment — the ClickPost v3 payload carries no per-item dimensions, so this is the only source. Only weight behaves as a fallback (line.weight ?? defaultParcel.weight).

Response 200 — updated ClickPostConfigResponse (re-fetched after the write).

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod (unknown key, short secret, bad pincode)
403FORBIDDENNo 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:

  1. Validates that all required config keys are present — api_key, username, pickup_* fields, and a courier_map entry for the chosen courier. Missing keys are listed in the error response.
  2. Builds the ClickPost v3 create-order payload:
    • pickup_info — from the vendor's pickup_* config fields.
    • drop_info — from the order's shipping address.
    • shipment_details — one item per order line carrying sku, description, quantity, price, and weight (from order_line.weightAtOrder, snapshotted at place-order; falls back to defaultParcel.weight when null). Total weight is the sum of per-line weights, falling back to defaultParcel.weight when every line is null. Parcel length / breadth / height come from defaultParcel unconditionally — 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) when payment_method === "cod". Set to 0 for prepaid orders.
    • courier_partner_id — the integer cpId from the vendor's courier_map for the chosen courier.
  3. Calls POST https://www.clickpost.in/api/v3/create-order/?username=<u>&key=<k> with Content-Type: application/json.
  4. Handles the response:
    • ClickPost always returns HTTP 200, even on errors. Success is signaled by meta.success being true and meta.status being 200, 102, or 202.
    • Status 200 — synchronous success. AWB is at result.waybill; label URL at result.label. Both are stamped onto the sub-order immediately.
    • Status 102 / 202async 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 (or meta.success === false) — business error. The error message from ClickPost is surfaced to the vendor.
  5. 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 fieldTypeReaches ClickPost?
weightintegerYes — 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 / heightintegerNo — centimetres, but the v3 payload takes dimensions only from defaultParcel.
countryOfOriginstringNo
midCodestringNo
hsnCodestringNo — snapshotted onto order_line at place-order and used for GST invoicing, not shipping.

CreateShipmentInput.items (built by OrderLineRepository.listForManifest) carries only sku, description, quantity, unitPriceSubunits, and weight, 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.


  • shipping — provider-agnostic config (enabledProviders, flat rate, tracking events). clickpost must appear in vendor.admin.shipping.enabled_providers for these credentials to be used at fulfill time. See shipping.md.
  • settingsvendor.admin.shipping.clickpost.* keys live in the vendor settings registry; this controller is a typed wrapper over vendor-settings. See settings.md.
  • orderPOST /vendor/orders/:id/fulfilled with providerId="clickpost" dispatches to ClickPostProvider.createShipment() which reads these credentials.

On this page