Supercommerce API Docs
Admin API

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, api-modules/order/src/controllers/admin-vendor-payout-config.controller.ts, api-modules/vendor/src/controllers/admin-vendor-bank-account.controller.ts.

Orders are split per-vendor — a parent order row aggregates the customer-facing totals while each order_vendor row is the fulfilment unit. Inventory reservations, fulfilment status, and the vendor ledger all key off order_vendor.


Conventions

Authentication

All endpoints require a Better-Auth admin session and a role granting the matching permission.

Endpoint groupPermission
GET /admin/orders, GET /admin/orders/:id, GET /admin/orders/:id/eventsorder: view
POST /admin/orders/:id/cancel, POST /admin/orders/cleanup-stale-pendingorder: cancel
POST /admin/orders/:id/mark-paid, POST /admin/orders/:id/mark-refundedorder: update
POST/GET /admin/orders/:id/refunds, POST /admin/orders/:id/refunds/:merchantRefundId/syncorder: refund
GET /admin/returns, GET /admin/returns/:idorder: view
POST /admin/orders/:id/returns, POST /admin/returns/:id/overrideorder: update
GET /admin/vendors/:id/balance, GET /admin/vendors/:id/ledger, GET /admin/vendors/:id/payouts, GET /admin/payouts, GET /admin/payouts/:id, GET /admin/vendors/:id/payout-config, GET /admin/vendors/:id/bank-accountpayout: view
POST /admin/vendors/:id/payouts, POST /admin/payouts/promotepayout: create
POST /admin/payouts/:id/mark-paidpayout: mark_paid
POST /admin/payouts/:id/cancelpayout: cancel
POST /admin/vendors/:id/ledger/adjustpayout: adjust
PUT /admin/vendors/:id/payout-configpayout: configure

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
403FORBIDDEN
404NOT_FOUND
409CONFLICT (illegal state transition, e.g. cancelling a delivered order)
500INTERNAL_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;
  customer: { id: string; name: string; email: string } | null; // The customer account that placed the order; null for legacy/guest orders.
  status: OrderStatus;
  paymentStatus: PaymentStatus;
  paymentProvider: string;
  paymentMethod: string;
  platform: Platform;

  shippingAddress: AddressBlock;
  billingAddress: AddressBlock;
  customerNote: string | null;       // shopper's checkout instruction; null when none

  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;
};

OrderVendorSummary (admin detail only)

On GET /admin/orders/:id, each vendorBreakdowns entry additionally carries a live vendor summary resolved at read time (unlike vendorNameAtOrder, which is the placement-time snapshot). The key is omitted on list responses and on the store surface; it is null when the vendor account no longer exists.

type OrderVendorSummary = {
  id: string;
  name: string;          // current business name (org name if no profile yet)
  slug: string;
  logo: string | null;
  email: string | null;  // vendor_profile.business_email
  phone: string | null;  // vendor_profile.business_phone
};

// vendorBreakdowns entries on the admin detail endpoint:
type OrderVendorResponse = /* shared shape */ & {
  vendor?: OrderVendorSummary | 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;
  actor: { id: string; name: string; email: string } | null; // Resolved actor account; null for system events.
  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

NameTypeDefaultNotes
pageint1>= 1
limitint(paginated default)
statusOrderStatus?Parent-order lifecycle filter
vendorIdstring?Narrows to orders containing a sub-order for this vendor

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. Each vendorBreakdowns entry includes the live vendor summary.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown 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" }
FieldTypeConstraints
reasonstring?Trimmed, 1..500

Response 200 — cancelled OrderResponse.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id
409CONFLICTA 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

StatusCodeWhen
404NOT_FOUNDUnknown id
409CONFLICTAlready 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"
}
FieldTypeConstraints
amountint?>= 1 subunits. Defaults to the remaining unpaid amount
returnIdstring?1..100; links per-return refund counter
externalReferencestring?1..200
reasonstring?1..500

Response 200 — refunded OrderResponse.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id
409CONFLICTOrder not in a refundable state

POST /admin/orders/:id/refunds — Refund through the payment gateway

Required permission: order: refund. Provider-agnostic: dispatches to whichever gateway placed the order, for any provider implementing the payment port's refund capability. Prefer this over mark-refunded when it is available — the money actually moves, rather than the platform recording that someone moved it by hand.

Gateway refunds are asynchronous. The response describes the refund that was accepted, not one that has settled: state is normally PENDING, and the order's payment_status moves only when the provider confirms (its webhook, or the sync endpoint below). Some gateways settle synchronously, in which case the row comes back COMPLETED and the order is already updated.

gatewayRefundSupported on the order response says whether this endpoint applies. Orders on COD, or on a gateway without the capability, use mark-refunded instead.

Body

{
  "amount": 50000,                    // optional; defaults to the remaining unrefunded balance
  "returnId": "01J9...",              // optional; links to an order_return row
  "reason": "Damaged on arrival"      // optional, 1..500
}
FieldTypeConstraints
amountint?Positive subunits. Defaults to the remaining balance. Already-settled and in-flight refunds both count against the ceiling, so two quick calls cannot over-refund
returnIdstring?Advances that return's refund counter and reverses the vendor's ledger entry
reasonstring?1..500; recorded on the order timeline

Response 200 — the PaymentRefund row:

{
  "id": "01J9...",
  "orderId": "01J9...",
  "returnId": null,
  "provider": "phonepe",
  "merchantRefundId": "RFND-ORD-2026-00000123-a1b2c3d4e5f6",
  "providerRefundId": "OMR7878098045517540996",
  "amountSubunits": 50000,
  "state": "PENDING",
  "reason": "Damaged on arrival",
  "initiatedBy": "01J9...",
  "createdAt": "2026-07-30T11:22:33.456Z",
  "updatedAt": "2026-07-30T11:22:33.456Z"
}

initiatedBy matters beyond audit: when the provider's webhook lands, that user is replayed as the actor on the resulting order event, so the refund is always attributable to a real person.

Errors

StatusCodeWhen
400BAD_REQUESTThe order's provider has no gateway-refund capability — use mark-refunded
400BAD_REQUESTAmount is not positive, or exceeds the remaining balance
404NOT_FOUNDUnknown id
409CONFLICTOrder is not paid / partially_refunded

GET /admin/orders/:id/refunds — Gateway refund history

Required permission: order: refund. Newest first. Includes PENDING rows, which are not yet reflected in the order's refunded amount — surface them so an operator does not double-refund while one is in flight.

Response 200PaymentRefund[].


POST /admin/orders/:id/refunds/:merchantRefundId/sync — Re-read a pending refund

Required permission: order: refund. Recovery path for a refund whose completion webhook never arrived: polls the provider and applies the result. A no-op when the refund has already settled.

Response 200 — the PaymentRefund row after syncing.

Errors

StatusCodeWhen
400BAD_REQUESTProvider cannot report refund state
404NOT_FOUNDUnknown refund, or it belongs to a different order

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

NameTypeDefaultNotes
pageint1>= 1
limitint
eventTypestring?Trimmed, 1..128. Narrow to one event class

Response 200 — paginated envelope of OrderEventResponse[].

Errors

StatusCodeWhen
404NOT_FOUNDUnknown 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 }
FieldTypeConstraints
timeoutHoursint?0..720. Defaults to admin.payment.pending_timeout_hours
limitint?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

NameTypeDefaultNotes
pageint1
limitint
statusstring?Trimmed, 1..32. Filter by return status

Response 200 — paginated envelope of ReturnResponse[].


GET /admin/returns/:id — Return detail

Required permission: order: view.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown 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
}
FieldTypeConstraints
orderVendorIdstringRequired; the sub-order being returned
reasonCodestring1..64
reasonNotesstring?max 2000
lines[]array1..100 entries
preApproveboolean?Skip vendor approval step
refundAmountOverrideint?>= 0 subunits

Response 200 — created ReturnResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod
404NOT_FOUNDOrder or sub-order not found

POST /admin/returns/:id/override — Admin override transition

Required permission: order: update. Escape hatch transitions on a wedged return.

actionEffect
"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" }
FieldTypeConstraints
actionenumOne of force_refund / approve / cancel
reasonstringRequired; 1..500

Response 200 — overridden ReturnResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod
404NOT_FOUNDUnknown id

Vendor payouts

GET /admin/vendors/:id/balance — Vendor balance summary

Required permission: payout: view.

Response 200VendorBalanceResponse.


GET /admin/vendors/:id/ledger — Vendor ledger

Required permission: payout: view.

Query

NameTypeDefaultNotes
pageint1
limitint
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

NameTypeDefaultNotes
pageint1
limitint
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 200PayoutResponse with entries[] populated.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id

GET /admin/vendors/:id/payout-config — Per-vendor payout policy

Required permission: payout: view. Returns the vendor's payout-policy override, the resolved effective values, and the platform defaults each null override inherits from. Backed by the dedicated vendor_payout_config table (previously the admin.payouts per-vendor settings group).

Response 200

{
  "override": {
    "commissionRate": 250,          // basis points, or null to inherit
    "minPayoutAmountSubunit": null, // subunits, or null to inherit
    "payoutHold": false,
    "notes": ""
  },
  "resolved": {                     // what the payout engine uses
    "commissionRate": 250,
    "minPayoutAmountSubunit": 5000,
    "payoutHold": false,
    "notes": ""
  },
  "platformDefaults": {             // admin.payouts.* fallbacks
    "commissionRate": 1000,
    "minPayoutAmountSubunit": 5000
  }
}

PUT /admin/vendors/:id/payout-config — Override a vendor's payout policy

Required permission: payout: configure. Full-representation upsert. commissionRate (basis points) and minPayoutAmountSubunit (subunits) are null to inherit the platform default; payoutHold freezes disbursements. The change is audit-logged via the vendor.payout_config_updated domain event. Vendors cannot see or edit these values.

Body

NameTypeNotes
commissionRateint (0–10000) | nullBasis points (1000 = 10%). null inherits admin.payouts.default_commission_rate.
minPayoutAmountSubunitint (≥0) | nullnull inherits admin.payouts.min_payout_amount_subunit.
payoutHoldbooleanBlocks createDraftPayout with 403 when true.
notesstring (≤5000)Private admin notes.

Response 200 — same shape as GET, reflecting the new values.


GET /admin/vendors/:id/bank-account — Vendor payout destination (masked)

Required permission: payout: view. Read-only admin view of the vendor's payout bank account with the account number masked to its last 4 digits. There is no admin write — vendors manage their own account via PUT /vendor/bank-account.

Response 200{ bankName, accountHolderName, accountNumberMasked, accountNumberLast4, routingNumber, swiftCode }, or null when none is on record.


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_payout_config.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"
}
FieldTypeNotes
periodStartISO datetime?Inclusive lower bound. Default: no lower bound
periodEndISO datetime?Inclusive upper bound. Default: now
notesstring?Trimmed, max 2000

Response 201 — draft PayoutResponse.

Errors

StatusCodeWhen
403FORBIDDENVendor on payout hold
400VALIDATION_ERRORNo 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" }
FieldTypeConstraints
bankReferencestringRequired, trimmed, 1..200
notesstring?Trimmed, max 2000

Response 200 — paid PayoutResponse.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id
409CONFLICTPayout 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" }
FieldTypeConstraints
reasonstringRequired, trimmed, 1..500

Response 200 — cancelled PayoutResponse.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown 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"
}
FieldTypeConstraints
amountsigned intNo min — manual adjustments bypass commission math
kindenum"manual" or "commission_adjustment"
descriptionstringRequired, 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 }

  • admin-rbac — gates every endpoint via order:* and payout:*. See admin-rbac.md.
  • cart — converted carts produce orders. See cart.md.
  • inventory — order placement reserves; mark-paid commits; cancel releases.
  • payment — emits payment.captured events that drive the auto mark-paid path (manual override goes through this admin endpoint).
  • shipping — vendors transition sub-orders through fulfilment states.
  • vendorvendor.payouts.payout_hold setting gates POST /admin/vendors/:id/payouts.
  • settingsadmin.payment.pending_timeout_hours drives the stale-pending sweep.
  • notifications — order events emit notification triggers; see notifications.md.

On this page

ConventionsAuthenticationResponse envelopeError envelopeMoney fieldsDomain typesOrderResponseOrderVendorSummary (admin detail only)OrderEventResponseReturnResponsePayoutResponseLedgerEntryResponseVendorBalanceResponseOrdersGET /admin/orders — List ordersGET /admin/orders/:id — Order detailPOST /admin/orders/:id/cancel — Cancel on behalf of customerPOST /admin/orders/:id/mark-paid — Manually mark paidPOST /admin/orders/:id/mark-refunded — Mark refundedPOST /admin/orders/:id/refunds — Refund through the payment gatewayGET /admin/orders/:id/refunds — Gateway refund historyPOST /admin/orders/:id/refunds/:merchantRefundId/sync — Re-read a pending refundGET /admin/orders/:id/events — Paginated audit logPOST /admin/orders/cleanup-stale-pending — Stale pending-payment sweepReturnsGET /admin/returns — List returnsGET /admin/returns/:id — Return detailPOST /admin/orders/:id/returns — Force-create a returnPOST /admin/returns/:id/override — Admin override transitionVendor payoutsGET /admin/vendors/:id/balance — Vendor balance summaryGET /admin/vendors/:id/ledger — Vendor ledgerGET /admin/vendors/:id/payouts — Vendor payoutsGET /admin/payouts — Cross-vendor payout listGET /admin/payouts/:id — Payout detailGET /admin/vendors/:id/payout-config — Per-vendor payout policyPUT /admin/vendors/:id/payout-config — Override a vendor's payout policyGET /admin/vendors/:id/bank-account — Vendor payout destination (masked)POST /admin/vendors/:id/payouts — Create a draft payoutPOST /admin/payouts/:id/mark-paid — Mark a draft as paidPOST /admin/payouts/:id/cancel — Cancel a draftPOST /admin/vendors/:id/ledger/adjust — Manual ledger adjustmentPOST /admin/payouts/promote — Manually promote pending → availableRelated modules