Agent skill
Purchasely Webhooks Skill
Receive and verify Purchasely webhooks. Use when setting up a Purchasely server event endpoint, verifying the X-PURCHASELY-REQUEST-SIGNATURE header, debugging signature failures or retries, or handling SUBSCRIPTION_STARTED, SUBSCRIPTION_RENEWED, SUBSCRIPTION_TERMINATED, RENEWAL_DISABLED and TRANSACTION_PROCESSED events.
Install this skill
npx skills add hookdeck/webhook-skills --skill purchasely-webhooks
Purchasely (in-app subscription and paywall management) sends server events to your HTTPS endpoint for the whole subscription lifecycle: lifecycle, offer, transactional and entitlement events. Each delivery is a POST with a JSON body whose event_name field names the event, signed with HMAC-SHA256 over the timestamp header value followed by the raw body.
When to Use This Skill
- How do I receive Purchasely webhooks?
- How do I verify the
X-PURCHASELY-REQUEST-SIGNATUREheader? - Why does my Purchasely signature verification fail?
- How do I handle
SUBSCRIPTION_STARTED,SUBSCRIPTION_RENEWEDorSUBSCRIPTION_TERMINATEDevents? - Why does Purchasely keep retrying, and why are webhooks for one user paused?
Verification (core)
Headers: X-PURCHASELY-REQUEST-SIGNATURE (bare lowercase hex digest, no prefix) and X-PURCHASELY-TIMESTAMP (Unix seconds). Signed message = timestamp header value + raw body, no separator. Key = the webhook shared secret as UTF-8 (not decoded). Ignore the deprecated X-PURCHASELY-SIGNATURE header.
const crypto = require('crypto');
const HEX_SHA256 = /^[0-9a-fA-F]{64}$/;
function verifyPurchasely(headers, rawBody, secret = process.env.PURCHASELY_WEBHOOK_SECRET) {
const sig = headers['x-purchasely-request-signature'];
const ts = headers['x-purchasely-timestamp'];
if (!secret || typeof sig !== 'string' || typeof ts !== 'string' || !ts) return false;
if (!HEX_SHA256.test(sig)) return false; // validate before decoding
const message = Buffer.concat([Buffer.from(ts, 'latin1'), rawBody]); // rawBody is a Buffer
const expected = crypto.createHmac('sha256', secret).update(message).digest();
return crypto.timingSafeEqual(expected, Buffer.from(sig.toLowerCase(), 'hex'));
}
Return 401 when it fails. Read the body raw (express.raw({ type: '*/*' }) in Express, await request.arrayBuffer() in Next.js, await request.body() in FastAPI) and parse JSON only after the check.
Timestamp window is off by default. Hookdeck retries and replays resend the original headers and timestamp, so a fixed window would reject legitimate deliveries. The examples offer an opt-in: set PURCHASELY_ (unset or 0 = off) to reject timestamps further than that many seconds from now.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Response and Retries
Answer exactly HTTP 200. Any other code, other 2xx included, counts as a failure: Purchasely retries up to 25 times over about 20 days, and pauses later webhooks for that user until the first succeeds (to keep events in order). Dedupe on event_id, and return 200 for events you choose to ignore.
Event Types
Dispatch on the body's event_name. Common events (full list in references/overview.md):
| Event | Sent When |
|---|---|
SUBSCRIPTION_STARTED | A subscription is purchased |
SUBSCRIPTION_RENEWED | The subscription renews |
RENEWAL_DISABLED | The customer turns off auto-renewal |
GRACE_PERIOD_STARTED / ENTERED_BILLING_RETRY | A billing issue occurs at renewal |
SUBSCRIPTION_TERMINATED | The subscription ends; no active subscription remains |
TRANSACTION_PROCESSED | A transaction is processed (amount and currency) |
ACTIVATE | An entitlement is activated (see Purchasely's entitlements guide) |
Environment Variables
PURCHASELY_WEBHOOK_SECRET=your-webhook-shared-secret # Console: [YOUR APP] > Settings > Webhooks
# PURCHASELY_WEBHOOK_TOLERANCE_SECONDS=300 # optional replay window, off by default
Local Development
Purchasely needs a public https URL. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 purchasely --path /webhooks/purchasely
Resources
- references/overview.md - How Purchasely webhooks work, events, payload fields
- references/setup.md - Configure the webhook URL and events in the Console
- references/verification.md - Signature details, test vector, gotchas
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests