Agent skill

Prive Webhooks Skill

Receive and verify Prive webhooks (Prive is now Recurly Commerce, Shopify subscriptions). Use when setting up Prive webhook handlers, debugging x-prive-hmac-sha256 signature verification, or handling subscriptions/created, billingAttempts/failed, orders/created, and paymentMethod/updated topics.

Install this skill

npx skills add hookdeck/webhook-skills --skill prive-webhooks


Prive is a subscription app for Shopify merchants, now part of Recurly Commerce. The docs, API and headers still use the Prive name, so this skill does too.

When to Use This Skill

  • How do I receive Prive webhooks?
  • How do I verify the x-prive-hmac-sha256 signature on a Prive webhook?
  • How do I handle subscriptions/created or billingAttempts/failed webhooks?
  • Why is my Prive webhook signature verification failing?
  • How do I route Prive topics to separate handlers?

Verification (core)

Prive signs the raw request body with HMAC-SHA256, keyed with the secret token Prive gives the merchant, and sends the digest in x-prive-hmac-sha256 as a bare base64 string (Shopify-style, no prefix). Prive's Node sample passes 'hex' as a third argument to update(), which Node ignores, so the digest is base64, not hex. There is no timestamp. Compare in constant time, without decoding the header.

Node:

const crypto = require('crypto');

function verifyPriveSignature(rawBody, header, secret) {
  if (!header || !secret) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('base64');
  const a = Buffer.from(header, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Python:

import base64, hashlib, hmac

def verify_prive_signature(raw_body: bytes, header: str | None, secret: str) -> bool:
    if not header or not secret:
        return False
    expected = base64.b64encode(
        hmac.new(secret.encode(), raw_body, hashlib.sha256).digest()
    ).decode()
    # Encode both sides: compare_digest raises TypeError on non-ASCII str
    return hmac.compare_digest(header.encode("utf-8"), expected.encode("utf-8"))

Read the raw body before any JSON parsing. In Express use express.raw({ type: '*/*' }) so any content type verifies.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Webhook Topics

Each Prive webhook subscribes to one topic, chosen when the webhook is created (topic field of the create-webhook endpoint).

TopicDescription
subscriptions/createdA subscription was created
subscriptions/line_removedA line was removed from a subscription
subscriptions/status/updatedA subscription's status changed
orders/createdA subscription order was created
billingAttempts/createdA billing attempt was created
billingAttempts/successA billing attempt succeeded
billingAttempts/failedA billing attempt failed
paymentMethod/updatedA payment method was updated
subscriptionActivities/createdA subscription activity was created
subscription/customAttributes/updatedA subscription's custom attributes were updated

No topic header or body field is documented. Create one webhook per topic, each with its own endpoint path (for example /webhooks/prive/billing-attempts-failed), or one Hookdeck connection per topic, and dispatch on the path. The examples map those paths to topics.

Setup

  1. Prive (now Recurly Commerce) provides the secret token. Ask Prive support for it if you don't have it.
  2. Create a webhook per topic with the Prive create-webhook endpoint, pointing at your endpoint URL.
  3. With Hookdeck, create a Prive source and give it the same secret token as its signing secret, then add one connection per topic. See references/setup.md.

Environment Variables

PRIVE_WEBHOOK_SECRET=your_prive_secret_token   # Provided by Prive; ask Prive support if you don't have it

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 prive --path /webhooks/prive

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 9, 2026

View on GitHub →