Order Module — Admin
HTTP surface for platform-admin oversight of orders, returns, and vendor payouts. Read every order on the platform; perform ops actions (cancel on behalf of the customer, manually…
HTTP surface for platform-admin oversight of orders, returns, and vendor payouts. Read every order on the platform; perform ops actions (cancel on behalf of the customer, manually mark a stuck pending payment as paid, mark a paid order as refunded, force-create or override returns); browse vendor ledgers and disburse payouts.
Source:
api-modules/order/src/controllers/admin-orders.controller.ts,api-modules/order/src/controllers/admin-returns.controller.ts,api-modules/order/src/controllers/admin-payouts.controller.ts.Orders are split per-vendor — a parent
orderrow aggregates the customer-facing totals while eachorder_vendorrow is the fulfilment unit. Inventory reservations, fulfilment status, and the vendor ledger all key offorder_vendor.
Conventions
Authentication
All endpoints require a Better-Auth admin session and a role granting the matching permission.
| Endpoint group | Permission |
|---|---|
GET /admin/orders, GET /admin/orders/:id, GET /admin/orders/:id/events | order: view |
POST /admin/orders/:id/cancel, POST /admin/orders/cleanup-stale-pending | order: cancel |
POST /admin/orders/:id/mark-paid, POST /admin/orders/:id/mark-refunded | order: update |
GET /admin/returns, GET /admin/returns/:id | order: view |
POST /admin/orders/:id/returns, POST /admin/returns/:id/override | order: update |
GET /admin/vendors/:id/balance, GET /admin/vendors/:id/ledger, GET /admin/vendors/:id/payouts, GET /admin/payouts, GET /admin/payouts/:id | payout: view |
POST /admin/vendors/:id/payouts, POST /admin/payouts/promote | payout: create |
POST /admin/payouts/:id/mark-paid | payout: mark_paid |
POST /admin/payouts/:id/cancel | payout: cancel |
POST /admin/vendors/:id/ledger/adjust | payout: adjust |
Response envelope
Successful responses are wrapped by ResponseInterceptor:
{
"data": <payload>,
"message": "Success",
"statusCode": 200,
"metadata": { /* optional, e.g. pagination */ }
}Error envelope
statusCode | errorCode examples |
|---|---|
| 400 | BAD_REQUEST, VALIDATION_ERROR |
| 401 | UNAUTHORIZED |
| 403 | FORBIDDEN |
| 404 | NOT_FOUND |
| 409 | CONFLICT (illegal state transition, e.g. cancelling a delivered order) |
| 500 | INTERNAL_SERVER_ERROR, DATABASE_ERROR |
Money fields
Every amount field is an integer subunit (paise / cents). commissionRate is in basis points (10000 = 100.00%).
Domain types
OrderResponse
type OrderResponse = {
id: string;
orderNumber: string;
status: OrderStatus;
paymentStatus: PaymentStatus;
paymentProvider: string;
paymentMethod: string;
platform: Platform;
shippingAddress: AddressBlock;
billingAddress: AddressBlock;
subtotal: number; // subunits
discountTotal: number;
shippingTotal: number;
taxTotal: number;
grandTotal: number;
vendorBreakdowns: OrderVendorResponse[]; // per-vendor sub-orders + lines
events: OrderEventResponse[]; // recent audit tail (up to 50)
pendingClientAction: {
provider: string;
payload: Record<string, unknown>;
} | null;
placedAt: string;
confirmedAt: string | null;
paidAt: string | null;
cancelledAt: string | null;
cancellationReason: string | null;
};OrderEventResponse
type OrderEventResponse = {
id: string;
orderVendorId: string | null;
eventType: string; // e.g. "payment.captured", "order_vendor.delivered", "return.requested"
actorType: "customer" | "vendor" | "admin" | "system";
actorId: string | null;
source: string; // free-form, e.g. "admin-panel", "webhook"
changes: Record<string, unknown>;
metadata: Record<string, unknown>;
createdAt: string; // ISO
};ReturnResponse
type ReturnResponse = {
id: string;
returnNumber: string;
orderId: string;
orderVendorId: string;
customerId: string | null;
vendorId: string;
type: string; // service-defined ("refund", "exchange")
status: string; // "requested" | "approved" | "rejected" | "picked_up" | "received" | "qc_passed" | "qc_failed" | "refunded" | "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[];
};PayoutResponse
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 populated on the detail endpoint
};LedgerEntryResponse
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;
};VendorBalanceResponse
type VendorBalanceResponse = {
vendorId: string;
pending: number; // net subunits in 'pending' status
available: number; // net subunits in 'available' status not yet on a draft payout
lifetimeEarned: number;
lifetimeRefunded: number;
lifetimePaidOut: number;
payoutHold: boolean; // vendor.payouts.payout_hold flag
commissionRate: number; // basis points
};Orders
Base path: /admin/orders.
GET /admin/orders — List orders
Required permission: order: view. Newest first across every customer and vendor.
Query
| Name | Type | Default | Notes |
|---|---|---|---|
page | int | 1 | >= 1 |
limit | int | (paginated default) | — |
status | OrderStatus? | — | Parent-order lifecycle filter |
Response 200 — paginated envelope of OrderResponse[].
GET /admin/orders/:id — Order detail
Required permission: order: view. Admin sees all fields (including provider payload). Inlines the most recent ~50 events; use /events for the full audit trail.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown id |
POST /admin/orders/:id/cancel — Cancel on behalf of customer
Required permission: order: cancel. Allowed only when no sub-order has been delivered. Cascades sub-orders, releases per-vendor reservations, writes audit rows.
Body
{ "reason": "Customer asked via support chat" }| Field | Type | Constraints |
|---|---|---|
reason | string? | Trimmed, 1..500 |
Response 200 — cancelled OrderResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown id |
| 409 | CONFLICT | A sub-order is already delivered |
POST /admin/orders/:id/mark-paid — Manually mark paid
Required permission: order: update. For bank-transfer settlements, COD edge cases (vendor confirmed delivery offline), or support overrides. Rejects orders already paid or cancelled. If the order was at pending_payment, transitions it to confirmed and commits the inventory reservation.
Body
{
"externalReference": "NEFT-UTR-12345", // optional, 1..200
"reason": "Customer wired funds directly" // optional, 1..500
}Response 200 — updated OrderResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown id |
| 409 | CONFLICT | Already paid or cancelled |
POST /admin/orders/:id/mark-refunded — Mark refunded
Required permission: order: update. v1 refund flow is admin-driven — the admin issues the refund out-of-band in the gateway's dashboard, then calls this to flip payment_status to refunded. Order lifecycle status (confirmed/delivered) stays as-is — refund is a money-only operation.
Body
{
"amount": 100000, // optional; defaults to remaining unpaid amount
"returnId": "01J9...", // optional; links event to an order_return row
"externalReference": "RZP-RFND-abc", // optional, 1..200
"reason": "Goodwill credit for late delivery"
}| Field | Type | Constraints |
|---|---|---|
amount | int? | >= 1 subunits. Defaults to the remaining unpaid amount |
returnId | string? | 1..100; links per-return refund counter |
externalReference | string? | 1..200 |
reason | string? | 1..500 |
Response 200 — refunded OrderResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown id |
| 409 | CONFLICT | Order not in a refundable state |
GET /admin/orders/:id/events — Paginated audit log
Required permission: order: view. Order detail inlines only the most recent ~50 events; this endpoint pages through the full history.
Query
| Name | Type | Default | Notes |
|---|---|---|---|
page | int | 1 | >= 1 |
limit | int | — | — |
eventType | string? | — | Trimmed, 1..128. Narrow to one event class |
Response 200 — paginated envelope of OrderEventResponse[].
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown order id |
POST /admin/orders/cleanup-stale-pending — Stale pending-payment sweep
Required permission: order: cancel. Out-of-band escape hatch matching the hourly BullMQ cron. Cancels orders stuck in pending_payment longer than timeoutHours (defaults to admin.payment.pending_timeout_hours). Caps blast radius at limit orders per call.
Body
{ "timeoutHours": 24, "limit": 200 }| Field | Type | Constraints |
|---|---|---|
timeoutHours | int? | 0..720. Defaults to admin.payment.pending_timeout_hours |
limit | int? | 1..1000. Defaults to the constant PENDING_PAYMENT_SWEEP_BATCH |
Response 200
{ "data": { "scanned": 312, "cancelled": 14, "errors": 0 }, "message": "Success", "statusCode": 200 }Returns
GET /admin/returns — List returns
Required permission: order: view. Cross-vendor list.
Query
| Name | Type | Default | Notes |
|---|---|---|---|
page | int | 1 | — |
limit | int | — | — |
status | string? | — | Trimmed, 1..32. Filter by return status |
Response 200 — paginated envelope of ReturnResponse[].
GET /admin/returns/:id — Return detail
Required permission: order: view.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown id |
POST /admin/orders/:id/returns — Force-create a return
Required permission: order: update. Support / dispute-resolution path: admin creates a return without the customer's own request. Optionally pre-approves the return and overrides the computed refund_amount.
Body
{
"orderVendorId": "01J9...",
"reasonCode": "damaged_in_transit",
"reasonNotes": "Customer photos in support ticket #ABC-123",
"lines": [
{ "orderLineId": "01J9...", "quantity": 1, "reasonCode": "damaged_in_transit" }
],
"preApprove": true,
"refundAmountOverride": 90000
}| Field | Type | Constraints |
|---|---|---|
orderVendorId | string | Required; the sub-order being returned |
reasonCode | string | 1..64 |
reasonNotes | string? | max 2000 |
lines[] | array | 1..100 entries |
preApprove | boolean? | Skip vendor approval step |
refundAmountOverride | int? | >= 0 subunits |
Response 200 — created ReturnResponse.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Body fails zod |
| 404 | NOT_FOUND | Order or sub-order not found |
POST /admin/returns/:id/override — Admin override transition
Required permission: order: update. Escape hatch transitions on a wedged return.
action | Effect |
|---|---|
"force_refund" | Clears qc_failure and moves to qc_passed so /admin/orders/:id/mark-refunded accepts the returnId |
"approve" | Reverses a vendor rejection — back to approved |
"cancel" | Cancels the return |
Body
{ "action": "force_refund", "reason": "QC dispute escalation; refunding as goodwill" }| Field | Type | Constraints |
|---|---|---|
action | enum | One of force_refund / approve / cancel |
reason | string | Required; 1..500 |
Response 200 — overridden ReturnResponse.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Body fails zod |
| 404 | NOT_FOUND | Unknown id |
Vendor payouts
GET /admin/vendors/:id/balance — Vendor balance summary
Required permission: payout: view.
Response 200 — VendorBalanceResponse.
GET /admin/vendors/:id/ledger — Vendor ledger
Required permission: payout: view.
Query
| Name | Type | Default | Notes |
|---|---|---|---|
page | int | 1 | — |
limit | int | — | — |
kind | "sale" | "refund" | "manual" | "commission_adjustment"? | — | — |
status | "pending" | "available" | "paid_out" | "cancelled"? | — | — |
Response 200 — paginated envelope of LedgerEntryResponse[].
GET /admin/vendors/:id/payouts — Vendor payouts
Required permission: payout: view.
Query
| Name | Type | Default | Notes |
|---|---|---|---|
page | int | 1 | — |
limit | int | — | — |
status | "pending" | "paid" | "cancelled" | "failed"? | — | — |
Response 200 — paginated envelope of PayoutResponse[] (without entries).
GET /admin/payouts — Cross-vendor payout list
Required permission: payout: view. Same query as the per-vendor list.
Response 200 — paginated envelope of PayoutResponse[] (without entries).
GET /admin/payouts/:id — Payout detail
Required permission: payout: view. Returns the payout with its linked ledger entries inlined.
Response 200 — PayoutResponse with entries[] populated.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown id |
POST /admin/vendors/:id/payouts — Create a draft payout
Required permission: payout: create. Picks all unattached available ledger entries (optionally within the supplied period), snapshots the sums, and creates a pending payout row. Rejects when the vendor is on hold (vendor.payouts.payout_hold = true) or when no available entries exist.
Body
{
"periodStart": "2026-04-01T00:00:00.000Z",
"periodEnd": "2026-04-30T23:59:59.999Z",
"notes": "April settlement"
}| Field | Type | Notes |
|---|---|---|
periodStart | ISO datetime? | Inclusive lower bound. Default: no lower bound |
periodEnd | ISO datetime? | Inclusive upper bound. Default: now |
notes | string? | Trimmed, max 2000 |
Response 201 — draft PayoutResponse.
Errors
| Status | Code | When |
|---|---|---|
| 403 | FORBIDDEN | Vendor on payout hold |
| 400 | VALIDATION_ERROR | No available entries match the period |
POST /admin/payouts/:id/mark-paid — Mark a draft as paid
Required permission: payout: mark_paid. Admin pastes the bank reference (NEFT UTR, IMPS ref, etc.) from the offline transfer. Flips the payout to paid and every linked ledger entry to paid_out.
Body
{ "bankReference": "NEFT-UTR-12345", "notes": "Sent via HDFC NEFT" }| Field | Type | Constraints |
|---|---|---|
bankReference | string | Required, trimmed, 1..200 |
notes | string? | Trimmed, max 2000 |
Response 200 — paid PayoutResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown id |
| 409 | CONFLICT | Payout not in pending |
POST /admin/payouts/:id/cancel — Cancel a draft
Required permission: payout: cancel. Releases linked ledger entries back to the pool (available).
Body
{ "reason": "Wrong period selected; redrafting" }| Field | Type | Constraints |
|---|---|---|
reason | string | Required, trimmed, 1..500 |
Response 200 — cancelled PayoutResponse.
Errors
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Unknown id |
POST /admin/vendors/:id/ledger/adjust — Manual ledger adjustment
Required permission: payout: adjust. Use for chargebacks, goodwill credits, off-platform reconciliation. Lands as a manual or commission_adjustment entry in available status — picked up by the next payout.
Body
{
"amount": 50000, // signed subunits; positive credits, negative debits
"kind": "manual", // or "commission_adjustment"
"description": "Q1 goodwill credit"
}| Field | Type | Constraints |
|---|---|---|
amount | signed int | No min — manual adjustments bypass commission math |
kind | enum | "manual" or "commission_adjustment" |
description | string | Required, trimmed, 1..500 |
Response 200 — updated VendorBalanceResponse.
POST /admin/payouts/promote — Manually promote pending → available
Required permission: payout: create. Ops escape hatch when the BullMQ cron has not run or has been disabled. Runs PayoutService.promotePendingEntries() once.
Response 200
{ "data": { "promoted": 42 }, "message": "Success", "statusCode": 200 }Related modules
admin-rbac— gates every endpoint viaorder:*andpayout:*. Seeadmin-rbac.md.cart— converted carts produce orders. Seecart.md.inventory— order placement reserves;mark-paidcommits; cancel releases.payment— emitspayment.capturedevents that drive the automark-paidpath (manual override goes through this admin endpoint).shipping— vendors transition sub-orders through fulfilment states.vendor—vendor.payouts.payout_holdsetting gatesPOST /admin/vendors/:id/payouts.settings—admin.payment.pending_timeout_hoursdrives the stale-pending sweep.notifications— order events emit notification triggers; seenotifications.md.
Notifications Module — Admin
HTTP surface for the admin notification operations: composing and sending broadcasts (one-shot email and/or push to a targeted audience) and inspecting the notification log…
Product Attribute Module — Admin
HTTP surface for managing the platform-wide catalogue of product attributes (typed metadata fields — Material, Capacity, Heel Height, etc.) and attribute groups that bundle…