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 nested child resources under it, so one promotion can have many independently expiring codes or no codes at all.

Use Coupons when you need the simple case: one reusable code, one percent or amount discount, and a flat product, variant, bundle, or category restriction list. Coupons are a facade over this same engine. Use Promotions when you need automatic application, multiple codes, buy-X-get-Y, customer or metadata conditions, service-charge discounts, or precise stacking behavior.

Because coupons and promotions share one engine and one ID space, the coupons endpoints only surface promotions that were created through the coupons API. A promotion created through /v1/promotions (and any buy_x_get_y promotion) is not readable at GET /v1/coupons/{id}; read it at GET /v1/promotions/{id}.

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.category", "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.categorystringeq, ne, in, contains, is_definedMatches any of the line item's catalog categories.
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.

Automatic promotion#

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

JSON
{
  "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.category", "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; 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#

Both coupons and promotions apply through the same order endpoint, POST /v1/orders/{order_id}/discounts, using a discount body with exactly one of coupon, promotion, or manual:

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

Use coupon (by coupon_id or coupon_code) for the simple flattened case, and promotion (by promotion_id or promotion_code) for the rule-engine case. 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 coupon or promotion discounts (the 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's status is disabled.
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, disabled, expired, not_yet_started, exhausted, no_active_codes, or deleted. no_active_codes means a code-gated promotion has no usable codes; exhausted means max_uses is reached.

PATCH /v1/promotions/{id} accepts only active or disabled 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: deleted (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.

List promotions#

GET/v1/promotionsRequires 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.

statusenum

Filter by promotion status.

activedisabledexpirednot_yet_startedexhaustedno_active_codesdeleted
querystring

Search across promotion ID, name, display name, description, and promotion codes.

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.

categorystring

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.

automaticcode
discount_classenum

Filter by promotion discount class.

orderline_itemservice_charge
sort_byenum

Sort field.

namecreated_atupdated_at
sort_directionenum

Sort direction.

ascdesc
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

Error codes

AUTH_REQUIREDINSUFFICIENT_SCOPEINVALID_API_KEYINVALID_REQUESTRATE_LIMIT_EXCEEDED
Bash
curl https://api.withflintpay.com/v1/promotions \
  -H "Authorization: Bearer YOUR_API_KEY"
JSON
{
  "data": [
    {
      "promotion_id": "promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J",
      "name": "Spring ticket launch Q2",
      "display_name": "20% off spring tickets",
      "status": "active",
      "redemption_type": "code",
      "application_method": {
        "type": "percent_off",
        "allocation": "across",
        "calculation_basis": "subtotal_pre_tax",
        "percent_off": 20,
        "discounted_item_rules": [
          {
            "attribute": "line_item.category",
            "operator": "eq",
            "values": [
              "tickets"
            ]
          }
        ]
      },
      "discount_class": "line_item",
      "combines_with": {
        "order": true,
        "line_item": false,
        "service_charge": true
      },
      "stacking_mode": "continue",
      "metadata": {
        "campaign": "spring_launch"
      },
      "merchant_id": "mer_123",
      "max_uses": 500,
      "uses_count": 24,
      "schedule": {
        "ends_at": "2026-04-01T00:00:00Z"
      },
      "is_coupon_managed": false,
      "created_at": "2026-03-17T14:30:00Z",
      "updated_at": "2026-03-17T14:30:00Z"
    }
  ],
  "next_page_token": "Zm9yd2FyZC1vbmx5LW9wYXF1ZS1jdXJzb3I",
  "request_id": "bce56cba-0827-44aa-bb56-4f200ba15ee6"
}

Create promotion#

POST/v1/promotionsIdempotentRequires 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.

orderline_itemservice_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
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
automaticcode
scheduleobject
stacking_modeenum
continuestop_after
Response · 201
dataobjectrequired
metaobject
request_idstring

Error codes

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

Get promotion#

GET/v1/promotions/{promotion_id}Requires scope: commerce.promotions.read or commerce.promotions.write

Returns a single promotion by ID.

Path parameters
promotion_idstringrequired

Flint promotion ID.

Response · 200
dataobjectrequired
metaobject
request_idstring

Error codes

AUTH_REQUIREDINSUFFICIENT_SCOPEINVALID_API_KEYINVALID_REQUESTRATE_LIMIT_EXCEEDEDRESOURCE_NOT_FOUND
Bash
curl https://api.withflintpay.com/v1/promotions/promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J \
  -H "Authorization: Bearer YOUR_API_KEY"
JSON
{
  "data": {
    "promotion_id": "promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J",
    "name": "Spring ticket launch Q2",
    "display_name": "20% off spring tickets",
    "status": "active",
    "redemption_type": "code",
    "application_method": {
      "type": "percent_off",
      "allocation": "across",
      "calculation_basis": "subtotal_pre_tax",
      "percent_off": 20,
      "discounted_item_rules": [
        {
          "attribute": "line_item.category",
          "operator": "eq",
          "values": [
            "tickets"
          ]
        }
      ]
    },
    "discount_class": "line_item",
    "combines_with": {
      "order": true,
      "line_item": false,
      "service_charge": true
    },
    "stacking_mode": "continue",
    "metadata": {
      "campaign": "spring_launch"
    },
    "merchant_id": "mer_123",
    "max_uses": 500,
    "uses_count": 24,
    "schedule": {
      "ends_at": "2026-04-01T00:00:00Z"
    },
    "is_coupon_managed": false,
    "created_at": "2026-03-17T14:30:00Z",
    "updated_at": "2026-03-17T14:30:00Z"
  },
  "request_id": "bce56cba-0827-44aa-bb56-4f200ba15ee6"
}

Update promotion#

PATCH/v1/promotions/{promotion_id}IdempotentRequires 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. Coupon-backed promotions cannot change discount_class.

orderline_itemservice_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
max_usesinteger
metadatamap of string
namestring

Internal promotion name for dashboards, searching, and reporting.

scheduleobject
stacking_modeenum
continuestop_after
statusenum
activedisabled
Response · 200
dataobjectrequired
metaobject
request_idstring

Error codes

AUTH_REQUIREDINSUFFICIENT_SCOPEINVALID_API_KEYINVALID_REQUESTRATE_LIMIT_EXCEEDEDRESOURCE_NOT_FOUND
Bash
curl -X PATCH https://api.withflintpay.com/v1/promotions/promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J \
  -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": "disabled"
  }'
JSON
{
  "data": {
    "promotion_id": "promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J",
    "name": "Spring ticket launch Q2",
    "display_name": "20% off spring tickets",
    "status": "active",
    "redemption_type": "code",
    "application_method": {
      "type": "percent_off",
      "allocation": "across",
      "calculation_basis": "subtotal_pre_tax",
      "percent_off": 20,
      "discounted_item_rules": [
        {
          "attribute": "line_item.category",
          "operator": "eq",
          "values": [
            "tickets"
          ]
        }
      ]
    },
    "discount_class": "line_item",
    "combines_with": {
      "order": true,
      "line_item": false,
      "service_charge": true
    },
    "stacking_mode": "continue",
    "metadata": {
      "campaign": "spring_launch"
    },
    "merchant_id": "mer_123",
    "max_uses": 500,
    "uses_count": 24,
    "schedule": {
      "ends_at": "2026-04-01T00:00:00Z"
    },
    "is_coupon_managed": false,
    "created_at": "2026-03-17T14:30:00Z",
    "updated_at": "2026-03-17T14:30:00Z"
  },
  "request_id": "bce56cba-0827-44aa-bb56-4f200ba15ee6"
}

Delete promotion#

DELETE/v1/promotions/{promotion_id}IdempotentRequires scope: commerce.promotions.write

Deletes a promotion.

Path parameters
promotion_idstringrequired

Flint promotion ID.

Response · 200
dataobjectrequired
metaobject
request_idstring

Error codes

AUTH_REQUIREDINSUFFICIENT_SCOPEINVALID_API_KEYINVALID_REQUESTRATE_LIMIT_EXCEEDEDRESOURCE_NOT_FOUND
Bash
curl -X DELETE https://api.withflintpay.com/v1/promotions/promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J \
  -H "Authorization: Bearer YOUR_API_KEY"
JSON
{
  "data": {
    "promotion_id": "promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J",
    "name": "Spring ticket launch Q2",
    "display_name": "20% off spring tickets",
    "status": "active",
    "redemption_type": "code",
    "application_method": {
      "type": "percent_off",
      "allocation": "across",
      "calculation_basis": "subtotal_pre_tax",
      "percent_off": 20,
      "discounted_item_rules": [
        {
          "attribute": "line_item.category",
          "operator": "eq",
          "values": [
            "tickets"
          ]
        }
      ]
    },
    "discount_class": "line_item",
    "combines_with": {
      "order": true,
      "line_item": false,
      "service_charge": true
    },
    "stacking_mode": "continue",
    "metadata": {
      "campaign": "spring_launch"
    },
    "merchant_id": "mer_123",
    "max_uses": 500,
    "uses_count": 24,
    "schedule": {
      "ends_at": "2026-04-01T00:00:00Z"
    },
    "is_coupon_managed": false,
    "created_at": "2026-03-17T14:30:00Z",
    "updated_at": "2026-03-17T14:30:00Z"
  },
  "request_id": "bce56cba-0827-44aa-bb56-4f200ba15ee6"
}

List promotion codes#

GET/v1/promotions/{promotion_id}/codesRequires scope: commerce.promotions.read or commerce.promotions.write

Returns a paginated list of codes for a promotion.

Path parameters
promotion_idstringrequired

Flint promotion ID.

Query parameters
page_sizeinteger

Page size, default 20, max 100.

page_tokenstring

Cursor returned by the previous list response.

Response · 200
dataarray of objectrequired
metaobject
next_page_tokenstring
request_idstring

Error codes

AUTH_REQUIREDINSUFFICIENT_SCOPEINVALID_API_KEYINVALID_REQUESTRATE_LIMIT_EXCEEDEDRESOURCE_NOT_FOUND
Bash
curl https://api.withflintpay.com/v1/promotions/promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J/codes \
  -H "Authorization: Bearer YOUR_API_KEY"
JSON
{
  "data": [
    {
      "promotion_code_id": "pcode_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J",
      "promotion_id": "promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J",
      "code": "SPRING20",
      "status": "active",
      "metadata": {
        "campaign": "spring_launch"
      },
      "merchant_id": "mer_123",
      "max_uses": 500,
      "uses_count": 24,
      "expires_at": "2026-04-01T00:00:00Z",
      "created_at": "2026-03-17T14:30:00Z",
      "updated_at": "2026-03-17T14:30:00Z"
    }
  ],
  "next_page_token": "Zm9yd2FyZC1vbmx5LW9wYXF1ZS1jdXJzb3I",
  "request_id": "bce56cba-0827-44aa-bb56-4f200ba15ee6"
}

Create promotion code#

POST/v1/promotions/{promotion_id}/codesIdempotentRequires 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
Response · 201
dataobjectrequired
metaobject
request_idstring

Error codes

AUTH_REQUIREDINSUFFICIENT_SCOPEINVALID_API_KEYINVALID_REQUESTRATE_LIMIT_EXCEEDEDRESOURCE_NOT_FOUND
Bash
curl -X POST https://api.withflintpay.com/v1/promotions/promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J/codes \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: a-unique-key" \
  -d '{
    "code": "VIP20",
    "max_uses": 100,
    "expires_at": "2026-04-01T00:00:00Z",
    "metadata": {
      "audience": "vip"
    }
  }'
JSON
{
  "data": {
    "promotion_code_id": "pcode_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J",
    "promotion_id": "promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J",
    "code": "SPRING20",
    "status": "active",
    "metadata": {
      "campaign": "spring_launch"
    },
    "merchant_id": "mer_123",
    "max_uses": 500,
    "uses_count": 24,
    "expires_at": "2026-04-01T00:00:00Z",
    "created_at": "2026-03-17T14:30:00Z",
    "updated_at": "2026-03-17T14:30:00Z"
  },
  "request_id": "bce56cba-0827-44aa-bb56-4f200ba15ee6"
}

Update promotion code#

PATCH/v1/promotions/{promotion_id}/codes/{promotion_code_id}IdempotentRequires 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
statusenum

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

activedisabled
Response · 200
dataobjectrequired
metaobject
request_idstring

Error codes

AUTH_REQUIREDINSUFFICIENT_SCOPEINVALID_API_KEYINVALID_REQUESTRATE_LIMIT_EXCEEDEDRESOURCE_NOT_FOUND
Bash
curl -X PATCH https://api.withflintpay.com/v1/promotions/promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J/codes/pcode_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: a-unique-key" \
  -d '{
    "status": "disabled",
    "max_uses": 50
  }'
JSON
{
  "data": {
    "promotion_code_id": "pcode_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J",
    "promotion_id": "promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J",
    "code": "SPRING20",
    "status": "active",
    "metadata": {
      "campaign": "spring_launch"
    },
    "merchant_id": "mer_123",
    "max_uses": 500,
    "uses_count": 24,
    "expires_at": "2026-04-01T00:00:00Z",
    "created_at": "2026-03-17T14:30:00Z",
    "updated_at": "2026-03-17T14:30:00Z"
  },
  "request_id": "bce56cba-0827-44aa-bb56-4f200ba15ee6"
}

Delete promotion code#

DELETE/v1/promotions/{promotion_id}/codes/{promotion_code_id}IdempotentRequires scope: commerce.promotions.write

Deletes a promotion code.

Path parameters
promotion_idstringrequired

Flint promotion ID.

promotion_code_idstringrequired

Flint promotion code ID.

Response · 200
dataobjectrequired
metaobject
request_idstring

Error codes

AUTH_REQUIREDINSUFFICIENT_SCOPEINVALID_API_KEYINVALID_REQUESTRATE_LIMIT_EXCEEDEDRESOURCE_NOT_FOUND
Bash
curl -X DELETE https://api.withflintpay.com/v1/promotions/promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J/codes/pcode_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J \
  -H "Authorization: Bearer YOUR_API_KEY"
JSON
{
  "data": {
    "promotion_code_id": "pcode_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J",
    "promotion_id": "promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J",
    "code": "SPRING20",
    "status": "active",
    "metadata": {
      "campaign": "spring_launch"
    },
    "merchant_id": "mer_123",
    "max_uses": 500,
    "uses_count": 24,
    "expires_at": "2026-04-01T00:00:00Z",
    "created_at": "2026-03-17T14:30:00Z",
    "updated_at": "2026-03-17T14:30:00Z"
  },
  "request_id": "bce56cba-0827-44aa-bb56-4f200ba15ee6"
}

Resolve promotion code#

GET/v1/promotions/by-code/{code}Requires scope: commerce.promotions.read or commerce.promotions.write

Resolves a buyer-entered promotion code to its promotion code record and parent promotion. This does not evaluate the code against an order or redeem it.

Path parameters
codestringrequired

Promotion code entered by the buyer.

Response · 200
dataobjectrequired
metaobject
request_idstring

Error codes

AUTH_REQUIREDINSUFFICIENT_SCOPEINVALID_API_KEYINVALID_REQUESTRATE_LIMIT_EXCEEDEDRESOURCE_NOT_FOUND
Bash
curl https://api.withflintpay.com/v1/promotions/by-code/{code} \
  -H "Authorization: Bearer YOUR_API_KEY"
JSON
{
  "data": {
    "promotion": {
      "promotion_id": "promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J",
      "name": "Spring ticket launch Q2",
      "display_name": "20% off spring tickets",
      "status": "active",
      "redemption_type": "code",
      "application_method": {
        "type": "percent_off",
        "allocation": "across",
        "calculation_basis": "subtotal_pre_tax",
        "percent_off": 20,
        "discounted_item_rules": [
          {
            "attribute": "line_item.category",
            "operator": "eq",
            "values": [
              "tickets"
            ]
          }
        ]
      },
      "discount_class": "line_item",
      "combines_with": {
        "order": true,
        "line_item": false,
        "service_charge": true
      },
      "stacking_mode": "continue",
      "metadata": {
        "campaign": "spring_launch"
      },
      "merchant_id": "mer_123",
      "max_uses": 500,
      "uses_count": 24,
      "schedule": {
        "ends_at": "2026-04-01T00:00:00Z"
      },
      "is_coupon_managed": false,
      "created_at": "2026-03-17T14:30:00Z",
      "updated_at": "2026-03-17T14:30:00Z"
    },
    "promotion_code": {
      "promotion_code_id": "pcode_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J",
      "promotion_id": "promo_01HZYV7Q9Y0Y4H8Q3H9F6R7T8J",
      "code": "SPRING20",
      "status": "active",
      "metadata": {
        "campaign": "spring_launch"
      },
      "merchant_id": "mer_123",
      "max_uses": 500,
      "uses_count": 24,
      "expires_at": "2026-04-01T00:00:00Z",
      "created_at": "2026-03-17T14:30:00Z",
      "updated_at": "2026-03-17T14:30:00Z"
    }
  },
  "request_id": "bce56cba-0827-44aa-bb56-4f200ba15ee6"
}
Rate this doc