lomi.
Payments

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:

  1. Create coupon with constraints (create_discount_coupon).
  2. Validate at checkout (validate_coupon_for_checkout / validate_coupon_for_frontend).
  3. Compute discount (calculate_coupon_discount) and apply (apply_coupon), or apply multiple sequentially (calculate_multi_coupon_discount, apply_coupons_to_checkout).
  4. Reserve usage against checkout session in coupon_usage.
  5. Link reservation to final transaction (link_or_insert_coupon_usage_for_transaction).
  6. 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

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_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:

  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_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

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