---
title: "What does the RevenueDot webhook event PURCHASE_REDEEMED mean?"
description: "PURCHASE_REDEEMED is sent when a web purchase is redeemed in the app through a redemption link. The anonymous web customer is merged into the app user."
url: https://revenuedot.app/docs/webhooks/webhook-purchase-redeemed
---

# What does the RevenueDot webhook event PURCHASE_REDEEMED mean?

The RevenueDot webhook event PURCHASE_REDEEMED means a web purchase was redeemed in the app, so the customer who paid on the web is now the app user.

## Quick facts

| | |
|---|---|
| Event | `PURCHASE_REDEEMED` |
| Where | RevenueDot webhook (`event.type`), RevenueCat's webhook format |
| Sent to | Every enabled webhook whose filters match |
| Fields that are specific to it | `redeemed_from`, `redeemed_by`, `redemption_outcome`, `redemption_platform` |
| What it means | A web purchase was redeemed in the app through a redemption link (`POST /v1/subscribers/redeem_purchase`): the anonymous customer who paid on the web was merged into the app user. |

## When it is sent

- The app called the redeem endpoint with a token from a redemption link after a web checkout.
- The anonymous customer who paid is merged into the app user.
- `redeemed_from` is the anonymous web buyer and `redeemed_by` the app user id that redeemed it. RevenueDot adds `app_user_id` to RevenueCat's sample.

## What your server should do

1. Link the web purchase to the app user in your own records.
2. Grant the entitlements the web purchase unlocks.
3. Do not treat it as a new purchase: the purchase has its own events.
4. Deduplicate on `event.id`: deliveries can repeat, because anything but HTTP 200 is retried after 5, 10, 20, 40 and 80 minutes.

## Example

```javascript
import { createHmac, timingSafeEqual } from "node:crypto";

// X-RevenueCat-Webhook-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">
function verify(rawBody, header, secret, toleranceSeconds = 300) {
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? "");
  if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${m[1]}.${rawBody}`).digest();
  return timingSafeEqual(expected, Buffer.from(m[2], "hex"));
}

// Sign a sample delivery the way RevenueDot does, then handle it.
const secret = "whsec_test_secret";
const rawBody = JSON.stringify({ api_version: "1.0", event: {"id":"66339910-3BFF-49F4-B873-D1283D673DE2","type":"PURCHASE_REDEEMED","app_user_id":"user_1","product_id":"pro_monthly","redeemed_from":["$RCAnonymousID:a"],"redeemed_by":["user_1"]} });
const t = Math.floor(Date.now() / 1000);
const header = `t=${t},v1=${createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex")}`;

const seen = new Set(); // use a database table in production

function handle(rawBody, header) {
  if (!verify(rawBody, header, secret)) return "401 bad signature";
  const { event } = JSON.parse(rawBody);
  if (seen.has(event.id)) return "200 duplicate";
  seen.add(event.id);
  if (event.type !== "PURCHASE_REDEEMED") return "200 ignored";
  return "link the web purchase to " + event.redeemed_by[0];
}

console.log(handle(rawBody, header));
```

*Run-checked: `npm run check:snippets` runs this snippet with Node and compares its output with `link the web purchase to user_1` (checked 2026-10-03).*

## How RevenueDot produces it

`services/web/checkout.ts` records it when the redeem endpoint moves the web customer's purchase to the app user, using the fields of RevenueCat's sample plus `app_user_id`.

## Related

- [What does the RevenueDot webhook event TRANSFER mean?](https://revenuedot.app/docs/webhooks/webhook-transfer.md)
- [What does the RevenueDot webhook event SUBSCRIBER_ALIAS mean?](https://revenuedot.app/docs/webhooks/webhook-subscriber-alias.md)
- [What does the RevenueDot webhook event INITIAL_PURCHASE mean?](https://revenuedot.app/docs/webhooks/webhook-initial-purchase.md)
- [PURCHASE_REDEEMED fields and a recorded example payload](https://revenuedot.app/docs/api/webhook-events.md#purchase_redeemed)
- [Webhooks guide: set up, verify and test](https://revenuedot.app/docs/guides/webhooks.md)

## Source

- [RevenueDot docs: which webhook events RevenueDot sends](https://revenuedot.app/docs/api/webhook-events)
- [RevenueCat: webhook event types and fields (the format RevenueDot follows)](https://www.revenuecat.com/docs/integrations/webhooks/event-types-and-fields)
- [RevenueDot core: events.ts (the event types and the rules that derive them)](https://github.com/revenuedot/revenuedot/blob/main/packages/core/src/events.ts)
- [RevenueDot server: services/web/checkout.ts](https://github.com/revenuedot/revenuedot/blob/main/apps/server/src/services/web/checkout.ts)
- [RevenueDot server: services/events.ts (how an event is recorded and delivered)](https://github.com/revenuedot/revenuedot/blob/main/apps/server/src/services/events.ts)

Checked: 2026-10-03
