Supercommerce API Docs
Vendor API

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…

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 layer is provider-agnostic and lives in this module; the provider integrations themselves are separate plugin modules (e.g. shipping-clickpost, shipping-self-handled). The two-layer model is intentional — customer pays a per-vendor flat rate (cart-side, plugin-free); vendor assigns a provider at the pending→fulfilled transition (post-order, plugin-driven).

Source: api-modules/shipping/src/controllers/vendor-shipping.controller.ts, api-modules/shipping/src/controllers/vendor-shipping-tracking.controller.ts.


Conventions

Authentication

All 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. The tracking endpoint joins against order_vendor.vendor_id so cross-vendor order_vendor.ids return 404 rather than 403 (no row leak).

Response envelope

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

Money

Charge amounts (flatRateSubunit, freeAboveSubunit) are integer subunits (paise / cents / eurocents).

Error envelope

statusCodeerrorCode examples
400BAD_REQUEST, VALIDATION_ERROR
401UNAUTHORIZED
403FORBIDDEN (no active vendor)
404NOT_FOUND
500INTERNAL_SERVER_ERROR

Domain types

ShippingProviderSummary

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

ShippingConfigResponse

type ShippingConfigResponse = {
  enabledProviders: string[];              // provider ids this vendor has switched on
  flatRateSubunit: number;                 // customer-charge rate, subunits
  freeAboveSubunit: number | null;         // null = no free-above threshold
};

ShippingNormalizedStatus

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

ShippingEventResponse

type ShippingEventResponse = {
  id: string;
  providerId: string;
  externalEventId: string | null;          // provider's event/webhook id (de-dup)
  statusCode: string;                      // provider's raw status string
  normalizedStatus: ShippingNormalizedStatus;
  payload: Record<string, unknown>;        // provider-specific raw body
  receivedAt: string;                      // ISO
};

Shipping providers

Base path: /vendor/shipping/providers.

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

Three-way intersection: registered providers (modules wired in ShippingModule.forRoot()) ∩ admin allow-list (platform-level admin.shipping.enabled_providers) ∩ vendor's enabled list (this vendor's admin.shipping.enabled_providers). Use it to populate the bulk-fulfill provider picker.

The list is small (one row per registered provider). The response is wrapped in the paginated envelope for consistency but always returns the full set in a single page.

Response 200 — paginated envelope of ShippingProviderSummary.

{
  "data": [
    { "id": "clickpost", "methods": ["express", "surface"] },
    { "id": "self-handled", "methods": ["standard"] }
  ],
  "metadata": { "total": 2, "items": 2, "perPage": 2, "currentPage": 1, "lastPage": 1 }
}

Errors

StatusCodeWhen
403FORBIDDENNo active vendor on session

Shipping config

Base path: /vendor/shipping/config. Reads/writes the three shipping settings under vendor.admin.shipping.* (see settings.md). Audit rows are written per changed key by VendorSettingsService.

GET /vendor/shipping/config — Current shipping config

Response 200ShippingConfigResponse.

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

Errors

StatusCodeWhen
403FORBIDDENNo active vendor on session

PATCH /vendor/shipping/config — Update shipping config

Partial update — fields not in the body are left untouched. The body is .strict() so unknown keys are rejected at the zod layer.

Body

{
  "enabledProviders": ["clickpost", "self-handled"],
  "flatRateSubunit": 4900,
  "freeAboveSubunit": 99900                // nullable — send null to drop the free-above threshold
}
FieldTypeConstraints
enabledProvidersstring[]?At least 1 entry when present; each entry must be a known provider id (and pass the admin allow-list at order time)
flatRateSubunitint?>= 0
freeAboveSubunitint | null?>= 0; null to disable free-above

Response 200 — updated ShippingConfigResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod (unknown key, negative amount, empty enabledProviders[])
403FORBIDDENNo active vendor on session

Tracking timeline

Base path: /vendor/shipping/orders/:id/tracking. :id is order_vendor.id.

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

Returns the shipping event timeline for one sub-order, newest first. Provider-agnostic — any provider that lands rows on shipping_event shows up here.

Path params

NameTypeNotes
idstring (UUID)order_vendor.id; must belong to active vendor

Query

NameTypeDefaultNotes
pageint1>= 1
limitint501..200

Response 200 — paginated envelope of ShippingEventResponse.

{
  "data": [
    {
      "id": "01J9...",
      "providerId": "clickpost",
      "externalEventId": "evt_abc123",
      "statusCode": "IT",
      "normalizedStatus": "in_transit",
      "payload": { /* provider-specific */ },
      "receivedAt": "2026-05-12T08:01:11.000Z"
    }
  ],
  "metadata": { "total": 4, "items": 4, "perPage": 50, "currentPage": 1, "lastPage": 1 }
}

Errors

StatusCodeWhen
404NOT_FOUNDSub-order does not exist or is not owned by the active vendor

  • settings — config is persisted under vendor.admin.shipping.*; the vendor-settings controller can read/write the same data with the generic settings API. See settings.md.
  • shipping-clickpost — provider plugin for ClickPost; its own config block lives at vendor.admin.shipping.clickpost.* and has a dedicated controller. See shipping-clickpost.md.
  • shipping-self-handled — built-in fallback provider; no per-vendor config.
  • orderPOST /vendor/orders/:id/fulfilled validates providerId against the list returned by /vendor/shipping/providers before dispatching createShipment(). See order.md.

On this page