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-SIGNATURE header?
  • Why does my Purchasely signature verification fail?
  • How do I handle SUBSCRIPTION_STARTED, SUBSCRIPTION_RENEWED or SUBSCRIPTION_TERMINATED events?
  • 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_WEBHOOK_TOLERANCE_SECONDS (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):

EventSent When
SUBSCRIPTION_STARTEDA subscription is purchased
SUBSCRIPTION_RENEWEDThe subscription renews
RENEWAL_DISABLEDThe customer turns off auto-renewal
GRACE_PERIOD_STARTED / ENTERED_BILLING_RETRYA billing issue occurs at renewal
SUBSCRIPTION_TERMINATEDThe subscription ends; no active subscription remains
TRANSACTION_PROCESSEDA transaction is processed (amount and currency)
ACTIVATEAn 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 10, 2026

View on GitHub →