Discount codes

A discount code is a string a customer enters at checkout to reduce their total: WELCOME10, BLACKFRIDAY, FREESHIP. The Discounts page is where you create them, edit them, watch redemptions land, and turn them off when a campaign ends.
You don't have to use codes for every promotion — sitewide markdowns are often better handled storefront-side. Use a code when you want a targeted, trackable lever: "share this code with newsletter subscribers", "first-order coupon for users coming from a paid ad", "winback offer in an abandoned-cart email."
Rule of thumb. If you want to know which campaign drove a sale, give the campaign a code. The redemption ledger gives you that attribution for free.
What a discount code holds
Every code has the same shape, whether you create it from the portal or the API:
| Field | What it's for |
|---|---|
id |
Ripllo-assigned, looks like disc_01H…. Stable forever. |
code |
The string the customer types. Case-insensitive within a workspace. |
description |
Internal-only note for your team. Customers never see it. |
type |
percent or fixed. |
value |
The amount: 10 for 10%, or 25000 for IDR 25,000 off. |
currency |
Currency for fixed-value codes (and for minPurchaseAmount). |
scope |
all, products, or tag. Controls which cart lines the code applies to. |
productIds |
When scope = products, the list of product IDs that qualify. |
tagFilter |
When scope = tag, the product tags that qualify. |
minPurchaseAmount |
Smallest order subtotal the code accepts. |
maxUsesTotal |
Global cap across all customers. |
maxUsesPerCustomer |
Cap per customer. |
startsAt / expiresAt |
When the code is valid. Either or both optional. |
active |
Toggle without deleting. |
public |
If true, the code surfaces in the public storefront teaser endpoint. |
The list view
From the dashboard sidebar, click Discounts. You land at /dashboard/discounts.
The page shows every code in the workspace sorted by createdAt desc. Filters at the top let you narrow to active only or expiring soon.
Columns
- Code — the string + short status pill (
active,paused,expired). - Type —
10% offorIDR 25,000 off. - Uses — current / cap. Empty cell if unlimited.
- Created — short date stamp.
Click any row to open the detail panel.
Creating a code
Click + New discount. The form covers the fields above. A few notes:
- The code field is case-insensitive on save and at validation time. Storing
welcome10vsWELCOME10matters for the URL slug but not for the customer. percentcodes don't need a currency for the value itself, only forminPurchaseAmount. The form hides the irrelevant fields based ontype.- Scoped codes (
productsortag) only discount the matching lines — not the whole cart. A 10% off "footwear" code on a cart with a shirt and a pair of shoes only discounts the shoes.
When you click Create, Ripllo POSTs to /api/v1/discount-codes and the new code appears in the list.
Editing a code
Open a code's detail panel and click Edit. You can change every field except code and type — those are immutable once redemptions exist. To change either, archive the existing code and create a new one.
Why immutable? Redemption rows reference the code at the time of redemption. If you renamed
WELCOME10toWELCOME15later, historical analytics would lie about what the customer actually used.
Validating before checkout
The storefront can call validate to preview whether a code applies, without consuming a use:
POST /api/v1/discount-codes/validate
{
"code": "WELCOME10",
"customerId": "cust_...",
"subtotal": 250000,
"currency": "IDR",
"lineItems": [...]
}
The response returns applies: true | false, the computed discountAmount, and a reason if it didn't apply (expired, min_purchase_not_met, out_of_uses, scope_mismatch, etc.). This is what powers the "code accepted" UX in your cart before the customer hits Pay.
Redeeming at payment-success
Once the order is paid, your payment handler calls redeem to consume one use:
POST /api/v1/discount-codes/redeem
{
"code": "WELCOME10",
"customerId": "cust_...",
"orderId": "order_...", // becomes the redemption's externalRef
"subtotal": 250000,
"currency": "IDR"
}
Redemptions are idempotent on orderId — if your webhook retries, the same redemption row is returned with created: false. No double-counting.
If you're integrating through Storlaunch, this is wired up for you — Storlaunch's payment-success handler calls ripllo.discountCodes.redeem automatically.
Public applicable-codes endpoint
The storefront can fetch a list of codes the current customer could apply, for a "you might also like" teaser:
GET /api/v1/discount-codes/applicable/:accountId?subtotal=250000¤cy=IDR
Only codes marked public: true show up here. Codes with public: false are still valid when the customer enters them manually, but they don't get surfaced.
Archiving a code
The trash icon on a code's row sends DELETE /api/v1/discount-codes/:id. This sets active: false and stamps archivedAt — the code is no longer redeemable but stays in the database for historical reporting. There is no hard-delete.
What you can't do (yet)
- Bulk import — one code at a time via UI or API. Bulk endpoint is on the backlog.
- Stacking codes — one code per order; the validator rejects a second one.
- Per-region codes — no geo-fence today. Workaround: use
tagscope on region-tagged products.
Next
- Referrals — the dual-sided counterpart to discounts.
- Marketing — combine discounts with abandoned-cart reminders.
- API reference — the full endpoint set.