Supercommerce API Docs
Admin API

Discount Module — Admin

HTTP surface for managing platform-wide discount coupons: percentage or fixed-amount discounts, with rich targeting (variants / categories / brands / tags / ingredients / vendors…

HTTP surface for managing platform-wide discount coupons: percentage or fixed-amount discounts, with rich targeting (variants / categories / brands / tags / ingredients / vendors via include/exclude filter entries), customer-scope rules, usage limits, time windows, sale-item exclusion, and a free-shipping flag.

Source: api-modules/discount/src/controllers/admin-discount.controller.ts.

Discounts are evaluated by the cart pricing pipeline at apply-coupon time. The applied coupon is snapshotted into cart.appliedCoupons[] and again into order.appliedCoupons[] at place-order — mutations after place-order do not retroactively change a finalised order.


Conventions

Authentication

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

Endpoint groupPermission
GET /admin/discounts, GET /admin/discounts/:iddiscount: read
POST /admin/discountsdiscount: create
PATCH /admin/discounts/:id, POST /admin/discounts/:id/restorediscount: update
PATCH /admin/discounts/:id/archive, PATCH /admin/discounts/:id/unarchivediscount: archive
DELETE /admin/discounts/:iddiscount: delete

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
409UNIQUE_VIOLATION (code already taken)
500INTERNAL_SERVER_ERROR, DATABASE_ERROR

Lifecycle

Discounts have three states:

StateHowVisibility
activeDefault after createListed and applicable at checkout
archivedPATCH /:id/archiveListed (in admin) but not applicable
deletedDELETE /:id (soft)Hidden from default lists; reversible via POST /:id/restore

code is immutable post-create — changing a live coupon's code confuses customers and breaks usage tracking.

Currency

value (when discountType=FIXED), minOrderAmount, and maxOrderAmount are integer subunits (paise / cents).


Domain types

DiscountResponse

type DiscountType         = "FIXED" | "PERCENTAGE";
type Platform             = "APP" | "WEB" | "BOTH";
type CustomerScope        = "ALL" | "INCLUDE" | "EXCLUDE";
type PurchaseHistoryMode  = "DISABLED" | "FIRST_ORDER" | "MIN_ORDERS";
type DiscountFilterMode   = "INCLUDE" | "EXCLUDE";

type DiscountFilterEntry = {
  id: string;
  mode: DiscountFilterMode;
};

type DiscountResponse = {
  id: string;
  name: string;                              // human-readable label
  code: string;                              // uppercase alnum + `-_`, 2..50
  isActive: boolean;
  archivedAt: string | null;                 // ISO
  platform: Platform;

  discountType: DiscountType;
  value: number;                             // subunits (FIXED) or whole-percent 1..100 (PERCENTAGE)

  minOrderAmount: number | null;             // subunits; null = no floor
  maxOrderAmount: number | null;             // subunits; null = no cap

  freeShipping: boolean;
  requireCustomerLogin: boolean;
  showOnCart: boolean;                       // suggest on cart page

  totalUsageLimit: number | null;            // platform-wide redemption cap
  usageLimitPerCustomer: number | null;
  startsAt: string | null;                   // ISO
  endsAt: string | null;                     // ISO
  individualUsageOnly: boolean;              // cannot stack with other coupons
  excludeSaleItems: boolean;
  excludeSaleItemsOverPercent: number | null; // 1..100

  purchaseHistoryMode: PurchaseHistoryMode;
  minOrderCount: number | null;              // required when MIN_ORDERS

  customerScope: CustomerScope;
  customerUserIds: string[];                 // applicable user ids when scope != ALL

  variants:    DiscountFilterEntry[];
  categories:  DiscountFilterEntry[];
  brands:      DiscountFilterEntry[];
  tags:        DiscountFilterEntry[];
  ingredients: DiscountFilterEntry[];
  vendors:     DiscountFilterEntry[];

  createdAt: string;
  updatedAt: string;
  deletedAt: string | null;
};

Endpoints

Base path: /admin/discounts.

GET /admin/discounts — List discounts

Required permission: discount: read.

Query

NameTypeDefaultNotes
qstring?Free-text search (trimmed, min 1)
status"active" | "archived" | "deleted" | "all""active"Lifecycle filter
platformPlatform?APP / WEB / BOTH
isActiveboolean?Coerced
sortBy"createdAt" | "updatedAt" | "name" | "code" | "endsAt""createdAt"
sortDirection"asc" | "desc""desc"
limitint1001..500
offsetint0>= 0

Response 200 — paginated envelope of DiscountResponse[].


GET /admin/discounts/:id — Get a discount

Required permission: discount: read.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id

POST /admin/discounts — Create a discount

Required permission: discount: create.

Body

{
  "name": "Welcome 10%",
  "code": "WELCOME10",
  "isActive": true,
  "platform": "BOTH",

  "discountType": "PERCENTAGE",
  "value": 10,                            // 1..100 for PERCENTAGE; >= 1 subunits for FIXED

  "minOrderAmount": 50000,                // subunits; null/optional
  "maxOrderAmount": null,

  "freeShipping": false,
  "requireCustomerLogin": false,
  "showOnCart": true,

  "totalUsageLimit": null,
  "usageLimitPerCustomer": 1,
  "startsAt": "2026-05-01T00:00:00.000Z",
  "endsAt":   "2026-06-01T00:00:00.000Z",
  "individualUsageOnly": false,
  "excludeSaleItems": false,
  "excludeSaleItemsOverPercent": null,

  "purchaseHistoryMode": "FIRST_ORDER",
  "minOrderCount": null,

  "customerScope": "ALL",
  "customerUserIds": [],

  "variants":    [{ "id": "01J9...", "mode": "EXCLUDE" }],
  "categories":  [],
  "brands":      [{ "id": "01J9...", "mode": "INCLUDE" }],
  "tags":        [],
  "ingredients": [],
  "vendors":     []
}
FieldTypeConstraints
namestring1..255
codestring2..50; ^[A-Z0-9_-]+$. Immutable on update
platformenumDefault BOTH
discountTypeenumFIXED or PERCENTAGE
valueintFIXED: >= 1 subunits. PERCENTAGE: 1..100 whole percent
minOrderAmount / maxOrderAmountint? | null>= 0. If both set, min <= max
totalUsageLimit / usageLimitPerCustomerint? | null>= 1
startsAt / endsAtISO datetime? | nullIf both, startsAt < endsAt
excludeSaleItemsOverPercentint? | null1..100
purchaseHistoryModeenumDISABLED / FIRST_ORDER / MIN_ORDERS. MIN_ORDERS requires minOrderCount >= 1
customerScopeenumALL / INCLUDE / EXCLUDE. Non-ALL requires non-empty customerUserIds
customerUserIdsstring[]Better-Auth user ids
variants / categories / brands / tags / ingredients / vendorsDiscountFilterEntry[]{ id, mode: "INCLUDE" | "EXCLUDE" }; default []

Response 201DiscountResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod or cross-field refinements
409UNIQUE_VIOLATIONcode already exists

PATCH /admin/discounts/:id — Update a discount

Required permission: discount: update. Partial — every field optional except code (omitted entirely). Cross-field refinements re-run on the merged result.

Response 200 — updated DiscountResponse.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id
400VALIDATION_ERRORBody fails zod / cross-field rules

PATCH /admin/discounts/:id/archive — Archive

Required permission: discount: archive. Stamps archivedAt; the discount becomes inapplicable at checkout but remains visible in admin.

Response 200 — archived DiscountResponse.


PATCH /admin/discounts/:id/unarchive — Unarchive

Required permission: discount: archive. Clears archivedAt.

Response 200DiscountResponse.


DELETE /admin/discounts/:id — Soft-delete

Required permission: discount: delete. Stamps deletedAt; hidden from default lists. Reversible via POST /:id/restore.

Response 200 — deleted DiscountResponse.


POST /admin/discounts/:id/restore — Restore

Required permission: discount: update. Clears deletedAt.

Response 200 — restored DiscountResponse.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id

  • admin-rbac — gates every endpoint via discount:*. See admin-rbac.md.
  • cart — applies discounts at coupon-apply time and snapshots them into cart.appliedCoupons[]. See cart.md.
  • order — re-snapshots applied coupons at place-order; the discount module's mutations do not retroactively affect placed orders.
  • catalogvariants, categories, brands, tags, ingredients filter entries reference catalog ids.
  • vendorvendors[] filter entries reference vendor ids.
  • customercustomerUserIds[] references Better-Auth user ids.

On this page