ripllo.discount_code.redeemed.v1
Fires when a discount code is successfully redeemed at the partner platform's checkout. This is the primary "the code was used for real" signal — it carries the redemption row plus enough context to reconcile against the partner's order log.
The event fires exactly once per (accountId, checkoutSessionId) — the redemption endpoint is idempotent, so retried partner-platform webhooks don't double-emit.
When it fires
When POST /api/v1/discount-codes/redeem commits a new DiscountRedemption row. The Storlaunch partner SDK calls this from its payment-success webhook, so in practice the event fires within seconds of money landing.
The event is single-shot per redemption. If the partner platform retries the same (accountId, checkoutSessionId), the endpoint returns the existing row with 200 OK but no new event fires.
Payload
{
"id": "evt_01HX...",
"type": "ripllo.discount_code.redeemed.v1",
"createdAt": "2026-05-13T10:43:22.187Z",
"accountId": "acc_01HX9C2K3M4N5P6Q7R8S9T0V1W",
"data": {
"redemption": {
"id": "dcr_01HX...",
"accountId": "acc_01HX...",
"discountCodeId": "dc_01HX...",
"checkoutSessionId": "cs_storlaunch_01HX...",
"customerId": "cus_01HX...",
"appliedAmount": 25000,
"appliedShipping": 0,
"externalSource": "storlaunch",
"externalRef": "ord_42",
"redeemedAt": "2026-05-13T10:43:22.140Z"
},
"code": {
"id": "dc_01HX...",
"code": "WELCOME10",
"type": "percent",
"value": 1000,
"currency": "IDR",
"scope": "cart",
"usesCount": 87,
"maxUsesTotal": null
}
}
}
Note the payload carries both the redemption row and a snapshot of the code at redemption time. The usesCount reflects the count including this redemption.
Handler examples
// Node
import { verifyWebhook } from '@forjio/ripllo-node';
app.post('/ripllo/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const event = verifyWebhook(req.body, req.headers, process.env.RIPLLO_WEBHOOK_SECRET);
if (event.type === 'ripllo.discount_code.redeemed.v1') {
const { redemption, code } = event.data;
analytics.track('discount_redeemed', {
code: code.code,
orderId: redemption.externalRef,
discountIdr: redemption.appliedAmount,
});
if (code.maxUsesTotal !== null && code.usesCount >= code.maxUsesTotal) {
notifyMerchant(`Code ${code.code} sold out`);
}
}
res.status(200).end();
});
# Python
from ripllo import verify_webhook
event = verify_webhook(raw_body, request.headers, os.environ['RIPLLO_WEBHOOK_SECRET'])
if event['type'] == 'ripllo.discount_code.redeemed.v1':
redemption = event['data']['redemption']
code = event['data']['code']
analytics.track('discount_redeemed',
code=code['code'],
order_id=redemption['externalRef'],
discount_idr=redemption['appliedAmount'])
// Go
import ripllo "github.com/hachimi-cat/saas-ripllo/sdk/go"
event, err := ripllo.VerifyWebhook(rawBody, headers, os.Getenv("RIPLLO_WEBHOOK_SECRET"))
if event.Type == "ripllo.discount_code.redeemed.v1" {
var data struct {
Redemption ripllo.DiscountRedemption `json:"redemption"`
Code ripllo.DiscountCode `json:"code"`
}
_ = json.Unmarshal(event.Data, &data)
analytics.Track("discount_redeemed", data.Code.Code, data.Redemption.ExternalRef, data.Redemption.AppliedAmount)
}
What to do
- Update analytics. Track discount usage by code, channel (
externalSource), and order value. This is the cleanest source of "which campaigns drove revenue". - Alert on sold-out codes. When
code.maxUsesTotal !== null && code.usesCount >= code.maxUsesTotal, the next attempt to validate this code will fail withMAX_USES_REACHED. Notify the merchant if it's a campaign code they didn't expect to exhaust. - Reconcile against partner orders.
redemption.externalRefis the partner's order ID; pair it withcode.codeto match what the buyer typed in. - Update commission accruals. If you pay affiliates or referrers based on discount-coded sales, the redemption is the trigger.
Common pitfalls
- Treating
redeemedAtas money-in-bank. It's the commit time of the redemption row, immediately after Ripllo's idempotency check. Funds settlement is on Plugipay's timeline, not Ripllo's. - Doing work twice on partner retry. Even though Ripllo doesn't double-emit, your own delivery may retry. Dedupe on
event.id. - Reading
code.usesCountas authoritative for "remaining slots". Two concurrent redemptions can both observeusesCount = Nand both commit, so the count can momentarily exceedmaxUsesTotalby one or two. Use it as a "roughly how full is the bucket" signal, not a strict guard. - Assuming
customerIdis set. Anonymous checkouts produce redemption rows withcustomerId: null. Default to "anonymous" in your analytics rather than dropping the row. - Trusting
externalSourcewithout validating. It's whatever the partner platform set. For the canonical Storlaunch integration,"storlaunch"is the convention, but a custom integration can pass anything.
Related events
ripllo.discount_code.created.v1— reserved-not-emitted; the create-side counterpart.ripllo.referral.created.v1— if the redemption was for a referral reward code, you'll see both events for the same order.ripllo.abandoned_cart.recovered.v1— if the discount came from a recovery email, the recover event fires shortly before this one.
Next
- Discount codes resource — the full CRUD + redemption API.
- Webhooks reference — envelope, retries, signature verification.