lomi.

Discount coupons

Create, validate, stack, and track coupon discounts with the same rules used in checkout.

The Discount coupons API supports strict server-side validation and usage tracking for checkout flows.

Worked examples (stacking, eligibility edge cases, retries): Coupon logic examples.

This page describes coupon validation and application rules used by checkout and the API, including:

  • creation-time validation
  • checkout-time validation (customer, scope, quantity, frequency)
  • single and multi-coupon application
  • deduped coupon usage reservation and transaction linking

How coupon logic works (end-to-end)

At a high level, coupon handling follows this flow:

  1. Create a coupon via POST /discount-coupons with your constraints.
  2. Validate at checkout (server-side rules for customer, scope, quantity, and frequency).
  3. Compute and apply the discount on the checkout session, including stacked coupons when allowed.
  4. Reserve usage against the checkout session until payment completes.
  5. Link the reservation to the final transaction when payment succeeds.
  6. Increment usage counters when the transaction reaches completed.

This keeps coupon behavior deterministic between dashboard preview, hosted checkout, and API integrations.

Create a discount coupon

Request Body

FieldTypeRequiredDescription
codestringYesUnique coupon code (auto-uppercased)
discount_typestringNopercentage or fixed (default: percentage)
discount_percentagenumberIf percentageDiscount percentage (> 0 and <= 100)
discount_fixed_amountnumberIf fixedFixed discount amount
descriptionstringNoCoupon description
is_activebooleanNoActive status (default: true)
max_usesnumberNoMaximum total uses
max_quantity_per_usenumberNoMax quantity per use
valid_fromstringNoStart date (ISO 8601)
expires_atstringNoExpiration date (ISO 8601)
customer_typestringNoall, new, returning
usage_frequency_limitstringNototal, per_customer, per_day, per_week, per_month
usage_limit_valuenumberConditionallyRequired if usage_frequency_limit != total
scope_typestringNoorganization_wide, specific_products, specific_prices
product_idsarrayNoProduct IDs (if scope is specific)

Creation-time validation rules

The API enforces these rules at creation:

  • coupon code is unique per organization
  • percentage and fixed discounts are mutually exclusive
  • valid_from < expires_at when both exist
  • usage_limit_value is required for non-total frequency modes
  • for specific_products / specific_prices, linked products must belong to the same organization
import { LomiSDK } from '@lomi./sdk';

const lomi = new LomiSDK({
  apiKey: process.env.LOMI_SECRET_KEY!,
  environment: 'live',
});

// Percentage discount
const coupon = await lomi.discountCoupons.create({
  code: 'SAVE20',
  discount_type: 'percentage',
  discount_percentage: 20,
  description: '20% off all products',
  max_uses: 100,
  expires_at: '2024-12-31T23:59:59Z',
});

// Fixed amount discount
const fixedCoupon = await lomi.discountCoupons.create({
  code: 'FLAT5000',
  discount_type: 'fixed',
  discount_fixed_amount: 5000,
  description: '5000 XOF off',
  customer_type: 'new',
});

console.log(`Coupon created: ${coupon.code}`);
from lomi import LomiClient
import os

client = LomiClient(
    api_key=os.environ["LOMI_SECRET_KEY"],
    environment="test"
)

coupon = client.discount_coupons.create({
    "code": "SAVE20",
    "discount_type": "percentage",
    "discount_percentage": 20,
    "description": "20% off all products",
    "max_uses": 100,
    "expires_at": "2024-12-31T23:59:59Z"
})

print(f"Coupon created: {coupon['code']}")
curl -X POST "https://api.lomi.africa/discount-coupons" \
  -H "X-API-KEY: $LOMI_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "SAVE20",
    "discount_type": "percentage",
    "discount_percentage": 20,
    "description": "20% off all products",
    "max_uses": 100,
    "expires_at": "2024-12-31T23:59:59Z"
  }'

Checkout validation behavior

Checkout validation combines structural and business checks. A coupon must pass all checks below:

  1. Exists and active in the same organization.
  2. Time window: valid_from has started and expires_at has not passed.
  3. Global cap: current_uses < max_uses (if max_uses set).
  4. Frequency cap for the customer (when enabled): per customer/day/week/month.
  5. Quantity cap: checkout quantity must not exceed max_quantity_per_use.
  6. Customer-type eligibility:
    • new: customer must have no completed payment/installment transactions in org
    • returning: customer must have at least one completed payment/installment transaction
    • all: no customer-history restriction
  7. Scope check:
    • organization_wide: valid for all products
    • specific_products / specific_prices: product must be linked in coupon_product_links

If any check fails, the API returns a specific reason message.

Discount calculation behavior

calculate_coupon_discount computes discount on the base amount excluding optional fees:

  • base_price = p_base_amount - p_fees_amount
  • percentage coupon: discount = base_price * percentage
  • fixed coupon: discount = fixed_amount
  • discount is multiplied by quantity when applicable
  • discount is clamped so it cannot exceed the eligible base price
  • final amount is recomputed as (base_price - discount) + fees

This protects against negative totals and over-discounting.

Multi-coupon stacking (sequential)

For multi-coupon flows, coupons are applied in order using a running amount:

  • coupon A applies to original current amount
  • coupon B applies to the reduced amount after A
  • and so on

This is sequential stacking, not parallel summing.
The response includes a breakdown per coupon with:

  • original amount before this coupon
  • discount amount for this coupon
  • final amount after this coupon

If any coupon in the array fails validation, the whole multi-coupon calculation fails.

Usage tracking and deduplication

Coupon usage is recorded in coupon_usage with safeguards to prevent duplicate pending reservations:

  • a pending reservation is unique per (coupon_id, checkout_session_id) while transaction_id IS NULL
  • stale duplicates are cleaned before enforcing the partial unique index
  • when payment completes, reservation is linked to transaction (instead of inserting duplicates)
  • if no reservation exists, a new transaction-linked record is inserted

This is the expected behavior for provider retries and webhook races.

List discount coupons

Retrieve all discount coupons for your organization.

const coupons = await lomi.discountCoupons.list();
coupons = client.discount_coupons.list()
curl -X GET "https://api.lomi.africa/discount-coupons" \
  -H "X-API-KEY: $LOMI_SECRET_KEY"

Get a discount coupon

Retrieve details of a specific coupon.

const coupon = await lomi.discountCoupons.get('dc_abc123...');
coupon = client.discount_coupons.get('dc_abc123...')
curl -X GET "https://api.lomi.africa/discount-coupons/dc_abc123..." \
  -H "X-API-KEY: $LOMI_SECRET_KEY"

Get coupon performance

Retrieve usage statistics and revenue impact for a coupon (completed transactions only).

const performance = await lomi.discountCoupons.getPerformance('dc_abc123...');

console.log(`Total uses: ${performance.total_uses}`);
console.log(`Total discounted: ${performance.total_discount_amount}`);
console.log(`Revenue generated: ${performance.total_revenue}`);
console.log(`Avg order value: ${performance.average_order_value}`);
performance = client.discount_coupons.get_performance('dc_abc123...')
print(f"Total uses: {performance['total_uses']}")
curl -X GET "https://api.lomi.africa/discount-coupons/dc_abc123.../performance" \
  -H "X-API-KEY: $LOMI_SECRET_KEY"

Response

{
  "total_uses": 45,
  "total_discounts": 25000,
  "total_revenue": 150000,
  "average_discount": 555.56,
  "unique_customers": 38
}

Discount Coupon Object

FieldTypeDescription
idstringUnique identifier
codestringCoupon code
discount_typestringpercentage or fixed
discount_percentagenumberPercentage value
discount_fixed_amountnumberFixed amount value
customer_typestringall, new, returning
usage_frequency_limitstringtotal, per_customer, per_day, per_week, per_month
usage_limit_valuenumberFrequency limit count where applicable
is_activebooleanActive status
max_usesnumberMaximum uses
current_usesnumberCurrent use count
max_quantity_per_usenumberMax quantity allowed per redemption
valid_fromstringStart date
expires_atstringExpiration date
scope_typestringApplication scope
product_linksarrayLinked products where scope is specific
completed_redemptionsnumberCompleted redemptions count
distinct_customers_completednumberDistinct customers with completed redemptions
created_atstringCreation timestamp

Common implementation patterns

Validate before applying

For a clean UX:

  1. validate coupon first (frontend validation endpoint),
  2. show discount preview,
  3. apply coupon during checkout creation/confirmation.

Handle retries safely

Payment providers may retry callbacks. The coupon reservation/linking logic is designed to be idempotent around checkout session + coupon pairs.

Prefer customer context when possible

For new and returning restricted coupons, send customer_id so eligibility can be evaluated correctly.


Error Responses

StatusDescription
400Invalid input or duplicate code
401Invalid or missing API key
404Coupon not found

On this page