ripllo.discount_code.redeemed.v1
The "a discount code was used for real" signal — it carries the redemption row plus enough context to reconcile against the partner's order log.
Reserved — not currently emitted in v1. Nothing in Ripllo writes a
ripllo.discount_code.redeemed.v1outbox row:POST /api/v1/discount-codes/redeemcommits theDiscountRedemptionand returns, and no event is built. More broadly, no Ripllo webhook is delivered to any endpoint today — the outbox worker'sdeliver()step is still a stub that marks rows published without POSTing them anywhere, so even the loyalty events that are written never reach aWebhookEndpoint. Subscribe defensively; for now, trigger off your ownredeem()return value or pollGET /api/v1/discount-codes.
When it will fire
When POST /api/v1/discount-codes/redeem commits a new DiscountRedemption row. The Storlaunch partner SDK calls this from its payment-success webhook, so once wired the event would fire within seconds of money landing.
It would be single-shot per redemption: the redemption is idempotent on (accountId, checkoutSessionId), so a partner retry of the same session returns the existing row with 200 OK and would not re-emit.
Payload
{
"id": "evt_01HX...",
"type": "ripllo.discount_code.redeemed.v1",
"occurredAt": "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",
"createdAt": "2026-05-13T10:43:22.140Z"
},
"code": {
"id": "dc_01HX...",
"code": "WELCOME10",
"type": "percent",
"value": 10,
"currency": "IDR",
"scope": "cart",
"redemptionCount": 87,
"maxUsesTotal": null
}
},
"metadata": {}
}
The envelope field is occurredAt, not createdAt, and every envelope also carries a metadata object (empty unless the emitter set one). accountId is nullable — platform-level events carry null.
The payload carries both the redemption row and a snapshot of the code at redemption time. redemptionCount (the code's denormalized counter — there is no usesCount field) reflects the count including this redemption. For percent codes, value is a whole percent.
Handler examples
All three SDKs take the Ripllo-Signature header value — a single string shaped t=<unix>,v1=<hex>, signing ${t}.${rawBody} — not the whole headers map. Pass the raw, unparsed body bytes.
// Node
import { verifyWebhook } from '@forjio/ripllo-node';
app.post('/ripllo/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const event = verifyWebhook({
rawBody: req.body,
signature: req.headers['ripllo-signature'],
secret: process.env.RIPLLO_WEBHOOK_SECRET,
// toleranceSec: 300 — optional, this is the default
});
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.redemptionCount >= code.maxUsesTotal) {
notifyMerchant(`Code ${code.code} sold out`);
}
}
res.status(200).end();
});
# Python — arguments are keyword-only
from ripllo import verify_webhook
event = verify_webhook(
raw_body=raw_body,
signature=request.headers.get('Ripllo-Signature'),
secret=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 — pass nil for the options struct to accept the defaults
import ripllo "github.com/hachimi-cat/ripllo-go"
event, err := ripllo.VerifyWebhook(rawBody, r.Header.Get("Ripllo-Signature"), os.Getenv("RIPLLO_WEBHOOK_SECRET"), nil)
if err != nil {
http.Error(w, "bad signature", http.StatusBadRequest)
return
}
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.redemptionCount >= 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.redemptionCountas authoritative for "remaining slots". Two concurrent redemptions can both observeredemptionCount = 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
All three are likewise reserved-not-emitted today:
ripllo.discount_code.created.v1— the create-side counterpart.ripllo.referral.created.v1— would pair with this one when the redemption is for a referral reward code.ripllo.abandoned_cart.recovered.v1— would fire shortly before this one when the discount came from a recovery email.
The only event types Ripllo builds outbox rows for at all are ripllo.loyalty.earned.v1, ripllo.loyalty.redeemed.v1 and ripllo.loyalty.voided.v1 — and those are marked published without being delivered.
Next
- Discount codes resource — the full CRUD + redemption API.
- Webhooks reference — envelope, retries, signature verification.