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 documents the behavior implemented in the discount logic migrations, 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:
- Create coupon with constraints (
create_discount_coupon). - Validate at checkout (
validate_coupon_for_checkout/validate_coupon_for_frontend). - Compute discount (
calculate_coupon_discount) and apply (apply_coupon), or apply multiple sequentially (calculate_multi_coupon_discount,apply_coupons_to_checkout). - Reserve usage against checkout session in
coupon_usage. - Link reservation to final transaction (
link_or_insert_coupon_usage_for_transaction). - Increment coupon usage counters when transaction is completed (
increment_coupon_usage_for_completed_transaction).
This keeps coupon behavior deterministic between frontend preview and final transaction recording.
Create a discount coupon
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
code | string | Yes | Unique coupon code (auto-uppercased) |
discount_type | string | No | percentage or fixed (default: percentage) |
discount_percentage | number | If percentage | Discount percentage (> 0 and <= 100) |
discount_fixed_amount | number | If fixed | Fixed discount amount |
description | string | No | Coupon description |
is_active | boolean | No | Active status (default: true) |
max_uses | number | No | Maximum total uses |
max_quantity_per_use | number | No | Max quantity per use |
valid_from | string | No | Start date (ISO 8601) |
expires_at | string | No | Expiration date (ISO 8601) |
customer_type | string | No | all, new, returning |
usage_frequency_limit | string | No | total, per_customer, per_day, per_week, per_month |
usage_limit_value | number | Conditionally | Required if usage_frequency_limit != total |
scope_type | string | No | organization_wide, specific_products, specific_prices |
product_ids | array | No | Product 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_atwhen both existusage_limit_valueis required for non-totalfrequency 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_API_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_API_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_API_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:
- Exists and active in the same organization.
- Time window:
valid_fromhas started andexpires_athas not passed. - Global cap:
current_uses < max_uses(ifmax_usesset). - Frequency cap for the customer (when enabled): per customer/day/week/month.
- Quantity cap: checkout quantity must not exceed
max_quantity_per_use. - Customer-type eligibility:
new: customer must have no completed payment/installment transactions in orgreturning: customer must have at least one completed payment/installment transactionall: no customer-history restriction
- Scope check:
organization_wide: valid for all productsspecific_products/specific_prices: product must be linked incoupon_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)whiletransaction_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_API_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_API_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_API_KEY"Response
{
"total_uses": 45,
"total_discounts": 25000,
"total_revenue": 150000,
"average_discount": 555.56,
"unique_customers": 38
}Discount Coupon Object
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier |
code | string | Coupon code |
discount_type | string | percentage or fixed |
discount_percentage | number | Percentage value |
discount_fixed_amount | number | Fixed amount value |
customer_type | string | all, new, returning |
usage_frequency_limit | string | total, per_customer, per_day, per_week, per_month |
usage_limit_value | number | Frequency limit count where applicable |
is_active | boolean | Active status |
max_uses | number | Maximum uses |
current_uses | number | Current use count |
max_quantity_per_use | number | Max quantity allowed per redemption |
valid_from | string | Start date |
expires_at | string | Expiration date |
scope_type | string | Application scope |
product_links | array | Linked products where scope is specific |
completed_redemptions | number | Completed redemptions count |
distinct_customers_completed | number | Distinct customers with completed redemptions |
created_at | string | Creation timestamp |
Common implementation patterns
Validate before applying
For a clean UX:
- validate coupon first (frontend validation endpoint),
- show discount preview,
- 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
| Status | Description |
|---|---|
400 | Invalid input or duplicate code |
401 | Invalid or missing API key |
404 | Coupon not found |