Supercommerce API Docs
Store API

Order Module — Storefront

HTTP surface for the customer-side order lifecycle — payment provider discovery, place-order, list/detail, customer-initiated cancel, and the customer-side return flow…

HTTP surface for the customer-side order lifecycle — payment provider discovery, place-order, list/detail, customer-initiated cancel, and the customer-side return flow (eligibility, photo upload, request, list, detail, cancel).

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

The module orchestrates the cart → order handoff: it consumes CartService (cart resolution + checkout commit), PaymentRegistry (provider/method dispatch), InventoryService (reservation lifecycle), and the shipping registry. Webhooks (payment, courier) live in their own provider modules.


Conventions

Authentication

Endpoint groupAuth
GET /store/checkout/payment-providersrequired (customer)
POST /store/checkout/place-orderrequired (customer)
GET /store/orders, GET /store/orders/:id, POST /store/orders/:id/cancelrequired (customer)
/store/orders/:id/returns/**, /store/returns/photosrequired (customer)

Customer order detail and return detail enforce a no-leak rule: ids that belong to a different customer return 404 Not Found, never 403.

Headers

POST /store/checkout/place-order requires:

HeaderRequiredNotes
x-cart-tokenyesCart handle issued by the cart endpoints. Identifies the active cart even for a logged-in customer (handles the guest→customer adoption window)
x-platformnoWEB or APP (case-insensitive). Defaults to WEB. Used to pick a platform-specific enabled payment provider list

GET /store/checkout/payment-providers accepts x-platform only.

Response envelope

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

Error envelope

statusCodeerrorCode examples
400BAD_REQUEST, VALIDATION_ERROR, PAYMENT_PROVIDER_NOT_ENABLED, PAYMENT_METHOD_INVALID
401UNAUTHORIZED
403FORBIDDEN (payment provider not enabled, or cart not yours)
404NOT_FOUND
409CONFLICT, INVALID_TRANSITION, PARENT_NOT_CANCELLABLE
500INTERNAL_SERVER_ERROR, DATABASE_ERROR

Currency

All money fields (subtotal, discountTotal, shippingTotal, taxTotal, grandTotal, unitPrice, lineSubtotal, lineTotal, discountAllocated, netAmount, refundAmount, tax components) are integer subunits (paise / cents / eurocents).

Lifecycle (customer-visible)

Parent — order.status

StatusNotes
pending_paymentSDK-driven providers (Razorpay etc.); inventory reserved but not committed
confirmedSynchronous payment (COD/manual) at place-order, or payment webhook
cancelledCustomer/admin cancel, or all sub-orders cancelled. Terminal.

Parent payment — order.payment_status

StatusNotes
pendingInitial value
paidProvider success, webhook, admin mark-paid, or last COD sub-order delivered
failedWebhook failure — order stays pending_payment for retry
refundedAdmin bookkeeping flag

Sub-order — order_vendor.fulfillment_status

pendingfulfilleddelivered, with cancelled reachable from pending or fulfilled. Customers don't drive these — they only observe them via vendorBreakdowns[].fulfillmentStatus.


Domain types

OrderResponse

type OrderStatus     = "pending_payment" | "confirmed" | "cancelled";
type PaymentStatus   = "pending" | "paid" | "failed" | "refunded";
type Platform        = "APP" | "WEB" | "BOTH";
type OrderLineType   = "PRODUCT" | "GIFT";

type AddressBlock = {
  firstName: string;
  lastName: string;
  fullAddress: string;
  city: string;
  pincode: string;
  state: string;
  phone: string;
  country: string;
};

type OrderResponse = {
  id: string;
  orderNumber: string;
  status: OrderStatus;
  paymentStatus: PaymentStatus;
  paymentProvider: string;          // e.g. "manual", "razorpay"
  paymentMethod: string;            // e.g. "cod", "upi", "card"
  platform: Platform;

  shippingAddress: AddressBlock;
  billingAddress: AddressBlock;

  subtotal: number;                 // subunits
  discountTotal: number;
  shippingTotal: number;
  taxTotal: number;
  grandTotal: number;

  vendorBreakdowns: OrderVendorResponse[];
  events: OrderEventResponse[];     // tail of audit-log rows (most recent first)

  /** Bootstrap data the storefront/SDK uses to complete an SDK-driven
   *  payment flow (Razorpay, Stripe, etc.). Absent for synchronous
   *  providers like manual COD. */
  pendingClientAction: {
    provider: string;
    payload: Record<string, unknown>;
  } | null;

  placedAt: string;                 // ISO
  confirmedAt: string | null;
  paidAt: string | null;
  cancelledAt: string | null;
  cancellationReason: string | null;
};

OrderVendorResponse

type OrderVendorResponse = {
  id: string;
  vendorId: string;
  vendorNameAtOrder: string;        // snapshot
  fulfillmentStatus: "pending" | "fulfilled" | "delivered" | "cancelled";
  subtotal: number;
  discountAllocated: number;
  shippingCost: number;
  taxAmount: number;
  total: number;
  shippingProviderId: string | null;
  shippingMethod: string | null;
  trackingCode: string | null;
  awbNumber: string | null;
  taxBreakdown: TaxComponent[];     // aggregated by tax type across this vendor's lines + shipping
  shippingNetAmount: number | null;
  shippingTaxBreakdown: TaxComponent[];
  fulfilledAt: string | null;
  deliveredAt: string | null;
  cancelledAt: string | null;
  cancellationReason: string | null;
  lines: OrderLineResponse[];
};

OrderLineResponse

type OrderLineResponse = {
  id: string;
  vendorId: string;
  variantId: string | null;
  productId: string | null;
  sku: string;
  productNameAtOrder: string;       // snapshot
  variantNameAtOrder: string | null;
  imageAtOrder: string | null;
  hsnCodeAtOrder: string | null;    // GST classification snapshot from variant
  type: OrderLineType;
  quantity: number;
  unitPrice: number;
  lineSubtotal: number;             // tax-inclusive amount displayed
  discountAllocated: number;
  lineTotal: number;
  netAmount: number | null;         // pre-tax portion
  taxBreakdown: TaxComponent[];
};

OrderEventResponse

type OrderEventResponse = {
  id: string;
  orderVendorId: string | null;
  eventType: string;
  actorType: "user" | "vendor" | "admin" | "system" | "webhook";
  actorId: string | null;
  source: string;
  changes: Record<string, unknown>;
  metadata: Record<string, unknown>;
  createdAt: string;
};

ReturnResponse

type ReturnResponse = {
  id: string;
  returnNumber: string;
  orderId: string;
  orderVendorId: string;
  customerId: string | null;
  vendorId: string;
  type: string;
  status: string;                   // e.g. requested / approved / picked_up / received / qc_passed / qc_failed / refunded / rejected / cancelled
  reasonCode: string | null;
  reasonNotes: string | null;
  refundAmount: number;             // subunits
  refundedAmount: number;
  externalRefundReference: string | null;
  shippingProvider: string | null;
  awbNumber: string | null;
  trackingCode: string | null;
  rejectionReason: string | null;
  qcFailureReason: string | null;
  requestedAt: string;
  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;
  quantity: number;
  unitPrice: number;
  taxPortion: number;
  lineRefundAmount: number;
  reasonCode: string | null;
  reasonNotes: string | null;
  restocked: boolean;
};

Checkout

GET /store/checkout/payment-providers — Enabled payment providers

Returns the providers/methods enabled by admin for the caller's platform. Use to render the payment selector. Defaults to WEB when x-platform is missing.

Headers

HeaderNotes
x-platformWEB or APP

Response 200 — provider list (shape per EnabledPaymentProvidersService.list).


POST /store/checkout/place-order — Convert active cart into an order

Resolves the cart via x-cart-token (and the session's customerId), validates the chosen provider+method against the platform's enabled list, then drives OrderService.createFromCart. For SDK-driven providers (Razorpay), the response carries pendingClientAction with bootstrap data the storefront SDK uses to complete payment. For synchronous providers (COD, manual) the order is confirmed and paymentStatus may already be paid on return.

Headers

HeaderRequiredNotes
x-cart-tokenyesCart handle
x-platformnoWEB / APP, default WEB

Body

{
  "paymentProvider": "razorpay",   // trimmed, min 1
  "paymentMethod": "upi",          // trimmed, min 1
  "billingAddress": {              // optional — when omitted, billing copies shipping
    "firstName": "Ada",
    "lastName": "Lovelace",
    "fullAddress": "221B Baker Street",
    "city": "London",
    "pincode": "110001",
    "state": "Delhi",
    "phone": "+919876543210",
    "country": "IN"
  }
}

billingAddress field constraints reuse the address-book validators (Indian pincode regex /^[1-9]\d{5}$/, phone /^(\+91)?[6-9]\d{9}$/).

Response 201OrderResponse.

Errors

StatusCodeWhen
400BAD_REQUESTx-cart-token header missing
400PAYMENT_PROVIDER_NOT_ENABLED / PAYMENT_METHOD_INVALIDProvider/method rejected by registry
403FORBIDDENCart belongs to another customer
404NOT_FOUNDCart cannot be resolved
409CART_EMPTY, INSUFFICIENT_INVENTORYCart no longer placeable

Side effects — emits order.placed; for synchronous-paid providers also order.paid. The cart transitions to converted.


Orders

GET /store/orders — Paginated list of my orders

Most recent first.

Query

NameTypeDefaultNotes
pageint1>= 1
limitint(module default)1..MAX_ORDER_PAGE_SIZE
statusOrderStatus?Filter by pending_payment / confirmed / cancelled
startDateTimeISO-8601?Inclusive lower bound on order placed-at
endDateTimeISO-8601?Inclusive upper bound on order placed-at; must be >= startDateTime

Response 200 — paginated OrderResponse[].


GET /store/orders/:id — Order detail

Response 200 — full OrderResponse with embedded vendorBreakdowns and recent events.

Errors

StatusCodeWhen
404NOT_FOUNDOrder does not exist or belongs to another customer

POST /store/orders/:id/cancel — Cancel my order

Allowed only when no sub-order has yet been fulfilled or delivered. The cancel cascades to all sub-orders, releases their reservations, and emits order.cancelled.

Body

{ "reason": "Changed my mind" }  // optional, 1..500 chars

Response 200 — cancelled OrderResponse.

Errors

StatusCodeWhen
404NOT_FOUNDOrder not yours / does not exist
409PARENT_NOT_CANCELLABLEAt least one sub-order is past pending

Returns

Base path: /store/orders/:id/returns (with the photo-upload helper at /store/returns/photos).

GET /store/orders/:id/returns/eligibility — Per-sub-order eligibility

For each sub-order of the given order, returns whether it's currently returnable, the window expiry, the eligible reason codes, and the vendor's return policy text. Use to gate the "Request return" CTA.

Response 200

{
  "data": {
    "vendors": [
      {
        "orderVendorId": "01J9...",
        "vendorId": "01J9...",
        "returnable": true,
        "reason": null,
        "windowExpiresAt": "2026-05-20T00:00:00.000Z",
        "eligibleReasons": ["DAMAGED", "WRONG_ITEM", "NOT_AS_DESCRIBED"],
        "policyText": "Returns within 7 days of delivery..."
      }
    ]
  }
}

When returnable is false, reason carries a stable code (e.g. WINDOW_EXPIRED, ALREADY_RETURNED, NOT_DELIVERED) and windowExpiresAt may still be populated.


POST /store/returns/photos — Presigned upload URL for return evidence

Returns a presigned PUT URL the client uses to upload evidence (one photo per call). Unused keys age out via S3 lifecycle. Pass the returned storageKey in photoKeys[] of the create-return body.

Body

{
  "contentType": "image/jpeg",
  "fileSizeBytes": 524288         // max 20 MiB (20 * 1024 * 1024)
}

Response 200

{
  "data": {
    "storageKey": "returns/2026-05/abc.jpg",
    "uploadUrl": "https://s3.../signed-put-url",
    "expiresAt": "2026-05-13T11:45:00.000Z"
  }
}

POST /store/orders/:id/returns — Create a return request

Open a return against one sub-order. Service enforces:

  • caller owns the order;
  • orderVendorId belongs to that order;
  • per-line quantity <= delivered quantity;
  • return is within the vendor's window and reason is in eligibleReasons.

Body

{
  "orderVendorId": "01J9...",                // sub-order being returned
  "reasonCode": "DAMAGED",                   // 1..64 chars
  "reasonNotes": "Box arrived crushed",      // optional, max 2000 chars
  "lines": [
    {
      "orderLineId": "01J9...",
      "quantity": 1,                          // integer >= 1
      "reasonCode": "DAMAGED",                // optional per-line override
      "reasonNotes": "Top half dented"        // optional, max 2000 chars
    }
  ],
  "photoKeys": ["returns/2026-05/abc.jpg"]   // optional, max 20 keys, from POST /store/returns/photos
}

lines must be 1..100 entries. photoKeys must be 0..20 keys, each 1..500 chars.

Response 201ReturnResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod
404NOT_FOUNDOrder or sub-order not visible to caller
409CONFLICTSub-order not currently returnable, or per-line quantity exceeds delivered

GET /store/orders/:id/returns — List returns on an order

Query

NameTypeDefaultNotes
pageint1>= 1
limitint(module default)1..MAX_ORDER_PAGE_SIZE
statusstring?1..32 chars; filter by return lifecycle status

Response 200 — paginated ReturnResponse[].


GET /store/orders/:id/returns/:returnId — Return detail

Response 200ReturnResponse.

Errors

StatusCodeWhen
404NOT_FOUNDReturn not yours / does not exist

POST /store/orders/:id/returns/:returnId/cancel — Withdraw a return

Allowed only before the courier confirms pickup (i.e. while the return is still in the customer's hands). Once pickedUpAt is stamped the customer can't withdraw — vendor/admin paths handle reversals after that.

Response 200 — cancelled ReturnResponse.

Errors

StatusCodeWhen
404NOT_FOUNDReturn not yours / does not exist
409CONFLICTAlready past requested/approved (courier picked up)

  • cartprepare-checkout runs before place-order to reserve inventory. See cart.md.
  • payment-razorpay / payment-manual — concrete providers behind paymentProvider/paymentMethod. See payment-razorpay.md for the storefront verify endpoint.
  • shipping — vendor-side shipping provider assignment; customer-side tracking is in shipping.md.
  • storage — backs the presigned upload for return photos.
  • customerbillingAddress shape mirrors the address-book validators. See customer.md.

On this page