Supercommerce API Docs
Vendor API

Order Module — Vendor surface

Vendor-facing HTTP surface for sub-order fulfillment (assign shipping provider, mark fulfilled, mark delivered, cancel, correct the delivery address), returns processing (approve / reject / pickup / receive /…

Vendor-facing HTTP surface for sub-order fulfillment (assign shipping provider, mark fulfilled, mark delivered, cancel), returns processing (approve / reject / pickup / receive / QC), and payouts (balance, ledger, and payout history). A parent order row can have one or more order_vendor sub-orders; the vendor surface only ever sees its own sub-orders and the customer's shipping address — never other vendors' slices, billing address, or the parent's payment internals.

Source: api-modules/order/src/controllers/vendor-orders.controller.ts, api-modules/order/src/controllers/vendor-returns.controller.ts, api-modules/order/src/controllers/vendor-payouts.controller.ts.


Conventions

Authentication

All endpoints require a Better-Auth bearer session with an active vendor.

Authorization: Bearer <session-token>

The vendor is resolved via resolveActiveVendorId(session) — prefers session.session.activeOrganizationId (better-auth canonical) and falls back to activeVendorId. Sessions missing an active vendor are rejected with 403 Forbidden (FORBIDDEN).

Tenant scoping

:id on order endpoints is order_vendor.id (a sub-order). :id on return endpoints is return_request.id. :id on payout endpoints is vendor_payout.id. Ids that belong to a different vendor return 404 Not Found, not 403 — no leak of foreign ids.

Response envelope

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

Error envelope

statusCodeerrorCode examples
400BAD_REQUEST, VALIDATION_ERROR
401UNAUTHORIZED
403FORBIDDEN
404NOT_FOUND
409CONFLICT, INVALID_TRANSITION, SUB_ORDER_NOT_CANCELLABLE
500INTERNAL_SERVER_ERROR, DATABASE_ERROR

Money + dates

All monetary fields (subtotals, totals, refund amounts, ledger amounts, etc.) are integer subunits (paise / cents / eurocents — single currency per tenant). Date fields are ISO datetimes.

Audit trail

Every status mutation writes one or more rows to order_events in the same transaction as the change. Each row carries actor_type (user / vendor / admin / system / webhook), actor_id, source, structured changes, and optional metadata. The most recent rows ship inline on detail responses (events[]).


Domain types

Sub-order lifecycle — OrderVendorStatus

type OrderVendorStatus = "pending" | "fulfilled" | "delivered" | "cancelled";
StatusSet byReversibleNotes
pendingcreated at place-orderyesawaiting vendor to assign a shipping provider
fulfilledPOST /vendor/orders/:id/fulfilled (or bulk-fulfill)partialprovider/method/tracking persisted; cancel still allowed (courier rejected, RTO)
deliveredPOST /vendor/orders/:id/delivered (or courier webhook)nostamps delivered_at. COD stays payment_status=pending until admin mark-paid (cash collected)
cancelledPOST /vendor/orders/:id/cancel, parent cancel cascadenoreleases this vendor's reservation, and restocks it if the bag was still pending and already paid for; if all siblings cancelled, parent auto-cancels

VendorOrderResponse

Vendor-scoped sub-order shape. Strips other vendors' sub-orders and the customer's billing address — vendors only see their slice + shipping address.

type TaxComponent = { type: string; rate: number; amount: number };

type VendorOrderResponse = {
  id: string;                              // order_vendor.id
  orderId: string;
  orderNumber: string;
  customer: { id: string; name: string; email: string } | null; // The customer account that placed the order; null for legacy/guest orders.
  parentStatus: "pending_payment" | "confirmed" | "fulfilled" | "delivered" | "cancelled" | "refunded";
  fulfillmentStatus: OrderVendorStatus;

  // What this seller may do to the order, decided server-side — the panel
  // renders from these rather than re-deriving the rule.
  canEditShippingAddress: boolean;  // sole vendor on the order AND nothing booked
  canAdvanceFulfillment: boolean;   // not store-processed, AND (per-vendor grouping OR sole vendor)

  subtotal: number;                        // subunits
  discountAllocated: number;
  shippingCost: number;
  taxAmount: number;
  total: number;

  shippingProviderId: string | null;       // null while pending
  shippingMethod: string | null;
  trackingCode: string | null;
  awbNumber: string | null;

  taxBreakdown: TaxComponent[];            // aggregated across lines + shipping
  shippingNetAmount: number | null;
  shippingTaxBreakdown: TaxComponent[];

  shippingAddress: {
    firstName: string; lastName: string;
    fullAddress: string;
    city: string; pincode: string; state: string;
    phone: string; country: string;
  };
  customerNote: string | null;             // parent order's shopper instruction; null when none

  lines: OrderLineResponse[];
  events: OrderEventResponse[];

  fulfilledAt: string | null;              // ISO
  deliveredAt: string | null;
  cancelledAt: string | null;
  cancellationReason: string | null;
  placedAt: string;
};

OrderLineResponse

type OrderLineResponse = {
  id: string;
  vendorId: string;
  variantId: string | null;
  productId: string | null;
  productSlug: string | null;              // CURRENT store slug, resolved live for `/product/{slug}` links; null if not store-visible
  sku: string;
  productNameAtOrder: string;
  variantNameAtOrder: string | null;
  imageAtOrder: string | null;
  hsnCodeAtOrder: string | null;           // snapshotted from variant at place-order
  type: "product" | "gift" | "shipping";   // see order.md for full type set
  quantity: number;
  unitPrice: number;
  lineSubtotal: number;
  discountAllocated: number;
  lineTotal: number;
  netAmount: number | null;                // pre-tax portion (null when vendor has no taxes)
  taxBreakdown: TaxComponent[];
};

ReturnResponse

type ReturnResponse = {
  id: string;
  returnNumber: string;
  orderId: string;
  orderVendorId: string;
  customerId: string | null;
  customer: { id: string; name: string; email: string } | null;
  vendorId: string;
  type: string;                            // e.g. "refund", "exchange"
  status: string;                          // requestedapprovedpicked_upreceivedqc_passed/qc_failedrefunded | rejected | cancelled
  reasonCode: string | null;
  reasonNotes: string | null;
  refundAmount: number;                    // subunits
  refundedAmount: number;
  externalRefundReference: string | null;
  shippingProvider: string | null;         // not currently populated by any flowalways null today
  awbNumber: string | null;
  trackingCode: string | null;
  rejectionReason: string | null;
  qcFailureReason: string | null;
  requestedAt: string;                     // ISO
  approvedAt: string | null;
  rejectedAt: string | null;
  pickedUpAt: string | null;
  receivedAt: string | null;
  qcPassedAt: string | null;
  qcFailedAt: string | null;
  refundedAt: string | null;
  cancelledAt: string | null;
  lines: ReturnLineResponse[];
  photos: ReturnPhotoResponse[];
};

type ReturnLineResponse = {
  id: string;
  orderLineId: string;
  variantId: string | null;
  productId: string | null;
  sku: string;
  productNameAtOrder: string;
  variantNameAtOrder: string | null;
  imageAtOrder: string | null;
  quantity: number;
  unitPrice: number;
  taxPortion: number;
  lineRefundAmount: number;
  reasonCode: string | null;
  reasonNotes: string | null;
  restocked: boolean;
};

type ReturnPhotoResponse = {
  id: string;
  storageKey: string;
  contentType: string | null;
  fileSizeBytes: number | null;
  sortOrder: number;
  uploadedAt: string;
};

Payouts — VendorBalanceResponse, LedgerEntryResponse, PayoutResponse

type VendorBalanceResponse = {
  vendorId: string;
  pending: number;                         // 'pending' ledger entries (return window not elapsed)
  available: number;                       // 'available' entries not yet on a draft payout
  lifetimeEarned: number;
  lifetimeRefunded: number;
  lifetimePaidOut: number;
  payoutHold: boolean;                     // vendor.payouts.payout_hold flag
  commissionRate: number;                  // basis points (10000 = 100.00%)
};

type LedgerEntryResponse = {
  id: string;
  vendorId: string;
  kind: "sale" | "refund" | "manual" | "commission_adjustment";
  status: "pending" | "available" | "paid_out" | "cancelled";
  grossAmount: number;
  commissionRate: number;                  // basis points
  commissionAmount: number;
  netAmount: number;
  orderId: string | null;
  orderVendorId: string | null;
  orderReturnId: string | null;
  payoutId: string | null;
  pendingUntil: string | null;
  availableAt: string | null;
  paidOutAt: string | null;
  cancelledAt: string | null;
  description: string | null;
  createdAt: string;
};

type PayoutResponse = {
  id: string;
  payoutNumber: string;
  vendorId: string;
  status: "pending" | "paid" | "cancelled" | "failed";
  periodStart: string;
  periodEnd: string;
  grossTotal: number;
  commissionTotal: number;
  netTotal: number;
  entryCount: number;
  bankAccountId: string | null;
  bankReference: string | null;
  notes: string | null;
  createdAt: string;
  paidAt: string | null;
  cancelledAt: string | null;
  entries?: LedgerEntryResponse[];         // only on detail
};

Vendor sub-orders

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

GET /vendor/orders — List my sub-orders

Paginated, newest placed first unless sortBy says otherwise. Always scoped to the caller's active vendor: the vendor predicate is applied first and unconditionally, so no combination of filters can reach another tenant's rows.

status filters the seller's own bag (order_vendor.fulfillment_status); every other filter reaches through to the parent order, which is why orderStatus is named apart from it.

Query

NameTypeDefaultNotes
pageint1>= 1
limitint201..100
statusOrderVendorStatus?The seller's own bag state
orderStatusOrderStatus?The parent order's lifecycle status
searchstring?Order-number search. The ORD-<year>- prefix and the serial's leading zeros are both optional
customerSearchstring?Case-insensitive partial match on the order's shipping first name, last name or phone, or the linked account's name or email
productSearchstring?Matches a product name or SKU among this bag's own lines. Correlated on order_vendor_id, not order_id, so a seller's search never matches because another vendor's half of the same order holds the product
dateField"paidAt" | "createdAt" | "updatedAt"?Which of the parent order's timestamps the range filters on. createdAt means the placed date
dateFromstring? (ISO datetime)Inclusive lower bound. Ignored without dateField
dateTostring? (ISO datetime)Inclusive upper bound
sortBy"paidAt" | "createdAt" | "updatedAt"?createdAtParent-order timestamp the list is ordered by. Rows with no value sort last in either direction
sortDirection"asc" | "desc"?desc
excludeRefundedboolean?Drops bags whose parent order was fully refunded. partially_refunded is kept — see the admin note for why
excludeUnpaidboolean?Drops bags whose parent order is confirmed but still unpaid, keeping Cash-on-Delivery — COD stays pending until the courier remits the cash and still has to be shipped. See the admin note for why

The New Orders queue in the seller panel is these filters composed: status=pending&orderStatus=confirmed&excludeRefunded=true&excludeUnpaid=true&sortBy=paidAt&sortDirection=desc.

Response 200 — paginated envelope of VendorOrderResponse.


Order exports

Base path: /vendor/orders/exports. The seller-side counterpart of the admin order export, scoped to the active vendor.

Ownership is taken from the session on every call and never from the request body — there is no vendorId field to send. It is stored on order_export.vendor_id as the row's owner, deliberately separate from the admin filter of the same name: an admin export filtered to one vendor carries whole-order money and every other supplier's rows, and must never surface in that vendor's own list. A mismatched id reads as 404, not 403, so an id cannot be probed for existence.

POST /vendor/orders/exports — Queue an export

Body — the same filters as GET /vendor/orders (status, orderStatus, search, customerSearch, productSearch, dateField, dateFrom, dateTo, excludeRefunded, excludeUnpaid), plus format ("csv" | "xlsx", default csv). Omitting every filter exports all of the seller's orders.

Response 201OrderExport with status: "pending".

GET /vendor/orders/exports — My export history

GET /vendor/orders/exports/:id — Export status

GET /vendor/orders/exports/:id/download — Download the file

Columns written (one row per item; money in major units, datetimes as YYYY-MM-DD HH:mm:ss):

Order columns — Order No · Placed At · Paid At · Updated At · Order Status · Payment Status · Fulfillment Status · Platform · Customer Name · Customer Email · Shipping Name · Shipping Phone · Shipping Address · Shipping City · Shipping State · Shipping Pincode · Shipping Country · Customer Note · My Subtotal · My Discount · My Shipping · My Tax · My Total · Courier · Courier Status · AWB Number · Tracking Code · Fulfilled At · Delivered At · Cancelled At · Cancellation Reason

Item columns — Item SKU · Item Name · Item Variant · Item Type · Item HSN · Item Quantity · Item Unit Price · Item Discount · Item Total

The column set is a strict subset of what VendorOrderResponse already exposes, so the export never widens a seller's view of an order. Specifically, and unlike the admin file:

  • money is the seller's own bag allocation (My …), never the whole-order subtotal, grand total or refunded amount — those would reveal what the customer paid other suppliers on a shared order;
  • no billing address, customer IP, cart id or redeemed points, none of which reach the vendor surface at all;
  • no payment provider or method. Payment Status is included because the vendor orders report already carries it.

Every row belongs to the requesting seller, so there is no vendor column, and Fulfillment Status is this bag's own rather than a comma-joined list.

The My … columns repeat on every row of a multi-item order, so they must not be summed across one — sum Item Total instead.


GET /vendor/orders/:id — Sub-order detail

Returns the active vendor's VendorOrderResponse for one sub-order.

Errors

StatusCodeWhen
404NOT_FOUNDSub-order does not belong to active vendor

POST /vendor/orders/:id/cancel — Cancel sub-order

Allowed from pending (declining fulfillment) or fulfilled (courier rejected, package returned, etc.). delivered and cancelled reject. For fulfilled cancels, a non-empty reason is required.

Body

{ "reason": "Out of stock at warehouse" }
FieldTypeConstraints
reasonstring?Trimmed; 1..500 chars. Required when sub-order is fulfilled

Response 200 — updated VendorOrderResponse with fulfillmentStatus="cancelled" and cancelledAt stamped.

Side effects

  • Releases this vendor's inventory reservation (no-op once it has been committed).
  • Restocks the bag when it was cancelled out of pending and its reservation had already been committed at payment — nothing shipped, so an order_cancelled movement puts the units back on hand. A fulfilled cancel is not restocked; those goods return through return → QC pass.
  • Fires the shipping registry's cancelShipment() hook (best-effort courier void).
  • Writes an audit row + emits order.vendor.cancelled.
  • If every sibling sub-order is now cancelled, the parent order auto-cancels.

Errors

StatusCodeWhen
404NOT_FOUNDSub-order not owned by vendor
409SUB_ORDER_NOT_CANCELLABLEStatus is delivered or already cancelled
400VALIDATION_ERRORCancelling a fulfilled sub-order without a reason
409SUB_ORDER_CHANGED_CONCURRENTLYThe bag was fulfilled between the eligibility check and the write. Nothing was cancelled — reload and retry

PATCH /vendor/orders/:id/shipping-address — Correct the delivery address

:id is order_vendor.id. Overwrites the order's shipping snapshot and logs an order.shipping_address_updated event with before → after, in the same transaction.

Available only when canEditShippingAddress is true on the sub-order. Two things close it:

ConditionWhy
Another seller has a bag on the orderThe address is a snapshot on the parent order, shared by every bag — editing it would redirect the other seller's parcel too. The store operator maintains it instead.
A courier already holds the addressIt is read off the order row at booking and sent to the carrier, so the waybill and printed label carry the old one. Editing after that changes this panel and nothing else.

Body

{
  "address": {
    "firstName": "Priya",
    "lastName": "Menon",
    "fullAddress": "88 Brigade Road",
    "city": "Bengaluru",
    "state": "Karnataka",
    "pincode": "560001",
    "phone": "+919000011111",
    "country": "IN"          // optional, defaults to IN
  }
}

Same validation the storefront applies at checkout, so a corrected address can never be weaker than a placed one.

Response 200 — the sub-order after the edit.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown sub-order, or it belongs to another seller
409ORDER_MULTI_VENDORAnother seller has a bag on this order
409ORDER_ADDRESS_LOCKEDA courier already holds this address

POST /vendor/orders/:id/fulfilled — Mark fulfilled

Assigns a shipping provider, dispatches provider.createShipment(), and transitions pending → fulfilled. Only allowed when the sub-order is pending.

Body

{
  "providerId": "clickpost",
  "method": "express",
  "trackingCode": "CP123456",              // optional
  "awbNumber": "AWB987654"                 // optional
}
FieldTypeConstraints
providerIdstringRequired. Must be enabled for the active vendor
methodstringRequired. Must belong to that provider
trackingCodestring?Trimmed; 1..200 chars. Some providers fill this asynchronously via webhook
awbNumberstring?Trimmed; 1..200 chars

Response 200 — updated VendorOrderResponse.

Side effects

  • Validates (providerId, method) against the vendor's enabled providers.
  • Dispatches provider.createShipment() — provider failure rolls back the transition (sub-order stays pending).
  • Writes audit row + emits order.vendor.fulfilled.

Errors

StatusCodeWhen
404NOT_FOUNDSub-order not owned by vendor
409INVALID_TRANSITIONSub-order is not pending
400VALIDATION_ERRORProvider/method invalid for this vendor

POST /vendor/orders/bulk-fulfill — Bulk fulfill

Apply the same provider/method/(tracking)/(awb) across up to 200 sub-orders in one call. Each sub-order runs in its own transaction so one failure does not roll back the rest. Vendors needing per-order tracking codes should call the single-order endpoint.

Body

{
  "orderVendorIds": ["01J9...", "01J9..."],
  "providerId": "clickpost",
  "method": "express",
  "trackingCode": "CP123456",              // optional, applies to every id
  "awbNumber": "AWB987654"                 // optional, applies to every id
}
FieldConstraints
orderVendorIds1..200 ids

Response 200

{
  "data": {
    "successful": ["01J9...", "01J9..."],
    "errors": [
      { "orderVendorId": "01J9...", "reason": "Sub-order not in pending status" }
    ]
  },
  "message": "Success",
  "statusCode": 200
}

POST /vendor/orders/:id/delivered — Mark delivered

Allowed only when the sub-order is fulfilled. Stamps deliveredAt and transitions to delivered.

Body — empty.

Response 200 — updated VendorOrderResponse.

Side effects

  • Audit row + order.vendor.delivered event.
  • COD payment: delivery does not mark paid. Admin mark-paid records cash collected; vendor sale earnings land only after payment is paid.

Errors

StatusCodeWhen
404NOT_FOUNDSub-order not owned by vendor
409INVALID_TRANSITIONSub-order is not fulfilled

Fulfilment ownership

GET /vendor/fulfillment/ownership — May I process my own orders?

Resolves the per-vendor override on this seller's profile, then the platform default at admin.fulfillment.owner, then the hard default vendor. The seller panel calls it once on load to decide whether to show anything shipping-related at all — per-order fulfillmentOwner arrives too late to hide a whole nav section.

Response 200

{
  "data": { "owner": "vendor", "canProcess": true },
  "message": "Success",
  "statusCode": 200
}
FieldTypeNotes
owner"vendor" | "admin"Who processes this vendor's sub-orders
canProcessbooleanMirror of owner === "vendor", for UI gating

When owner is admin the store operator ships from their central warehouse: fulfilled, delivered, cancel, and the return transitions all return 403 FULFILLMENT_OWNER_ADMIN on this panel, and the shipping configuration surfaces are hidden. Only the operator can change this — the setting is admin-only and never editable by the vendor.


Vendor returns

Base path: /vendor/returns. :id is return_request.id. Return requests are created by the customer (or admin) against a delivered sub-order; the vendor approves/rejects, schedules pickup, receives the parcel, and runs QC. Refunds themselves are admin-actioned.

GET /vendor/returns — List my returns

Query

NameTypeDefaultNotes
pageint1>= 1
limitint201..100
statusstring?Trimmed; 1..32 chars. Filter by return status

Response 200 — paginated envelope of ReturnResponse.


GET /vendor/returns/:id — Return detail

Response 200ReturnResponse.

Errors

StatusCodeWhen
404NOT_FOUNDReturn not owned by active vendor

POST /vendor/returns/:id/approve — Approve a return

Optionally override the computed refund_amount (capped between 0 and the system-computed amount).

Body

{ "refundAmountOverride": 49900 }          // optional
FieldTypeConstraints
refundAmountOverrideint?>= 0 and ≤ computed refundAmount

Response 200 — updated ReturnResponse with approvedAt stamped.


POST /vendor/returns/:id/reject — Reject a return

Body

{ "reason": "Item shows signs of wear inconsistent with the return reason" }
FieldTypeConstraints
reasonstringTrimmed; 1..500 chars. Required

Response 200 — updated ReturnResponse with rejectedAt and rejectionReason stamped.


POST /vendor/returns/:id/pickup — Mark courier pickup confirmed

Optionally attach courier identifiers from the pickup attempt.

Body

{
  "awbNumber": "AWB12345",                 // optional
  "trackingCode": "TRK67890"               // optional
}
FieldTypeConstraints
awbNumberstring?Trimmed; 1..200 chars
trackingCodestring?Trimmed; 1..200 chars

Response 200 — updated ReturnResponse with pickedUpAt stamped.


POST /vendor/returns/:id/receive — Mark the returned parcel as received

Body — empty.

Response 200 — updated ReturnResponse with receivedAt stamped.


POST /vendor/returns/:id/qc-pass — Pass QC and restock

Restocks each return line via the inventory module. Restock is best-effort per line — each call is idempotent on (return_request, return_line). Refund itself still requires admin action.

Body — empty.

Response 200 — updated ReturnResponse with qcPassedAt stamped and lines[].restocked = true where applicable.


POST /vendor/returns/:id/qc-fail — Fail QC (no restock)

Terminal for the vendor flow. Admin can still force-refund via the admin override endpoint.

Body

{ "reason": "Item arrived damaged beyond resale" }
FieldTypeConstraints
reasonstringTrimmed; 1..500 chars. Required

Response 200 — updated ReturnResponse with qcFailedAt and qcFailureReason stamped.


Vendor payouts

Base path: /vendor. Read-only — payout creation, marking paid, balance adjustments, and cancellation are all admin actions; the vendor sees the ledger and the payouts admin has cut.

GET /vendor/balance — Vendor balance

Snapshot of the active vendor's funds and commission rate.

Response 200VendorBalanceResponse.

{
  "data": {
    "vendorId": "01J9...",
    "pending": 125000,
    "available": 240000,
    "lifetimeEarned": 5400000,
    "lifetimeRefunded": 120000,
    "lifetimePaidOut": 5040000,
    "payoutHold": false,
    "commissionRate": 1500                 // 15.00%
  }
}

GET /vendor/ledger — Ledger entries

Paginated ledger for the active vendor. One row per sale, refund, manual adjustment, or commission adjustment.

Query

NameTypeDefaultNotes
pageint1>= 1
limitint201..100
kind"sale" | "refund" | "manual" | "commission_adjustment"?Filter by entry kind
status"pending" | "available" | "paid_out" | "cancelled"?Filter by entry status

Response 200 — paginated envelope of LedgerEntryResponse.


GET /vendor/payouts — List my payouts

Query

NameTypeDefaultNotes
pageint1>= 1
limitint201..100
status"pending" | "paid" | "cancelled" | "failed"?Filter by payout status

Response 200 — paginated envelope of PayoutResponse (without entries[]).


GET /vendor/payouts/:id — Payout detail

Returns the payout plus its constituent entries[] (full LedgerEntryResponse[]).

Response 200PayoutResponse with entries[] populated.

Errors

StatusCodeWhen
404NOT_FOUNDPayout not owned by active vendor

Domain events

Emitted via EventEmitter2. The vendor surface only emits a subset; the full catalogue is in order.md.

EventFired when
order.vendor.fulfilledSub-order pending → fulfilled via POST /vendor/orders/:id/fulfilled
order.vendor.deliveredSub-order fulfilled → delivered via POST /vendor/orders/:id/delivered
order.vendor.cancelledSub-order * → cancelled via POST /vendor/orders/:id/cancel
order.cancelledAuto-cascade when every sibling sub-order is cancelled
order.paidadmin mark-paid (COD cash collection or prepaid capture)
order.return.*Return lifecycle transitions on the vendor returns endpoints
vendor.payout.*Payout lifecycle (admin actions — read-only for vendors)

  • cart — orders are created by committing a prepared checkout; cart never appears on the vendor surface.
  • inventory — sub-order cancel releases reservations, and restocks an unshipped bag whose reservation was already committed at payment; a shipped bag comes back only through return QC-pass.
  • shipping + shipping-clickpostproviderId/method validation and the createShipment() / cancelShipment() hooks live there. See shipping.md, shipping-clickpost.md.
  • tax-flat — line and shipping taxBreakdown is computed from per-vendor tax config. See tax.md.
  • notifications — listens to order.vendor.* events for vendor-targeted email/push.

On this page

ConventionsAuthenticationTenant scopingResponse envelopeError envelopeMoney + datesAudit trailDomain typesSub-order lifecycle — OrderVendorStatusVendorOrderResponseOrderLineResponseReturnResponsePayouts — VendorBalanceResponse, LedgerEntryResponse, PayoutResponseVendor sub-ordersGET /vendor/orders — List my sub-ordersOrder exportsPOST /vendor/orders/exports — Queue an exportGET /vendor/orders/exports — My export historyGET /vendor/orders/exports/:id — Export statusGET /vendor/orders/exports/:id/download — Download the fileGET /vendor/orders/:id — Sub-order detailPOST /vendor/orders/:id/cancel — Cancel sub-orderPATCH /vendor/orders/:id/shipping-address — Correct the delivery addressPOST /vendor/orders/:id/fulfilled — Mark fulfilledPOST /vendor/orders/bulk-fulfill — Bulk fulfillPOST /vendor/orders/:id/delivered — Mark deliveredFulfilment ownershipGET /vendor/fulfillment/ownership — May I process my own orders?Vendor returnsGET /vendor/returns — List my returnsGET /vendor/returns/:id — Return detailPOST /vendor/returns/:id/approve — Approve a returnPOST /vendor/returns/:id/reject — Reject a returnPOST /vendor/returns/:id/pickup — Mark courier pickup confirmedPOST /vendor/returns/:id/receive — Mark the returned parcel as receivedPOST /vendor/returns/:id/qc-pass — Pass QC and restockPOST /vendor/returns/:id/qc-fail — Fail QC (no restock)Vendor payoutsGET /vendor/balance — Vendor balanceGET /vendor/ledger — Ledger entriesGET /vendor/payouts — List my payoutsGET /vendor/payouts/:id — Payout detailDomain eventsRelated modules