Supercommerce API Docs
Admin API

Shipping ClickPost Module — Admin surface

Admin-facing HTTP surface for the platform (central warehouse) ClickPost account — credentials, pickup address, courier map, parcel defaults, webhook secret, the live active-courier lookup, and automatic courier assignment. This is the account used when an admin fulfills a sub-order with credentialSource=platform.

Admin-facing HTTP surface for the platform (central warehouse) ClickPost account — credentials, pickup address, courier partner map, parcel defaults, webhook secret, and the live active-courier lookup. This is the account used when an admin fulfills a sub-order with credentialSource=platform; sellers shipping under their own ClickPost account configure the mirror surface in vendor/shipping-clickpost.md.

Source: api-modules/shipping-clickpost/src/controllers/admin-clickpost-config.controller.ts.

The endpoints are a typed wrapper over the admin.shipping.clickpost.* settings group. They exist as a dedicated surface because clickpost.courier_map is a record of objects — the generic settings editor can edit an existing value but cannot add keys to it.


Conventions

Authentication

All endpoints require a Better-Auth admin session and a role granting the matching permission. Uses the adminSetting resource — the same resource that gates the underlying settings group, so no separate grant is needed to manage ClickPost.

EndpointPermission
GET /admin/shipping/clickpost/configadminSetting: read
GET /admin/shipping/clickpost/active-couriersadminSetting: read
PATCH /admin/shipping/clickpost/configadminSetting: update

Response envelope

{
  "data": <payload>,
  "message": "Success",
  "statusCode": 200,
  "metadata": { /* optional */ }
}

Error envelope

statusCodeerrorCode examples
400BAD_REQUEST, VALIDATION_ERROR
401UNAUTHORIZED
403FORBIDDEN
500INTERNAL_SERVER_ERROR

Domain types

Identical to the vendor surface — see vendor/shipping-clickpost.md for the full field-by-field notes on ClickPostCourierEntry, weights/dimensions units, and where to obtain cpId / accountCode.

ClickPostConfigResponse

type ClickPostConfigResponse = {
  apiKey: string;
  username: string;
  webhookSecret: string;
  pickupPincode: string;                   // Indian 6-digit
  enabledCouriers: string[];
  courierMap: Record<string, { cpId: number; accountCode: string }>;

  pickupName: string;
  pickupAddress: string;
  pickupCity: string;
  pickupState: string;
  pickupPhone: string;
  pickupEmail: string;
  pickupTin: string;   // TIN / GSTIN

  defaultParcel: {
    weight: number;    // grams
    length: number;    // centimetres
    breadth: number;
    height: number;
  } | null;

  /** Computed from PUBLIC_API_BASE_URL + the platform webhook route.
   *  Null when PUBLIC_API_BASE_URL isn't set on the API process. */
  webhookUrl: string | null;
};

Unlike the vendor variant, webhookUrl resolves to the platform route (/webhooks/shipping/clickpost/platform), not a per-vendor one.

ActiveCourier

type ActiveCourier = {
  code: string;         // courier code derived from the name, e.g. "DELHIVERY"
  courierName: string;  // name as shown on the ClickPost dashboard
  cpId: number;
  accountCode: string;
};

Platform ClickPost config

Base path: /admin/shipping/clickpost.

GET /admin/shipping/clickpost/config — Get config

Returns the platform warehouse ClickPost credentials, pickup address, courier map, parcel defaults, and the resolved platform webhook URL.

Response 200ClickPostConfigResponse.

{
  "data": {
    "apiKey": "cp_live_...",
    "username": "acme-warehouse",
    "webhookSecret": "whsec_...",
    "pickupPincode": "560001",
    "enabledCouriers": ["DELHIVERY", "BLUEDART"],
    "courierMap": {
      "DELHIVERY": { "cpId": 4, "accountCode": "DL_ACME_002" },
      "BLUEDART": { "cpId": 12, "accountCode": "BD_ACME_001" }
    },
    "pickupName": "Acme Central Warehouse",
    "pickupAddress": "123 Industrial Area",
    "pickupCity": "Bengaluru",
    "pickupState": "Karnataka",
    "pickupPhone": "9876543210",
    "pickupEmail": "dispatch@acme.com",
    "pickupTin": "29ABCDE1234F1Z5",
    "defaultParcel": { "weight": 500, "length": 20, "breadth": 15, "height": 10 },
    "webhookUrl": "https://api.example.com/webhooks/shipping/clickpost/platform"
  },
  "message": "Success",
  "statusCode": 200
}

Errors

StatusCodeWhen
403FORBIDDENCaller lacks adminSetting:read

GET /admin/shipping/clickpost/active-couriers — Active couriers

Calls ClickPost's fetch-accounts API with the saved platform API key and returns every active account, each with its cpId and accountCode. The code is derived from the account's courier name (uppercased, separators → _), falling back to a canonical spelling when we already have one for that courier — so activating a new courier partner on the ClickPost dashboard makes it selectable here with no code change. The config UI uses this to offer only couriers that will actually work and to prefill the courier map.

Returns an empty array — never an error — when the API key isn't saved yet or ClickPost is unreachable, so an optional lookup never breaks the page.

Response 200ActiveCourier[].

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

Errors

StatusCodeWhen
403FORBIDDENCaller lacks adminSetting:read

PATCH /admin/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. Each field maps to a setting under admin.shipping.clickpost.* and is written via SettingsService.setMany (audit row per changed key, one transaction).

Body — same shape as the vendor PATCH:

{
  "apiKey": "cp_live_...",
  "username": "acme-warehouse",
  "webhookSecret": "whsec_...",
  "pickupPincode": "560001",
  "enabledCouriers": ["DELHIVERY", "BLUEDART"],
  "courierMap": {
    "DELHIVERY": { "cpId": 4, "accountCode": "DL_ACME_002" },
    "BLUEDART": { "cpId": 12, "accountCode": "BD_ACME_001" }
  },
  "pickupName": "Acme Central Warehouse",
  "pickupAddress": "123 Industrial Area",
  "pickupCity": "Bengaluru",
  "pickupState": "Karnataka",
  "pickupPhone": "9876543210",
  "pickupEmail": "dispatch@acme.com",
  "pickupTin": "29ABCDE1234F1Z5",
  "defaultParcel": { "weight": 500, "length": 20, "breadth": 15, "height": 10 }
}
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
courierMapRecord<string, { cpId: number; accountCode: string }>?cpId positive int; accountCode 1..120 charsclickpost.courier_map
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 charsclickpost.pickup_tin
defaultParcel{ weight, length, breadth, height }?All four positive integers; weight in grams, rest in cm. Required in practice — see belowclickpost.default_parcel

defaultParcel is not optional in practice. getCredentials rejects a config without it, and buildCreateOrderPayload sends length / breadth / height from it on every shipment — ClickPost's v3 payload carries no per-item dimensions, so this is the only source. Only weight behaves as a true fallback (item.weight ?? defaultParcel.weight).

enabledCouriers gates nothing on the server. It drives the config UI's courier picker, but no code path validates a shipment against it: resolveCourier and methodsForPlatform both key off courierMap. A courier is usable when it has a courierMap entry, whether or not it appears in enabledCouriers.

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

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod, or a value fails its registry schema
403FORBIDDENCaller lacks adminSetting:update

Fulfillment flow

When an admin marks a sub-order fulfilled via POST /admin/orders/vendors/:orderVendorId/fulfilled with providerId="clickpost" and credentialSource="platform", ClickPostShippingProvider.createShipment reads these settings instead of the vendor's. credentialSource="vendor" reads the seller's own vendor.admin.shipping.clickpost.* keys.

GET /admin/orders/vendors/:orderVendorId/fulfillment-options returns both accounts with their selectable couriers; the platform account's list is the keys of the courierMap configured here — so a courier with no mapping simply cannot be picked.

The remaining mechanics (v3 payload construction, async 102/202 handling, variant weight/dimension fallbacks) are identical to the vendor flow — see vendor/shipping-clickpost.md.


Automatic courier assignment

Off by default. When enabled, an order that is placed has a courier chosen for it straight away — but nothing is booked until an operator says so.

The two steps

StepTriggerWhat happensCost
Assignorder.placedClickPost's recommendation API is asked which of the operator's active accounts service this pickup → drop lane. The best-ranked one is recorded against the sub-order.None — no waybill is created
ConfirmAn operator clicks Confirm shipmentThe order is created on ClickPost with the assigned account and an AWB comes back.A real, billable shipment

The sub-order stays pending through both. It becomes fulfilled only when ClickPost reports the courier collected the parcel (status code 4, PickedUp), so the customer's "shipped" notification lines up with the parcel actually moving.

Assignment runs for sub-orders processed on the platform (central warehouse) account. Sub-orders a seller ships under their own ClickPost setup keep the manual courier picker.

Settings

Both live on Admin → Settings → Shipping.

KeyTypeDefaultMeaning
clickpost.auto_assign_enabledbooleanfalseMaster switch. Off, nothing is queued and nothing changes.
clickpost.auto_assign_accountenumplatformWhich account to book against. Only platform is supported.

The recommendation call

POST https://www.clickpost.in/api/v1/recommendation_api/?key=<API_KEY>

Two things differ from every other ClickPost endpoint we call:

  • It authenticates with key alone — passing username is wrong here.
  • The request body is a JSON array, not an object.

length, breadth, height and weight are documented as optional but are always sent from clickpost.default_parcel: without them ClickPost omits shipping_charge, which is the figure that makes an assignment reviewable.

An unserviceable lane comes back as HTTP 200 with meta.success: false and "Pin code not serviceable" — a verdict, not an outage. It is recorded on the assignment as a recommendation failure and not retried, because retrying cannot change the answer. Timeouts and 5xx are retried with backoff.

Every attempt is on the order log

Assignment runs unattended, so each decision it makes — including each decision to do nothing — writes one row to the order's event log, readable at GET /admin/orders/:id/events and on the order's Events tab. Without them "the courier was never picked" has no visible cause.

eventTypeWhen
shipment.auto_assign_queuedA job was enqueued for the order or one bag.
shipment.auto_assign_succeededA carrier was picked. metadata carries cpId, accountCode, cpName, shippingChargeRupees and the runners-up.
shipment.auto_assign_skippedDeliberately did nothing. changes.reason says which: feature_disabled, vendor_account_selected, vendor_fulfilled, not_configured (with the missing setting keys), order_not_found, order_cancelled, sub_order_not_found, no_pending_bags, bag_already_dispatched, already_claimed.
shipment.auto_assign_failedAn attempt failed. changes.reason is the message, metadata.stage is enqueue, recommendation or job, and metadata.request is the parcel we quoted.

A ClickPost verdict also carries metadata.diagnostics — the provider's own response body — so an unserviceable lane can be diagnosed without a replay. Only the final job attempt logs stage: "job"; the retries in between are already on the order as the stage that raised them.

Courier map is bypassed

Assignment books on the raw cp_id + account_code the recommendation returns, so it can pick an account that was never added to clickpost.courier_map. Those two identifiers are authoritative; the method code stored alongside them is for display and is derived from the account_code (not cp_name, which would collapse several accounts of one carrier into a single name).

Because such a courier is absent from the picker's method list, fulfillment-options adds it explicitly — otherwise the picker would drop the selection and silently swap the carrier.

When a carrier declines

ClickPost ranks several carriers, and a booking that the top one rejects is offered to the next — under the same reference_number, which is what ClickPost asks for when retrying one shipment across carriers. The runners-up are stored on the assignment at recommendation time, so the fallback needs no second recommendation call.

At most three carriers are tried. The bound matters: a malformed payload — a bad drop pincode, say — is refused by every carrier in turn, and without a limit that reads as "nobody would take it" rather than as the payload problem it is. Each refusal is recorded on the assignment's attempts, so the first carrier's reason survives even when a later one succeeds.

Only a carrier declining triggers the fallback. A timeout, an unreachable upstream or an unpaid order would fail identically at the next carrier, so those stop immediately.

Packing: one parcel per bag, or per order

admin.shipping.shipment_grouping decides how a multi-vendor order is handed to the courier.

ValueBehaviour
per_vendor (default)One shipment per vendor bag. Right when sellers ship from their own warehouses. Unchanged from before this setting existed.
per_orderOne shipment for the whole order — one waybill, one label, one reference. Only coherent when every bag leaves from the same warehouse.

In per_order mode the recommendation is asked once per order, using the combined weight and the order's total, because that is the parcel that will actually be booked. Booking then writes a single shipment against the order and mirrors the waybill onto every bag it covers, so tracking, returns and the existing admin surfaces keep resolving shipping the way they always have.

Each sub-order keeps its own fulfilment status, payouts and returns — vendor accounting still needs them. Only the shipment consolidates.

Consequences worth knowing:

  • The courier receives the order's invoice value and, for COD, the full amount due at the door. The label declares the same, so the collected sum matches what is in the box.
  • invoice_number is order-scoped, drawn from its own sequence. Per-vendor tax invoices are unaffected — a parcel carrying several suppliers' goods still has one tax invoice each.
  • One tracking callback advances every bag the waybill covers.
  • Cancelling one bag voids the shared waybill and re-books the remainder, since both the contents and the COD total have changed.
  • The reference carries 0 in the vendor position — ORD-26-00000001-0-1 — marking a parcel that belongs to no single vendor.

The manual Fulfill action always acts on a single bag, whichever mode is set: it is a per-bag control, and it stays the way out when a consolidated booking is rejected.

Outcomes

The assignment row carries one of:

StatusMeaning
assignedA courier is chosen and the shipment is awaiting confirmation.
bookedConfirmed — a shipment exists and an AWB is live.
failedNothing serviceable, or the carrier rejected the booking. failure_stage says which step.
skippedClickPost is not configured on this deployment. Nothing is wrong.

Failures never block an order and never block fulfilment: the manual picker keeps working exactly as it did before.

Printing labels

A confirmed shipment has an AWB while its sub-order is still pending, which is precisely when its label needs printing. Admin → Print Labels therefore lists orders with a booked shipment rather than fulfilled ones. ClickPost's own label URL is kept on the shipment as a per-shipment fallback for couriers that reject third-party artwork.

Shipment references

reference_number is unique per shipment and capped at 20 characters. It is ORD-<yy>-<serial>-<vendorIndex>-<dispatchSequence> — e.g. ORD-26-00000001-1-1. The dispatch counter matters: a re-dispatch after an RTO is a new shipment and needs a new reference, and ClickPost answers a reused one with status 323 reported as success, which would silently attach the earlier dispatch's waybill. The same reference is used for the recommendation and the later order creation, as ClickPost advises.


Webhook receiver

Tracking events for platform-fulfilled bags arrive at POST /webhooks/shipping/clickpost/platform, HMAC-verified against admin.shipping.clickpost.webhook_secret and resolved by AWB globally (not vendor-scoped). See webhooks/shipping-clickpost.md.

A pending sub-order holding a live AWB is promoted to fulfilled on the first checkpoint that shows the courier has the parcel — 4 PickedUp normally, but any of in-transit, out-for-delivery or delivered will do it. That fallback matters: without it a missed or out-of-order pickup scan would strand the order in pending for good, with no delivered transition, no delivered mail, no rewards and no payout. Returns are deliberately excluded, since backfilling one would email the customer that an order shipped as it comes back.

Checkpoint times on those pushes are read as wall clock in admin.shipping.clickpost.webhook_timezone (default Asia/Kolkata) — ClickPost sends the courier's local time suffixed Z. The key is edited on Admin → Settings → Shipping rather than through the endpoints above, and covers the per-vendor webhooks too.


Rollout

Deploying automatic assignment or per-order grouping has a required order — check the database, migrate, deploy the worker, configure, then enable. See ClickPost automation — rollout.

  • shipping — provider-agnostic config; clickpost must appear in admin.shipping.enabled_providers. See admin/shipping.md.
  • settingsadmin.shipping.clickpost.* keys live in the admin settings registry under the shipping group; these endpoints are a typed wrapper. See admin/settings.md.
  • order — admin fulfillment dispatch and the credentialSource choice. See admin/order.md.
  • admin-rbac — gates every endpoint via adminSetting:*. See admin/admin-rbac.md.

On this page