Supercommerce API Docs
Admin API

Free Gift Module — Admin

HTTP surface for managing free-gift rules — one row per promotion. The cart engine evaluates active rules and produces cart.pendingGifts[], which the customer resolves into an…

HTTP surface for managing free-gift rules — one row per promotion. The cart engine evaluates active rules and produces cart.pendingGifts[], which the customer resolves into an attached gift selection at checkout.

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

Three rule types: AUTOMATIC (apply a fixed quantity of pre-set variants when criteria pass), BUYXGETY (buy N matching variants → get M gift variants), and COUPON_BASED (customer applies a coupon code → receives pre-set gift variants). Each type has its own config sub-block; only one config field is allowed per rule.


Conventions

Authentication

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

Endpoint groupPermission
GET /admin/free-gifts, GET /admin/free-gifts/:idfreeGift: read
POST /admin/free-giftsfreeGift: create
PATCH /admin/free-gifts/:id, POST /admin/free-gifts/:id/restorefreeGift: update
PATCH /admin/free-gifts/:id/archive, PATCH /admin/free-gifts/:id/unarchivefreeGift: archive
DELETE /admin/free-gifts/:idfreeGift: 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
500INTERNAL_SERVER_ERROR, DATABASE_ERROR

Lifecycle

Same active / archived / deleted model as discounts.

StateHowVisibility
activeDefault after createListed and evaluated by the cart engine
archivedPATCH /:id/archiveListed (admin) but not evaluated
deletedDELETE /:id (soft)Hidden from default lists; reversible via POST /:id/restore

Currency

minAmount, maxAmount are integer subunits. Quantity/count fields are plain integers.


Domain types

Enums

type FreeGiftRuleType         = "AUTOMATIC" | "BUYXGETY" | "COUPON_BASED";
type FreeGiftBuyScope         = "VARIANT" | "BRAND" | "CATEGORY" | "TAG" | "INGREDIENT" | "VENDOR";
type FreeGiftProductMode      = "SAME" | "DIFFERENT";
type FreeGiftCriteriaScope    = "CART_SUBTOTAL" | "ORDER_TOTAL"
                              | "CATEGORY_TOTAL" | "BRAND_TOTAL" | "TAG_TOTAL"
                              | "INGREDIENT_TOTAL" | "VENDOR_TOTAL";
type FreeGiftFilterMode       = "INCLUDE" | "EXCLUDE";
type Platform                 = "APP" | "WEB" | "BOTH";
type CustomerScope            = "ALL" | "INCLUDE" | "EXCLUDE";
type PurchaseHistoryMode      = "DISABLED" | "FIRST_ORDER" | "MIN_ORDERS";

FreeGiftResponse

type FreeGiftFilterEntry = { id: string; mode: FreeGiftFilterMode };

type FreeGiftResponse = {
  id: string;
  name: string;
  description: string | null;
  isActive: boolean;
  archivedAt: string | null;
  platform: Platform;

  type: FreeGiftRuleType;

  automaticConfig: {
    quantity: number;
    variantIds: string[];
  } | null;

  buyXGetYConfig: {
    buyScope: FreeGiftBuyScope;
    buyScopeIds: string[];
    buyQuantity: number;
    getQuantity: number;
    giftProductMode: FreeGiftProductMode;
    giftVariantIds: string[];
    repeatGift: boolean;
    repeatLimit: number | null;
  } | null;

  couponConfig: {
    couponCode: string;
    couponQuantity: number;
    variantIds: string[];
  } | null;

  criteriaScope: FreeGiftCriteriaScope;
  criteriaScopeIds: string[];
  minAmount: number | null;        // subunits
  maxAmount: number | null;
  minQuantity: number | null;
  maxQuantity: number | null;
  minProductCount: number | null;
  maxProductCount: number | null;

  startsAt: string | null;
  endsAt: string | null;
  totalUsageLimit: number | null;
  usageLimitPerCustomer: number | null;
  requireCustomerLogin: boolean;
  purchaseHistoryMode: PurchaseHistoryMode;
  minOrderCount: number | null;
  individualUsageOnly: boolean;

  customerScope: CustomerScope;
  customerUserIds: string[];

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

  showOnCart: boolean;

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

Endpoints

Base path: /admin/free-gifts.

GET /admin/free-gifts — List rules

Required permission: freeGift: read.

Query

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

Response 200 — paginated envelope of FreeGiftResponse[].


GET /admin/free-gifts/:id — Get a rule

Required permission: freeGift: read.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id

POST /admin/free-gifts — Create a rule

Required permission: freeGift: create. Cross-field rules enforced by zod:

  • type=AUTOMATICautomaticConfig is required; buyXGetYConfig and couponConfig must be absent.
  • type=BUYXGETYbuyXGetYConfig is required; automaticConfig and couponConfig must be absent.
    • If giftProductMode=DIFFERENT, giftVariantIds[] must be non-empty; if SAME, must be empty.
    • If repeatGift=false, repeatLimit must be absent (or null).
  • type=COUPON_BASEDcouponConfig is required; automaticConfig and buyXGetYConfig must be absent.
  • criteriaScope in {CART_SUBTOTAL, ORDER_TOTAL}criteriaScopeIds must be empty.
  • criteriaScope in {CATEGORY_TOTAL, BRAND_TOTAL, TAG_TOTAL, INGREDIENT_TOTAL, VENDOR_TOTAL}criteriaScopeIds must be non-empty.
  • purchaseHistoryMode=MIN_ORDERS requires minOrderCount >= 1.
  • customerScope != ALL requires non-empty customerUserIds.
  • If both set, minAmount <= maxAmount, minQuantity <= maxQuantity, minProductCount <= maxProductCount.
  • If both set, startsAt < endsAt.

Body

{
  "name": "Buy 2 lipsticks, get 1 free",
  "description": "Mix and match across the lipstick category.",
  "isActive": true,
  "platform": "BOTH",

  "type": "BUYXGETY",

  "buyXGetYConfig": {
    "buyScope": "CATEGORY",
    "buyScopeIds": ["01J9..."],
    "buyQuantity": 2,
    "getQuantity": 1,
    "giftProductMode": "SAME",
    "giftVariantIds": [],
    "repeatGift": true,
    "repeatLimit": 3
  },

  "criteriaScope": "CART_SUBTOTAL",
  "criteriaScopeIds": [],
  "minAmount": 100000,
  "maxAmount": null,

  "startsAt": null,
  "endsAt": null,
  "totalUsageLimit": null,
  "usageLimitPerCustomer": 1,
  "requireCustomerLogin": false,
  "purchaseHistoryMode": "DISABLED",
  "minOrderCount": null,
  "individualUsageOnly": false,

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

  "variants":    [],
  "categories":  [],
  "brands":      [],
  "tags":        [],
  "ingredients": [],
  "vendors":     [],

  "showOnCart": true
}

Field shapes are documented in the type definitions above. Validation summary:

FieldTypeConstraints
namestring1..255
descriptionstring | null?max 2000
couponConfig.couponCodestring2..50, uppercase alnum + -_
buyXGetYConfig.buyQuantity / getQuantityint>= 1
automaticConfig.quantityint>= 1
*.variantIds / buyScopeIdsstring[]Non-empty arrays where required
criteriaScopeIdsstring[]Required non-empty for per-entity criteria scopes

Response 201FreeGiftResponse.

Errors

StatusCodeWhen
400VALIDATION_ERRORBody fails zod or cross-field refinements

PATCH /admin/free-gifts/:id — Update a rule

Required permission: freeGift: update. Partial. All cross-field rules re-run on the merged state.

Response 200 — updated FreeGiftResponse.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id
400VALIDATION_ERRORBody fails zod

PATCH /admin/free-gifts/:id/archive — Archive

Required permission: freeGift: archive. Stamps archivedAt; rule stops being evaluated by the cart engine but stays visible in admin.

Response 200FreeGiftResponse.


PATCH /admin/free-gifts/:id/unarchive — Unarchive

Required permission: freeGift: archive.

Response 200FreeGiftResponse.


DELETE /admin/free-gifts/:id — Soft-delete

Required permission: freeGift: delete.

Response 200 — deleted FreeGiftResponse.


POST /admin/free-gifts/:id/restore — Restore

Required permission: freeGift: update.

Response 200 — restored FreeGiftResponse.

Errors

StatusCodeWhen
404NOT_FOUNDUnknown id

  • admin-rbac — gates every endpoint via freeGift:*. See admin-rbac.md.
  • cart — evaluates active free-gift rules and produces cart.pendingGifts[]. See cart.md.
  • discountcouponConfig.couponCode references a discount.code at evaluation time.
  • catalogvariants, categories, brands, tags, ingredients, buyScopeIds, criteriaScopeIds, *Config.variantIds all reference catalog ids.
  • vendorvendors[] references vendor ids.

On this page