Promotions

Promotions are Flint's discount rule engine for automatic discounts, code campaigns, buy-X-get-Y offers, eligibility conditions, and stacking control. A promotion is the campaign-level rule. Promotion codes are independent child resources, so one promotion can have many independently expiring codes or no codes at all.

Core shape#

application_method decides what the promotion does. eligibility_rules decides when it can apply. discount_class decides whether it discounts the order, line items, or service charges. combines_with, exclusivity, and stacking_mode decide how it interacts with other promotion discounts on the same order.

Only name and application_method are required. discount_class defaults to order, redemption_type defaults to automatic (or code when you include codes), display_name defaults to name, stacking_mode defaults to continue, and allocation defaults by class. name is your internal campaign label; display_name is the buyer-facing string shown at checkout. Set display_name only when your name is operational rather than buyer-safe.

All timestamps (created_at, updated_at, expires_at, schedule.starts_at, schedule.ends_at) are RFC3339 UTC, for example 2026-04-01T00:00:00Z, on both read and write.

Rule groups#

Rule groups use an array shorthand for a simple list of rules (an implicit AND), or { "all": [...] } / { "any": [...] } for nested logic. A group sets exactly one of the array shorthand, all, or any. Rule values are public JSON primitives or money objects, and values is always an array even for a single value:

JSON
[
  { "attribute": "line_item.categories", "operator": "in", "values": ["hats", "shirts"] },
  { "attribute": "customer.is_verified", "operator": "eq", "values": [true] },
  { "attribute": "order.subtotal", "operator": "gte", "values": [{ "amount": 5000, "currency": "USD" }] }
]

Rule attributes#

Every rule targets one attribute. The attribute determines the value type and which operators are allowed; sending an unknown attribute, a disallowed operator, or a value of the wrong type is rejected at create time (you will not ship a promotion that silently never applies). metadata.<key> reads a string from the customer's metadata (for example metadata.channel). Orders without a customer, or customers without the key, fail closed unless you use is_defined.

AttributeValue typeOperatorsNotes
order.subtotalmoneygt, gte, lt, lteOrder subtotal in the order currency.
matched_items.subtotalmoneygt, gte, lt, lteSubtotal of only the line items matched by the same rule group.
order.currencystringeq, ne, in, contains, is_definedISO currency code, for example USD.
customer.group_idstringeq, in, is_definedCustomer group identifier.
customer.is_verifiedbooleaneqWhether the customer is verified.
line_item.product_idstringeq, ne, in, contains, is_definedCatalog product ID (prod_...).
line_item.variant_idstringeq, ne, in, contains, is_definedCatalog variant ID (var_...).
line_item.bundle_idstringeq, ne, in, contains, is_definedCatalog bundle ID.
line_item.categoriescategory handleeq, ne, in, is_definedMatches the line item's catalog categories. ne means none of its categories match.
line_item.quantitynumbereq, ne, gt, gte, lt, lte, in, is_definedUnsettled quantity of the line item.
charge.typestringeq, ne, in, contains, is_definedService charge type: service_fee, delivery_fee, shipping_fee, handling_fee, and similar.
delivery_choice.typestringeq, ne, in, contains, is_definedSelected delivery type: shipment, pickup, or local_delivery.
delivery_choice.method_idstringeq, ne, in, contains, is_definedStable delivery method ID selected for a choice group.
delivery_choice.raw_amountmoneygt, gte, lt, lteDelivery amount before delivery discounts and tax.
delivery_choice_group.idstringeq, ne, in, contains, is_definedChoice-group ID that owns the selected method and any attached service charge.
metadata.<key>stringeq, ne, in, contains, is_definedReads a string value from the customer's metadata.

Additional operator rules:

  • Only in accepts more than one value. Every other operator with more than one value is rejected.
  • When an attribute allows is_defined, it takes no values (and no currency_options); it tests only for presence.
  • contains is a case-insensitive substring match on a single string value, not list membership. For "attribute is one of these values," use in.
  • Money values require a non-empty currency, and money-valued rules may carry a currency_options map to set per-currency thresholds.

The same attribute/type/operator table is published in the OpenAPI spec as the x-flint-rule-attributes extension on the PromotionRule schema, so you can generate or validate rules against it directly. If you send an operator an attribute does not allow, the request is rejected at create time with INVALID_OPERATOR, and the error message lists the operators that are valid for that attribute.

List promotion codes#

Call GET /v1/promotion-codes to find a code when its parent promotion is unknown, or to list a promotion's codes with promotion_id. The code filter is an exact match that ignores case and accents: CAFÉ matches cafe. This is the same rule that makes promotion codes unique. Codes preserve their original spelling. Leading and trailing whitespace are rejected, not trimmed.

cURL
curl 'https://api.withflintpay.com/v1/promotion-codes?code=summer10&expand=promotion' \
  -H "Authorization: Bearer YOUR_API_KEY"

Each item in data is a promotion code. expand=promotion adds its parent promotion without removing promotion_id; both the list and expansion require commerce.promotions.read. The list includes every code status: active, inactive, expired, and exhausted. Read status to determine whether the code is active, or filter by exactly one status. Deleted codes are excluded. A code that does not exist returns an empty array.

Results are ordered by created_at descending, then promotion_code_id descending. page_size defaults to 20 and accepts 1 through 100. Pass next_page_token as page_token for the next page, keeping the other parameters unchanged.

Create a discount preview#

Call POST /v1/discount-previews before applying a code to show its expected discount or an unmet purchase minimum. Supply the required order_id and an optional discount. Omit discount to evaluate automatic promotions for the order.

cURL
curl -X POST https://api.withflintpay.com/v1/discount-previews \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"order_id":"ord_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J","discount":{"promotion":{"promotion_code":"SUMMER10"}}}'

The response is 200 OK with applied, skipped, and available arrays directly inside data. The preview does not change the order, create an addressable resource or ID, or require an idempotency key. It uses commerce.orders.read and also accepts the order's checkout session credentials. Merchant credentials can select a promotion by ID or code; checkout session credentials must select by code.

Automatic promotion#

Automatic promotions apply during order pricing when they are active, eligible, and allowed by the merchant or checkout promotion settings.

Response
{
  "name": "Hat launch",
  "display_name": "20% off hats",
  "redemption_type": "automatic",
  "discount_class": "line_item",
  "application_method": {
    "type": "percent_off",
    "percent_off": 20,
    "discounted_item_rules": [
      { "attribute": "line_item.categories", "operator": "eq", "values": ["hats"] }
    ]
  }
}

Eligibility vs. targeting vs. class#

Three fields decide who qualifies, what gets discounted, and which layer the discount sits on. They are distinct on purpose:

  • eligibility_rules is the gate: conditions the order, customer, or items must satisfy before the promotion applies at all. Omit it and the promotion always qualifies.
  • The effect's discounted_item_rules (on percent_off and amount_off) is the target: which line items receive the discount.
  • discount_class is the layer: order discounts the whole order, line_item discounts matched items, service_charge discounts fees. It also changes the math (a whole-order percent vs. a per-item percent).

For a line_item promotion, discounted_item_rules is required unless your eligibility_rules already consists only of line-item predicates (line_item.* or matched_items.subtotal), in which case those matched items are the discount target. If your eligibility_rules gate on order.subtotal (an order-level condition), you must also send discounted_item_rules to say which items to discount, or you will get DISCOUNTED_ITEM_RULES_REQUIRED.

Allocation#

allocation controls how the effect spreads across the matched items when more than one qualifies. It defaults to each for line_item and service_charge promotions and across for order promotions.

  • each applies the effect to every matched item independently. A fixed amount_off is charged per item (10 USD off 3 matched items discounts 30 USD, each capped at that item's price); a percent_off is taken from each item.
  • across applies the effect once to the matched set as a whole. A fixed amount_off is the whole amount one time (10 USD off the group, not prorated per item); a percent_off is taken from the combined subtotal.

The distinction matters most for fixed amount_off, where each and across differ by the number of matched items. For percent_off both produce the same total and differ only in how the discount is attributed to individual line items. max_discounted_quantity requires each.

Code campaign#

Use redemption_type: "code" when the buyer or server must provide a promotion code. Create and manage codes through /v1/promotions/{promotion_id}/codes, and list a promotion's codes with GET /v1/promotion-codes?promotion_id=promo_1kmn0aExample; codes are not a scalar field on the promotion because a campaign can issue, disable, expire, and cap many codes independently.

JSON
{
  "name": "VIP campaign",
  "display_name": "VIP savings",
  "redemption_type": "code",
  "discount_class": "order",
  "application_method": {
    "type": "amount_off",
    "amount_off_money": { "amount": 1000, "currency": "USD" }
  },
  "codes": [{ "code": "VIP10", "max_uses": 500 }]
}

Apply a discount to an order#

Apply promotions through POST /v1/orders/{order_id}/discounts, using a discount body with exactly one of promotion or manual:

JSON
{ "promotion": { "promotion_code": "VIP10" } }

Use promotion_id for server-side apply flows where you already know the promotion. Use promotion_code when redeeming a customer-entered campaign code. Line-item targeting is rule-driven, not caller-driven: order_line_item_ids is not accepted for promotion discounts because the promotion rules decide the targets. Only a manual discount carries its own manual.order_line_item_ids.

See the Promotions guide for the full apply, preview, reprice, and remove walkthrough, including how a valid-but-unapplied code is reported.

Decline reasons#

When a promotion does not apply, Flint always tells you why with a stable, machine-readable reason. Where the reason rides depends on the surface (the value set is the same everywhere):

  • On an apply error (4xx): error.reason.
  • On an apply success where a code was accepted but did not win: meta.warnings[].reason.
  • On a preview result: promotion_decline_reason on each candidate.

The reason is one of:

ReasonMeaning
not_eligibleEligibility rules were not satisfied (also the fallback for any reason a client does not recognize).
minimum_not_metA spend minimum was not reached. Apply errors carry required_money, current_money, and gap_money; preview available[] candidates carry threshold_money, current_money, and gap_money.
expiredThe code or promotion ended (schedule.ends_at in the past, or the code expired).
not_yet_startedThe promotion's schedule.starts_at is in the future.
exhaustedThe code's or promotion's max_uses is reached.
code_requiredThe promotion is code-gated and no matching code was entered.
code_invalidNo code record matches the entered string (a typo or unknown code).
disabledThe promotion is inactive.
automatic_disabledMerchant settings have automatic promotions turned off.
codes_disabledMerchant settings have code promotions turned off.
already_appliedThe promotion is already applied to the order.
not_combinableA combines_with class conflict with an existing discount.
superseded_by_better_offerA higher-value discount in the same exclusivity group applied instead.
supersededAn admitted stop_after discount earlier in the order halted this one.
max_promotions_reachedThe order hit max_promotions_per_order.
no_discountable_balanceNothing left to discount.
buy_item_missingBuy-X-get-Y qualifying items are not present.
currency_mismatchThe discount or threshold currency does not match the order currency.
unknown_typeThe promotion uses a newer grammar than this integration understands.

Branch your buyer-facing copy on the reason value, not the human message. A new backend reason a client does not yet know surfaces as not_eligible, so always keep a generic fallback.

Status#

Returned status is computed. It is one of active, inactive, expired, not_yet_started, exhausted, no_active_codes, or archived. no_active_codes means a code-gated promotion has no usable codes; exhausted means max_uses is reached.

A subscription uses a promotion once, when it starts. Its renewals don't count toward max_uses, so reaching the limit stops new signups but never refuses a renewal. A renewal gets the discount only as the promotion's recurrence allows.

GET /v1/promotions?status= filters on the returned value and matches any of several: repeat the parameter or separate values with commas. ?status=active,no_active_codes lists every promotion that is switched on and inside its dates, including code-gated ones that currently have no usable code. ?status=expired,exhausted lists the ones that are over. A page token belongs to the status set that issued it, so start again from the first page when the set changes.

A code-gated promotion also returns codes_summary, so a list can show how buyers redeem each promotion without fetching every promotion's codes:

JSON
"codes_summary": { "total_count": 3, "active_count": 2, "newest_active_code": "SUMMER10" }

total_count counts the promotion's codes, leaving out deleted ones. active_count counts the codes a buyer can redeem now: active, not expired, and under their max_uses. When it is 0 the promotion's status is no_active_codes, and newest_active_code is omitted. Automatic promotions have no codes and omit codes_summary.

PATCH /v1/promotions/{id} accepts only active or inactive for status, which updates the stored base state. The other returned values are derived from the schedule, usage, and codes; you cannot set them directly. DELETE /v1/promotions/{id} is a soft delete: the promotion moves to status: archived (a subsequent GET returns it with that status rather than a 404), drops out of default list results, and its codes stop working.

Stacking and conflicts#

Manual discounts are admitted first and are not removed by promotion conflict resolution. Promotion candidates are then evaluated together, including already pending promotion discounts and newly eligible automatic promotions.

combines_with is bidirectional: two promotion discounts can coexist only when each promotion allows the other's discount_class. If you omit combines_with, the promotion combines with every class (order, line_item, and service_charge) by default, up to max_promotions_per_order; set it explicitly to restrict stacking rather than to enable it.

exclusivity.group limits a set of promotions to one winner. It is a free-form string you define, matched by exact equality, so keep the naming consistent (a typo like bogo vs. BOGO splits one group into two and lets both promotions apply). Within a group, exclusivity.selection is either best_of (the highest standalone amount wins) or highest_priority (the highest exclusivity.priority wins; larger numbers win).

stacking_mode: "stop_after" stops later promotion candidates after that promotion is admitted. Merchant and checkout settings can also disable automatic promotions, disable promotion codes, or cap max_promotions_per_order.

Pending promotion discounts are dynamic until payment. If the order changes, Flint recalculates eligible pending promotions, removes candidates that no longer qualify, and admits newly eligible automatic promotions using the same conflict rules. Most integrations do not need to trigger this themselves; it runs automatically on every order mutation. If you need an explicit refresh before a final review screen, call POST /v1/orders/{order_id}/discounts/reprice. After payment, redeemed discounts are frozen for settlement and refunds.

Multi-currency#

percent_off is currency-agnostic and works in any order currency. amount_off is denominated in amount_off_money.currency. To support other currencies, add a currency_options map keyed by currency code:

JSON
"application_method": {
  "type": "amount_off",
  "amount_off_money": { "amount": 1000, "currency": "USD" },
  "currency_options": { "EUR": { "amount": 900, "currency": "EUR" } }
}

An order whose currency is neither amount_off_money.currency nor present in currency_options does not receive the discount; the promotion is declined with currency_mismatch rather than converted. The same applies to money-valued rule thresholds (order.subtotal), which can carry their own currency_options.

The Promotion object#

Every field on a promotion, as returned by retrieve and carried by the endpoints below.

Attributes

application_methodone ofRequired

Canonical REST shape is flat: send type plus the effect fields on this object. Responses always use this flat shape.

codes_summaryobject

Read-only rollup of this promotion's codes. Present only on code-gated promotions; automatic promotions omit it.

combines_withobject
created_atstring

RFC3339 timestamp.

descriptionstring
discount_classenumRequired
  • order
  • line_item
  • service_charge
display_namestringRequired

Buyer-facing promotion name shown in checkout and order discount surfaces.

eligibility_rulesone of

A rule group is exactly one of three forms, never a blend: an array of rules for a simple list (an implicit AND), an object with only all for nested AND, or an object with only any for OR. The all and any arrays contain rules or nested rule groups. There is no rules key. Sending a rules key, an unexpected key, or both all and any is rejected with INVALID_RULE_GROUP.

exclusivityobject
external_reference_idstring

Caller-owned identifier for this resource in an external system.

max_usesinteger
merchant_idstring
metadatamap of string
namestringRequired

Internal promotion name for dashboards, searching, and reporting.

promotion_idstringRequired
redemption_typeenumRequired
  • automatic
  • code
scheduleobject
stacking_modeenumRequired
  • continue
  • stop_after
statusenumRequired

Read-only, computed from the schedule, usage, and codes: active, inactive, expired, not_yet_started, exhausted, no_active_codes, or archived. Only active and inactive can be set via PATCH.

  • active
  • inactive
  • expired
  • not_yet_started
  • exhausted
  • no_active_codes
  • archived
updated_atstring

RFC3339 timestamp.

uses_countintegerRequired
JSON
{
  "application_method": {
    "allocation": "across",
    "calculation_basis": "subtotal_pre_tax",
    "discounted_item_rules": [
      {
        "attribute": "line_item.categories",
        "operator": "eq",
        "values": [
          "tickets"
        ]
      }
    ],
    "percent_off": 20,
    "type": "percent_off"
  },
  "codes_summary": {
    "active_count": 2,
    "newest_active_code": "SPRING20",
    "total_count": 3
  },
  "combines_with": {
    "line_item": false,
    "order": true,
    "service_charge": true
  },
  "created_at": "2026-03-17T14:30:00Z",
  "discount_class": "line_item",
  "display_name": "20% off spring tickets",
  "max_uses": 500,
  "merchant_id": "mer_123",
  "metadata": {
    "campaign": "spring_launch"
  },
  "name": "Spring ticket launch Q2",
  "promotion_id": "promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J",
  "redemption_type": "code",
  "schedule": {
    "ends_at": "2026-04-01T00:00:00Z"
  },
  "stacking_mode": "continue",
  "status": "active",
  "updated_at": "2026-03-17T14:30:00Z",
  "uses_count": 24
}

List promotion codes#

GET/v1/promotion-codes

Requires scope commerce.promotions.read or commerce.promotions.write

Returns a paginated list of promotion codes in every status, ordered by created_at descending, then promotion_code_id descending. Deleted codes are excluded.

Query parameters

promotion_idstring

Exact match on the parent promotion ID.

codestring

Exact match that ignores case and accents: CAFÉ matches cafe. This is the same rule that makes promotion codes unique. Codes preserve their original spelling. Leading and trailing whitespace are rejected, not trimmed.

statusenum

Filter by exactly one promotion code status.

  • active
  • inactive
  • expired
  • exhausted
page_sizeinteger

Page size, default 20, max 100.

page_tokenstring

Cursor returned by the previous list response.

expandarray of enum

Supported expansions: promotion. Expand each code's parent promotion. Expansion requires commerce.promotions.read. Limits: at most 10 unique expand paths per request; path depth at most 2. Repeat expand, for example expand=promotion&expand=promotion, or pass one comma-separated value.

  • promotion

Response · 200

dataarray of objectRequired
metaobject
next_page_tokenstring
request_idstring
curl https://api.withflintpay.com/v1/promotion-codes \
  -H "Flint-Version: 2026-09-07" \
  -H "Authorization: Bearer YOUR_API_KEY"
JSON
{
  "data": [
    {
      "code": "SPRING20",
      "created_at": "2026-03-17T14:30:00Z",
      "expires_at": "2026-04-01T00:00:00Z",
      "max_uses": 500,
      "merchant_id": "mer_123",
      "metadata": {
        "campaign": "spring_launch"
      },
      "promotion_code_id": "pcode_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J",
      "promotion_id": "promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J",
      "status": "active",
      "updated_at": "2026-03-17T14:30:00Z",
      "uses_count": 24
    }
  ],
  "next_page_token": "example",
  "request_id": "bce56cba-0827-44aa-bb56-4f200ba15ee6"
}

List promotions#

GET/v1/promotions

Requires scope commerce.promotions.read or commerce.promotions.write

Returns a paginated list of promotions for the authenticated merchant.

Query parameters

page_sizeinteger

Page size, default 20, max 100.

page_tokenstring

Cursor returned by the previous list response.

statusarray of enum

Filter by promotion status. Repeat the parameter or pass comma-separated values to match multiple statuses.

  • active
  • inactive
  • expired
  • not_yet_started
  • exhausted
  • no_active_codes
  • archived
external_reference_idstring

Exact-match filter on the caller-owned external reference ID.

querystring

Search across promotion ID, external reference ID, name, display name, description, and promotion codes. Text fields match any part of the value, and %, _ and \ are ordinary characters, not wildcards. IDs match from the start and need the type prefix, such as ord_01.

product_idstring

Filter to promotions with an explicit product target rule. Universal promotions are not included by catalog target filters.

variant_idstring

Filter to promotions with an explicit variant target rule. Universal promotions are not included by catalog target filters.

bundle_idstring

Filter to promotions with an explicit bundle target rule. Universal promotions are not included by catalog target filters.

category_handlestring

Filter to promotions with an explicit category target rule. Universal promotions are not included by catalog target filters.

redemption_typeenum

Filter by promotion redemption type.

  • automatic
  • code
discount_classenum

Filter by promotion discount class.

  • order
  • line_item
  • service_charge
sort_byenum

Sort field.

  • name
  • created_at
  • updated_at
sort_directionenum

Sort direction.

  • asc
  • desc
created_afterstring

RFC3339 lower bound for created_at.

created_beforestring

RFC3339 upper bound for created_at.

updated_afterstring

RFC3339 lower bound for updated_at.

updated_beforestring

RFC3339 upper bound for updated_at.

Response · 200

dataarray of objectRequired
metaobject
next_page_tokenstring
request_idstring
curl https://api.withflintpay.com/v1/promotions \
  -H "Flint-Version: 2026-09-07" \
  -H "Authorization: Bearer YOUR_API_KEY"
JSON
{
  "data": [
    {
      "application_method": {
        "allocation": "across",
        "calculation_basis": "subtotal_pre_tax",
        "discounted_item_rules": [
          {
            "attribute": "line_item.categories",
            "operator": "eq",
            "values": [
              "tickets"
            ]
          }
        ],
        "percent_off": 20,
        "type": "percent_off"
      },
      "codes_summary": {
        "active_count": 2,
        "newest_active_code": "SPRING20",
        "total_count": 3
      },
      "combines_with": {
        "line_item": false,
        "order": true,
        "service_charge": true
      },
      "created_at": "2026-03-17T14:30:00Z",
      "discount_class": "line_item",
      "display_name": "20% off spring tickets",
      "max_uses": 500,
      "merchant_id": "mer_123",
      "metadata": {
        "campaign": "spring_launch"
      },
      "name": "Spring ticket launch Q2",
      "promotion_id": "promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J",
      "redemption_type": "code",
      "schedule": {
        "ends_at": "2026-04-01T00:00:00Z"
      },
      "stacking_mode": "continue",
      "status": "active",
      "updated_at": "2026-03-17T14:30:00Z",
      "uses_count": 24
    }
  ],
  "next_page_token": "example",
  "request_id": "bce56cba-0827-44aa-bb56-4f200ba15ee6"
}

Create promotion#

POST/v1/promotionsIdempotent

Requires scope commerce.promotions.write

Creates a promotion for the authenticated merchant.

Request body

application_methodone ofRequired

Canonical REST shape is flat: send type plus the effect fields on this object. Responses always use this flat shape.

codesarray of object

Write-only create sugar for code-gated promotions. Accepted only on create; it is not returned on read and cannot be changed via PATCH. Manage codes after create via the /v1/promotions/{promotion_id}/codes child routes.

combines_withobject
descriptionstring
discount_classenum

Defaults to order when omitted.

  • order
  • line_item
  • service_charge
display_namestring

Buyer-facing promotion name shown in checkout and order discount surfaces. Defaults to name when omitted.

eligibility_rulesone of

A rule group is exactly one of three forms, never a blend: an array of rules for a simple list (an implicit AND), an object with only all for nested AND, or an object with only any for OR. The all and any arrays contain rules or nested rule groups. There is no rules key. Sending a rules key, an unexpected key, or both all and any is rejected with INVALID_RULE_GROUP.

exclusivityobject
external_reference_idstring

Caller-owned identifier for this resource in an external system.

max_usesinteger
metadatamap of string
namestringRequired

Internal promotion name for dashboards, searching, and reporting. This is not intended for buyer-facing checkout or receipt copy.

redemption_typeenum
  • automatic
  • code
scheduleobject
stacking_modeenum
  • continue
  • stop_after

Response · 201

dataobjectRequired
metaobject
request_idstring
curl -X POST https://api.withflintpay.com/v1/promotions \
  -H "Flint-Version: 2026-09-07" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: a-unique-key" \
  -d '{
    "application_method": {
      "allocation": "across",
      "calculation_basis": "subtotal_pre_tax",
      "discounted_item_rules": [
        {
          "attribute": "line_item.categories",
          "operator": "eq",
          "values": [
            "tickets"
          ]
        }
      ],
      "percent_off": 20,
      "type": "percent_off"
    },
    "codes": [
      {
        "code": "SPRING20",
        "max_uses": 500
      }
    ],
    "combines_with": {
      "line_item": false,
      "order": true,
      "service_charge": true
    },
    "discount_class": "line_item",
    "display_name": "20% off spring tickets",
    "max_uses": 500,
    "metadata": {
      "campaign": "spring_launch"
    },
    "name": "Spring ticket launch Q2",
    "redemption_type": "code",
    "schedule": {
      "ends_at": "2026-04-01T00:00:00Z"
    },
    "stacking_mode": "continue"
  }'
curl https://api.withflintpay.com/v1/promotions/promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J \
  -H "Flint-Version: 2026-09-07" \
  -H "Authorization: Bearer YOUR_API_KEY"
JSON
{
  "data": {
    "application_method": {
      "allocation": "across",
      "calculation_basis": "subtotal_pre_tax",
      "discounted_item_rules": [
        {
          "attribute": "line_item.categories",
          "operator": "eq",
          "values": [
            "tickets"
          ]
        }
      ],
      "percent_off": 20,
      "type": "percent_off"
    },
    "codes_summary": {
      "active_count": 2,
      "newest_active_code": "SPRING20",
      "total_count": 3
    },
    "combines_with": {
      "line_item": false,
      "order": true,
      "service_charge": true
    },
    "created_at": "2026-03-17T14:30:00Z",
    "discount_class": "line_item",
    "display_name": "20% off spring tickets",
    "max_uses": 500,
    "merchant_id": "mer_123",
    "metadata": {
      "campaign": "spring_launch"
    },
    "name": "Spring ticket launch Q2",
    "promotion_id": "promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J",
    "redemption_type": "code",
    "schedule": {
      "ends_at": "2026-04-01T00:00:00Z"
    },
    "stacking_mode": "continue",
    "status": "active",
    "updated_at": "2026-03-17T14:30:00Z",
    "uses_count": 24
  },
  "request_id": "bce56cba-0827-44aa-bb56-4f200ba15ee6"
}

Update promotion#

PATCH/v1/promotions/{promotion_id}Idempotent

Requires scope commerce.promotions.write

Applies a sparse update to promotion fields.

Path parameters

promotion_idstringRequired

Flint promotion ID.

Request body

application_methodone of

Canonical REST shape is flat: send type plus the effect fields on this object. Responses always use this flat shape.

combines_withobject
descriptionstring
discount_classenum

Promotion discount class.

  • order
  • line_item
  • service_charge
display_namestring

Buyer-facing promotion name shown in checkout and order discount surfaces.

eligibility_rulesone of

A rule group is exactly one of three forms, never a blend: an array of rules for a simple list (an implicit AND), an object with only all for nested AND, or an object with only any for OR. The all and any arrays contain rules or nested rule groups. There is no rules key. Sending a rules key, an unexpected key, or both all and any is rejected with INVALID_RULE_GROUP.

exclusivityobject
external_reference_idstring

Caller-owned identifier for this resource in an external system.

max_usesinteger
metadatamap of string or null

Caller-owned metadata. Omit this field to leave metadata unchanged. Send an object to merge by key, set a key to null to remove it, or set metadata to null to clear all metadata. An empty object makes no change. Empty strings are stored. Keys starting with flint_ are reserved and cannot be written through the public API.

namestring

Internal promotion name for dashboards, searching, and reporting.

scheduleobject
stacking_modeenum
  • continue
  • stop_after
statusenum
  • active
  • inactive

Response · 200

Same response as Create promotion.

curl -X PATCH https://api.withflintpay.com/v1/promotions/promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J \
  -H "Flint-Version: 2026-09-07" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: a-unique-key" \
  -d '{
    "description": "20% off selected spring tickets",
    "status": "inactive"
  }'
curl -X DELETE https://api.withflintpay.com/v1/promotions/promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J \
  -H "Flint-Version: 2026-09-07" \
  -H "Authorization: Bearer YOUR_API_KEY"

Create promotion code#

POST/v1/promotions/{promotion_id}/codesIdempotent

Requires scope commerce.promotions.write

Creates a code for a code-gated promotion.

Path parameters

promotion_idstringRequired

Flint promotion ID.

Request body

codestringRequired
expires_atstring

RFC3339 timestamp.

max_usesinteger
metadatamap of string
timezonestring

Response · 201

dataobjectRequired
metaobject
request_idstring
curl -X POST https://api.withflintpay.com/v1/promotions/promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J/codes \
  -H "Flint-Version: 2026-09-07" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: a-unique-key" \
  -d '{
    "code": "VIP20",
    "expires_at": "2026-04-01T00:00:00Z",
    "max_uses": 100,
    "metadata": {
      "audience": "vip"
    }
  }'

Update promotion code#

PATCH/v1/promotions/{promotion_id}/codes/{promotion_code_id}Idempotent

Requires scope commerce.promotions.write

Applies a sparse update to a promotion code.

Path parameters

promotion_idstringRequired

Flint promotion ID.

promotion_code_idstringRequired

Flint promotion code ID.

Request body

expires_atstring

RFC3339 timestamp.

max_usesinteger
metadatamap of string or null

Caller-owned metadata. Omit this field to leave metadata unchanged. Send an object to merge by key, set a key to null to remove it, or set metadata to null to clear all metadata. An empty object makes no change. Empty strings are stored. Keys starting with flint_ are reserved and cannot be written through the public API.

statusenum

Stored code switch. Only active and inactive can be set; expired and exhausted are computed on reads.

  • active
  • inactive
timezonestring

Response · 200

Same response as Create promotion code.

curl -X PATCH https://api.withflintpay.com/v1/promotions/promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J/codes/pcode_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J \
  -H "Flint-Version: 2026-09-07" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: a-unique-key" \
  -d '{
    "max_uses": 50,
    "status": "inactive"
  }'
curl -X DELETE https://api.withflintpay.com/v1/promotions/promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J/codes/pcode_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J \
  -H "Flint-Version: 2026-09-07" \
  -H "Authorization: Bearer YOUR_API_KEY"

Was this helpful?