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 with MAX_USES_REACHED. Notify the merchant if it's a campaign code they didn't expect to exhaust.
  • Reconcile against partner orders. redemption.externalRef is the partner's order ID; pair it with code.code to 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 redeemedAt as 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.usesCount as authoritative for "remaining slots". Two concurrent redemptions can both observe usesCount = N and both commit, so the count can momentarily exceed maxUsesTotal by one or two. Use it as a "roughly how full is the bucket" signal, not a strict guard.
  • Assuming customerId is set. Anonymous checkouts produce redemption rows with customerId: null. Default to "anonymous" in your analytics rather than dropping the row.
  • Trusting externalSource without validating. It's whatever the partner platform set. For the canonical Storlaunch integration, "storlaunch" is the convention, but a custom integration can pass anything.

Next