---
title: "What does the RevenueDot webhook event INITIAL_PURCHASE mean?"
description: "INITIAL_PURCHASE is sent when a customer first buys a subscription, including a free trial start. Grant access to the entitlements and record the customer."
url: https://revenuedot.app/docs/webhooks/webhook-initial-purchase
---

# What does the RevenueDot webhook event INITIAL_PURCHASE mean?

The RevenueDot webhook event INITIAL_PURCHASE means a customer bought a subscription for the first time, including the start of a free trial.

## Quick facts

| | |
|---|---|
| Event | `INITIAL_PURCHASE` |
| Where | RevenueDot webhook (`event.type`), RevenueCat's webhook format |
| Sent to | Every enabled webhook whose filters match |
| Fields that are specific to it | none beyond the common subscription fields |
| What it means | The first purchase of a subscription, including a free trial start. |

## When it is sent

- A subscription chain that RevenueDot has not seen before is recorded, from the SDK, a store notification (when new purchases are tracked), an import or a web checkout.
- `period_type` is `TRIAL` for a free trial and `INTRO` for a paid introductory offer; `NORMAL` otherwise.
- It is sent once per chain. A lapsed customer who comes back gets `RENEWAL`, not a second `INITIAL_PURCHASE`.

## What your server should do

1. Grant the entitlements in `entitlement_ids` until `expiration_at_ms`.
2. Record `original_transaction_id` as the key for the whole subscription.
3. In a trial, do not count revenue yet: `price` is the money that moved, and is 0 for a free trial.
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":"INITIAL_PURCHASE","app_user_id":"user_1","product_id":"pro_monthly","period_type":"NORMAL","entitlement_ids":["pro"]} });
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 !== "INITIAL_PURCHASE") return "200 ignored";
  return "grant " + event.product_id + " until expiration_at_ms, record the customer";
}

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

*Run-checked: `npm run check:snippets` runs this snippet with Node and compares its output with `grant pro_monthly` (checked 2026-10-03).*

## How RevenueDot produces it

When a store change arrives for a chain RevenueDot has no stored purchase for, the diff in `diffSubscription` returns `INITIAL_PURCHASE` first. A refunded or already cancelled first purchase also gets a `CANCELLATION` right after it, and an outstanding price increase gets its consent event.

## Related

- [What does the RevenueDot webhook event RENEWAL mean?](https://revenuedot.app/docs/webhooks/webhook-renewal.md)
- [What does the RevenueDot webhook event CANCELLATION mean?](https://revenuedot.app/docs/webhooks/webhook-cancellation.md)
- [What does the RevenueDot webhook event NON_RENEWING_PURCHASE mean?](https://revenuedot.app/docs/webhooks/webhook-non-renewing-purchase.md)
- [INITIAL_PURCHASE fields and a recorded example payload](https://revenuedot.app/docs/api/webhook-events.md#initial_purchase)
- [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/events.ts (how an event is recorded and delivered)](https://github.com/revenuedot/revenuedot/blob/main/apps/server/src/services/events.ts)
- [RevenueDot server: services/purchases.ts (where store changes become events)](https://github.com/revenuedot/revenuedot/blob/main/apps/server/src/services/purchases.ts)

Checked: 2026-10-03
