Supercommerce API Docs
Full Module Docs

Shipping Module

Two-layer shipping model. Customer-charge layer: every vendor has a flat per-order shipping rate (with optional free-above threshold) that the cart applies as a single line at…

Two-layer shipping model. Customer-charge layer: every vendor has a flat per-order shipping rate (with optional free-above threshold) that the cart applies as a single line at checkout — no provider involved. Provider layer: at the pending → fulfilled sub-order transition, the vendor assigns a registered shipping provider (e.g. clickpost, self-handled) which dispatches createShipment() and starts recording tracking events.

Sources:

  • api-modules/shipping — base module, registry, config, tracking, audit events. Registered via ShippingModule.forRoot({ providers: [...] }) in apps/api/src/app.module.ts.
  • api-modules/shipping-clickpost — concrete provider (ClickPost.in). Registers into the base module's SHIPPING_PROVIDERS token.
  • api-modules/shipping-self-handled — fallback "no courier" provider; manual delivery confirmation only.

The order module knows nothing about ClickPost specifically — it dispatches through ShippingRegistryService keyed by providerId. New couriers register a new ShippingProvider class against the registry.


Conventions

Authentication

Endpoint groupAuthPermission
GET /vendor/shipping/providers, GET /vendor/shipping/config, PATCH /vendor/shipping/configrequired (vendor session)active vendor in session
GET /vendor/shipping/clickpost/config, PATCH /vendor/shipping/clickpost/configrequired (vendor session)active vendor in session
GET /vendor/shipping/orders/:id/trackingrequired (vendor session)active vendor in session
GET /store/shipping/orders/:id/trackingrequired (customer)
GET /admin/vendors/:vendorId/shipping/configrequiredplatformVendorSetting: read
PATCH /admin/vendors/:vendorId/shipping/configrequiredplatformVendorSetting: update
POST /webhooks/shipping/clickpost/:vendorIdnone — HMAC over raw body, header x-clickpost-signature

:id on the tracking endpoints is order_vendor.id (a sub-order). Vendor-scoped reads enforce order_vendor.vendor_id = activeVendor; customer-scoped reads enforce order.customer_id = caller via join. Cross-scope ids return 404 Not Found (no row leak).

Response envelope

Successful responses are wrapped by ResponseInterceptor:

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

Error envelope

statusCodeerrorCode examples
400BAD_REQUEST, VALIDATION_ERROR
401UNAUTHORIZED (invalid HMAC signature)
403FORBIDDEN (no active vendor, or missing platform permission)
404NOT_FOUND (cross-vendor sub-order, unknown vendor webhook target)
500INTERNAL_SERVER_ERROR, DATABASE_ERROR

Currency

Customer-charge fields (flatRateSubunit, freeAboveSubunit) are integer subunits (paise / cents / eurocents — single currency per tenant). The cart applies flatRateSubunit per vendor unless the vendor's per-order subtotal meets or exceeds freeAboveSubunit (null = no free-above threshold).

Two-layer model — quick reference

ConcernLayerWhere
What does the customer pay for shipping?Customer-chargevendor/shipping/config (flatRateSubunit, freeAboveSubunit). Applied at the cart layer (cart.md), no provider involved
Which provider ships this sub-order?ProviderSelected at POST /vendor/orders/:id/fulfilled from this vendor's enabled providers. See order.md
How do tracking updates land?ProviderProvider-specific webhook (e.g. POST /webhooks/shipping/clickpost/:vendorId), normalized + persisted as shipping_event rows

Domain types

ShippingConfigResponse

The customer-charge config + the vendor's enabled provider list.

type ShippingConfigResponse = {
  enabledProviders: string[];      // provider ids; subset of the platform-allowed list
  flatRateSubunit: number;          // >= 0, in subunits
  freeAboveSubunit: number | null;  // >= 0, null = no free-above threshold
};

ShippingProviderSummary

type ShippingProviderSummary = {
  id: string;          // e.g. "clickpost", "self-handled"
  methods: string[];   // e.g. ["express", "standard"] or ["self"] for self-handled
};

ShippingEventResponse

type ShippingNormalizedStatus =
  | "pending"
  | "in_transit"
  | "out_for_delivery"
  | "delivered"
  | "failed"
  | "returned";

type ShippingEventResponse = {
  id: string;
  providerId: string;              // e.g. "clickpost"
  externalEventId: string | null;  // provider-side event id (used for idempotency)
  statusCode: string;              // raw provider code (e.g. "OFD")
  normalizedStatus: ShippingNormalizedStatus;
  payload: Record<string, unknown>; // full raw webhook payload
  receivedAt: string;              // ISO
};

ClickPostConfigResponse

type ClickPostConfigResponse = {
  apiKey: string;
  username: string;
  webhookSecret: string;
  pickupPincode: string;
  enabledCouriers: string[];
  // 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 — fall back
  // to manual instructions.
  webhookUrl: string | null;
};

Vendor

GET /vendor/shipping/providers — Providers I may use

Three-way intersection: registered ∩ admin allow-list ∩ vendor's enabled list. Use this to populate the bulk-fulfill provider picker. Paginated for consistency, but always returns the full set in one page (one row per provider).

Response 200 — paginated envelope of ShippingProviderSummary[].


GET /vendor/shipping/config — Customer-charge + enabled providers

Response 200

{
  "data": {
    "enabledProviders": ["clickpost", "self-handled"],
    "flatRateSubunit": 4900,
    "freeAboveSubunit": 99900
  },
  "message": "Success",
  "statusCode": 200
}

PATCH /vendor/shipping/config — Update shipping config

Partial update — fields not in the body are left untouched. Audit row written per changed key by VendorSettingsService.

Body

{
  "enabledProviders": ["clickpost"],
  "flatRateSubunit": 4900,
  "freeAboveSubunit": 99900
}
FieldTypeConstraints
enabledProvidersstring[]?At least one entry; each must be a registered provider id permitted by the admin allow-list
flatRateSubunitint?>= 0
freeAboveSubunitint | null?>= 0. null = no free-above threshold

Response 200 — updated ShippingConfigResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod; unknown providerId; provider not in admin allow-list
403FORBIDDENNo active vendor on session

GET /vendor/shipping/orders/:id/tracking — Sub-order timeline

Paginated, newest first. Provider-agnostic — every row that's been written to shipping_event for this sub-order, regardless of which provider produced it. Useful when a sub-order changes hands between providers mid-flight (rare but supported).

Path params

NameTypeNotes
idstringorder_vendor.id. Cross-vendor ids → 404

Query

NameTypeDefaultConstraints
pageint1>= 1
limitint501..200

Response 200 — paginated envelope of ShippingEventResponse[].


Storefront

GET /store/shipping/orders/:id/tracking — My sub-order timeline

Same shape and semantics as the vendor endpoint, but scope is resolved through the parent order's customer_id. Sub-orders for other customers return 404 Not Found.

Path params

NameTypeNotes
idstringorder_vendor.id. Cross-customer ids → 404

Admin

GET /admin/vendors/:vendorId/shipping/config — Read any vendor's config

Required permission: platformVendorSetting:read.

Response 200ShippingConfigResponse for the target vendor.


PATCH /admin/vendors/:vendorId/shipping/config — Override any vendor's config

Required permission: platformVendorSetting:update. Same body shape as the vendor self-service endpoint; bypasses any forAdmin write guard on the underlying vendor-settings store, so platform staff can set any registered key.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod
403FORBIDDENCaller lacks platformVendorSetting:update

ClickPost provider (shipping-clickpost)

A concrete ShippingProvider registered against the base shipping registry. Provider id is "clickpost".

Vendor: GET /vendor/shipping/clickpost/config

Response 200

{
  "data": {
    "apiKey": "ck_live_xxx...",
    "username": "vendor-shop",
    "webhookSecret": "shared-secret-32-chars-...",
    "pickupPincode": "560001",
    "enabledCouriers": ["DELHIVERY", "BLUEDART"],
    "webhookUrl": "https://api.example.com/webhooks/shipping/clickpost/01J9..."
  },
  "message": "Success",
  "statusCode": 200
}

webhookUrl is null when PUBLIC_API_BASE_URL isn't set on the API process — surface manual instructions in that case.


Vendor: PATCH /vendor/shipping/clickpost/config

Partial update. Audit row per changed key.

Body

{
  "apiKey":         "ck_live_xxx...",
  "username":       "vendor-shop",
  "webhookSecret":  "at-least-8-chars",
  "pickupPincode":  "560001",
  "enabledCouriers": ["DELHIVERY", "BLUEDART"]
}
FieldTypeConstraints
apiKeystring?1..500 chars, trimmed
usernamestring?1..200 chars, trimmed
webhookSecretstring?8..500 chars
pickupPincodestring?Indian 6-digit pincode (/^\d{6}$/)
enabledCouriersstring[]?Subset of platform-supported couriers (see below)

Supported couriers (universe — per-vendor filtering happens against enabledCouriers):

DELHIVERY, DELHIVERY_AIR, BLUEDART, BLUEDART_SURFACE,
EKART, DTDC, INDIA_POST, XPRESSBEES, ECOM_EXPRESS, SHADOWFAX

Response 200 — updated ClickPostConfigResponse.


Webhook: POST /webhooks/shipping/clickpost/:vendorId

Public ClickPost webhook. HMAC-verified against the per-vendor webhookSecret. Resolves the target sub-order from the AWB (scoped to the vendor), records the event idempotently (by (provider_id, external_event_id)), and — when the normalized status is delivered and the sub-order is fulfilled — auto-flips the sub-order via OrderService.markVendorDelivered.

Failure modes minimize information leakage: external messages stay generic; detail goes to the server log.

Path params

NameTypeNotes
vendorIdstringTarget vendor id

Headers

NameRequiredNotes
x-clickpost-signatureyesHMAC over the raw body

Body — ClickPost's tracking event payload (event id, AWB, status code, courier metadata). The handler extracts AWB + status code; the full body is stored as-is on the resulting shipping_event.payload.

Response 200

{
  "data": {
    "accepted": true,
    "eventId": "01J9...",
    "normalizedStatus": "out_for_delivery",
    "duplicate": false
  },
  "message": "Success",
  "statusCode": 200
}

duplicate: true means the (provider_id, external_event_id) pair already existed — the event is not re-recorded and auto-deliver is not re-attempted.

Errors

StatusCodeWhen
400BAD_REQUESTMissing rawBody; invalid JSON; payload missing AWB or status code
401UNAUTHORIZEDHMAC signature mismatch
404NOT_FOUNDNo vendor at :vendorId (no webhook secret configured); AWB doesn't resolve to a sub-order on this vendor

Status mapping

ClickPost status codes → normalized statuses (unmapped codes flow through as pending and are logged):

ClickPost codesnormalizedStatus
OM, OPpending
OS, OT, INTin_transit
OO, OFDout_for_delivery
DEL, ODdelivered
OR, RTO, RTDreturned
OND, OUD, OCfailed

Auto-deliver

When a webhook lands a delivered normalized status and the sub-order is currently fulfilled, the handler calls OrderService.markVendorDelivered(actorType="webhook", source="clickpost-webhook"). If the sub-order has since transitioned (e.g. customer already received it via vendor-panel mark-delivered, or it was cancelled), the order-side ConflictException is swallowed — the webhook still acks 200 with accepted: true.


Domain events

EventFired whenPayload
shipping.shipment.createdSub-order pending → fulfilled (provider assigned)order id, order-vendor id, vendor id, provider id, method, tracking code, AWB, occurred at
shipping.shipment.cancelledSub-order cancelled while it had a provider attachedorder-vendor id, provider id, reason
shipping.tracking.updatedA shipping_event row is persisted (any status)order-vendor id, provider id, normalized status, raw status code

Consumed by the notifications module (push + email — "your order is out for delivery", etc.) and by analytics jobs.


Configuration

Base shipping module

No env vars — vendor-side knobs are entirely in vendor settings (vendor.shipping.*).

ClickPost provider

Lives under the vendor settings shipping group as clickpost.* keys (read via VendorSettingsService):

KeyRequiredPurpose
shipping.clickpost.api_keyyes (for createShipment)Vendor's ClickPost API key
shipping.clickpost.usernameyesVendor's ClickPost username
shipping.clickpost.webhook_secretyes (for webhook auth)Shared HMAC secret. Must match the value configured in ClickPost's webhook settings
shipping.clickpost.pickup_pincodeyesIndian 6-digit pincode used as the shipment's pickup point
shipping.clickpost.enabled_couriersyesPer-vendor allow-list of courier codes the vendor wants exposed

The vendor's enabled-providers list also has to contain "clickpost" (/vendor/shipping/config) for the provider to be selectable at the fulfill step.

Process env

Env varDefaultPurpose
PUBLIC_API_BASE_URLUsed to render webhookUrl in GET /vendor/shipping/clickpost/config. Unset → null, and vendor docs should print manual instructions

  • order — owns the pending → fulfilled and fulfilled → delivered transitions, dispatches provider.createShipment() / provider.cancelShipment(). See order.md.
  • cart — applies the customer-charge layer per vendor at checkout (flatRateSubunit, freeAboveSubunit). See cart.md.
  • settings — provides VendorSettingsService (vendor-scope) and PlatformVendorSettingsService (admin override). See settings.md.

On this page