Send RevenueCat Initial-Purchase Alerts to Your Team's Phones
Verify one RevenueCat INITIAL_PURCHASE webhook, queue a shared alert, and keep customer and transaction details out of the Ping.

To send a RevenueCat initial-purchase event to a small team's phones, place a narrow server-side relay between RevenueCat and a PingRoom incoming webhook. Verify RevenueCat's signature over the raw request body, accept one production INITIAL_PURCHASE event for the app, product, and entitlement you chose, save one minimal job, then send fixed copy to the Room.
This is a webhook recipe, not a native RevenueCat integration. RevenueCat remains the source of truth for subscriptions, purchases, entitlements, customers, stores, and webhook delivery. PingRoom carries one selected attention signal to the people who need it.
Decide whether this is the right destination
RevenueCat already supports destinations such as Slack, Discord, and OneSignal. Use one of those when it already reaches the right people in the right context.
Use a RevenueCat-to-PingRoom relay when one narrow event should reach a small shared Room, including people who do not watch a billing dashboard or team chat.
| Need | Better path |
|---|---|
| Review complete subscription and customer state | RevenueCat dashboard and APIs |
| Send lifecycle data to an existing supported platform | RevenueCat's native integration for that platform |
| Notify app customers inside your product | Your app's customer-engagement and push system |
| Give a small internal group one selected phone signal | Verified RevenueCat webhook → relay → PingRoom Room |
Do not forward the RevenueCat payload to a lock screen. This guide sends one fixed title and message. Customer identity, transaction IDs, store, product metadata, price, currency, and revenue stay in RevenueCat.
Check both Pro requirements
RevenueCat lists webhooks as a Pro integration. The PingRoom Room owner also needs PingRoom Pro to create an incoming webhook. These are separate subscriptions.
You also need:
- an HTTPS relay that can read the exact raw request bytes;
- a durable store or queue with an atomic insert-if-absent operation;
- the RevenueCat app ID, product ID, and entitlement ID you intend to accept;
- a private PingRoom Room for the people who should receive the signal.
PingRoom allows up to four incoming webhooks per Room. Create or choose the Room, open Settings → Connections → From another app, add a webhook named RevenueCat, keep Quick Ping on Custom message, and copy the secret URL into protected server configuration. The incoming-webhook walkthrough covers that setup, while the webhook reference carries the current request limits, replay behavior, cooldowns, and error responses.
Configure one RevenueCat event
In RevenueCat, open the project, choose Integrations → Webhooks, and add your relay's public HTTPS endpoint. Then narrow the configuration:
- Choose Production events.
- Select the one app this receiver expects.
- Select
INITIAL_PURCHASEas the event type. - Enable HMAC webhook signing and save the signing secret in server-side secret storage.
- Send a test event before enabling the real route.
RevenueCat can also send an authorization header. This recipe relies on the stronger timestamped HMAC check and does not treat a secret header alone as proof that the body is unchanged.
INITIAL_PURCHASE needs careful wording. A trial can produce it with period_type: TRIAL, and some store-specific product changes can produce it alongside other events. This relay accepts only period_type: NORMAL, but the resulting signal is still not proof of a first-ever customer, settled revenue, or completed provisioning.
Verify the raw body before parsing JSON
RevenueCat's current signature header uses t=<timestamp>,v1=<hex>. The HMAC-SHA-256 input is the timestamp, a period, and the exact raw JSON body.
import { createHmac, timingSafeEqual } from "node:crypto"; function verifyRevenueCat({ rawBody, header, secret, nowSeconds }) { const parts = new Map( String(header) .split(",") .map((part) => part.trim().split("=", 2)), ); const timestamp = parts.get("t"); const signature = parts.get("v1"); if (!/^\d{1,16}$/.test(timestamp ?? "")) return false; if (!/^[a-f0-9]{64}$/i.test(signature ?? "")) return false; if (Math.abs(nowSeconds - Number(timestamp)) > 300) return false; const expected = createHmac("sha256", secret) .update(`${timestamp}.`, "utf8") .update(rawBody) .digest(); const received = Buffer.from(signature, "hex"); return ( received.length === expected.length && timingSafeEqual(received, expected) ); }
Read and cap the body before verification. Do not parse it and then recreate JSON for the HMAC input; whitespace or key-order changes can invalidate a legitimate delivery. Reject a missing signature, a bad digest, and a timestamp more than five minutes away in either direction. Fix a drifting server clock instead of casually widening the window.
Filter and queue one minimal job
After verification, parse the JSON and apply every boundary before storing work:
export async function receiveInitialPurchase({ rawBody, headers, env, queue, nowSeconds = Math.floor(Date.now() / 1000), }) { const bytes = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody); if (bytes.length > 128 * 1024) return { status: 413 }; if ( !verifyRevenueCat({ rawBody: bytes, header: headers.get("x-revenuecat-webhook-signature"), secret: env.REVENUECAT_SIGNING_SECRET, nowSeconds, }) ) { return { status: 401 }; } let payload; try { payload = JSON.parse(bytes.toString("utf8")); } catch { return { status: 400 }; } const event = payload?.event; if (event?.type !== "INITIAL_PURCHASE") return { status: 200 }; if (event?.environment !== "PRODUCTION") return { status: 200 }; if (event?.period_type !== "NORMAL") return { status: 200 }; if (event?.app_id !== env.REVENUECAT_APP_ID) return { status: 200 }; if (event?.product_id !== env.REVENUECAT_PRODUCT_ID) return { status: 200 }; if (!event?.entitlement_ids?.includes(env.REVENUECAT_ENTITLEMENT_ID)) { return { status: 200 }; } if (!/^[A-Za-z0-9_-]{8,128}$/.test(event?.id ?? "")) { return { status: 400 }; } const eventKey = `revenuecat:initial-purchase:${event.id}`; try { const inserted = await queue.putIfAbsent(eventKey, { eventKey }); return { status: 200, code: inserted ? "queued" : "duplicate" }; } catch { return { status: 503, code: "queue_unavailable" }; } }
RevenueCat retries non-200 responses and reuses the payload event ID. Store one derived key atomically before acknowledging the delivery, then let a worker contact PingRoom. The queued job contains no app_user_id, subscriber attribute, transaction ID, store, price, currency, or product label.

Pingo the Pigeon, PingRoom's fictional CMO, is keeping his usual distance from the detail chest in this illustrated example. He cannot inspect RevenueCat data, access the private Room, or send the Ping. People configure the relay and remain responsible for the signal's meaning.
Send reviewed copy from a worker
Build the Ping from constants, not from fields in the purchase event:
export async function sendRevenueCatSignal({ job, webhookUrl, fetchImpl = fetch, }) { const body = JSON.stringify({ title: "RevenueCat initial purchase", message: "Open RevenueCat to review the event", correlation_id: job.eventKey, }); let response; try { response = await fetchImpl(webhookUrl, { method: "POST", headers: { "Content-Type": "application/json", "Idempotency-Key": job.eventKey, }, body, }); } catch { return { outcome: "retry" }; } if (response.status === 429 || response.status >= 500) { return { outcome: "retry" }; } if (!response.ok) return { outcome: "configuration_failure" }; const result = await response.json().catch(() => null); return result?.success === true ? { outcome: "delivered" } : { outcome: "retry" }; }
Keep the request body and idempotency key unchanged across retries. A network error, HTTP 429, or server error stays on the retry path. Another 4xx usually means a person needs to repair the destination, Pro state, secret, or payload.
The atomic insert prevents repeated RevenueCat deliveries from creating several jobs. PingRoom's idempotency handling lets the worker retry one unchanged request without intentionally creating another Ping. That is replay-safe processing, not an exactly-once guarantee.
Test the signal without customer data
Use RevenueCat's dashboard test event or a sandbox purchase, a test Room, and non-sensitive fixture values.
- Send one signed production-shaped
INITIAL_PURCHASEevent for the configured app, product, and entitlement withperiod_type: NORMAL. Confirm the queue stores only the derived event key and the Room receives fixed copy. - Replay the same event. Confirm the receiver reports
duplicateand keeps one job. - Send a
RENEWAL, a sandbox event, a trial, and events for another app, product, and entitlement. Confirm each is acknowledged and ignored. - Change one raw-body byte, remove the signature, sign with the wrong secret, and move the timestamp outside the five-minute window. Confirm nothing is stored.
- Make the durable store unavailable. Confirm the receiver returns non-200 so RevenueCat can retry instead of losing the event.
- Make the PingRoom call return 429, 500, and a network error. Confirm the worker retries with the same body and key. Treat another 4xx as a configuration problem for human review.
- Inspect the queue, logs, and outbound request. Confirm none contains customer, transaction, store, price, revenue, or currency values.
Our recorded fixture check covers those paths with locally generated HMAC signatures, an injected clock, a stateful in-memory queue, and a mocked PingRoom receiver. It does not use a live RevenueCat project, a live PingRoom Room, a store purchase, or a physical phone. Run the harmless end-to-end test in your own environments before relying on the route.
Read the Ping conservatively
The Ping means only the relay validated a fresh RevenueCat delivery, accepted one matching production INITIAL_PURCHASE event with period_type: NORMAL, saved one minimal job, and sent the reviewed signal toward PingRoom. Arrival still depends on each recipient's device, connectivity, Room preferences, and notification settings.
Open RevenueCat to review current purchase, subscription, entitlement, customer, and store state. Do not treat the Ping as proof of a first-ever customer, collected or settled revenue, access provisioning, accounting closure, staff acknowledgement, or a guaranteed phone display.
RevenueCat's webhook guide documents configuration, retries, tests, and raw-body HMAC verification. Its event reference defines INITIAL_PURCHASE, while the common flows guide explains trials and product-change cases.
Review PingRoom's complete webhook contract before rollout. The incoming webhook feature page explains the Room connection without presenting it as RevenueCat-native.
When all seven tests pass, open PingRoom, choose the private destination Room, and activate only the initial-purchase signal its members are prepared to interpret correctly.
The two editorial illustrations were created with AI using approved PingRoom character references and reviewed by the PingRoom team.
PingRoom
Short-form communication.


