Guide to Prive Webhooks: Features and Best Practices
Prive webhooks tell your systems when something changes in a Shopify merchant's subscription program: a subscription is created or changes status, a renewal order is placed, a billing attempt succeeds or fails, or a customer updates their payment method. Prive is now Recurly Commerce, but its developer docs, API and webhook header still use the Prive name, so this guide does too.
This guide covers how Prive webhooks are created and signed, the topics you can subscribe to, the gaps in Prive's documentation that shape how you build a receiver, and the practices that make it reliable.
What are Prive webhooks?
A Prive webhook is a URL registered through Prive's API for a single topic, such as billingAttempts/failed. When that topic fires, Prive sends a request to the URL with an x-prive-hmac-sha256 header: the base64 HMAC-SHA256 of the raw request body, keyed with a secret token Prive gives the merchant.
Prive's public documentation is short. It covers how to create webhooks and how to verify them, but it doesn't show a delivery body, name a field or header that carries the topic, or describe retries. The practical consequence is that you should give each topic its own URL, so you always know which topic a request belongs to.
Prive webhook features
| Feature | Details |
|---|---|
| Configuration | Prive API, POST /api/v1/webhooks, plus list, get, update, delete and test |
| API base URL | https://subs.api.tryprive.com |
| API authentication | Bearer API key, from Prive support |
| Subscription | One topic per webhook |
| Signature header | x-prive-hmac-sha256 |
| Scheme | HMAC-SHA256, bare base64, no prefix |
| Signed content | The raw request body, nothing else |
| Secret | A merchant secret token provided by Prive |
| Topic in the request | Not documented |
| Testing | POST /api/v1/webhooks/{id}/test |
| Retries, timeout, source IPs | Not documented |
Webhook topics
Prive's create-webhook endpoint accepts 10 topics:
| Area | Topics |
|---|---|
| Subscriptions | subscriptions/created, subscriptions/line_, subscriptions/status/updated, subscription/customAttributes/updated, subscriptionActivities/created |
| Orders | orders/created |
| Billing | billingAttempts/created, billingAttempts/success, billingAttempts/failed |
| Payment methods | paymentMethod/updated |
Prive gives no descriptions beyond the names, and no example payloads. Note the irregular spelling: subscriptions/... for most subscription topics, but subscription/customAttributes/updated in the singular.
Setting up Prive webhooks
- Ask Prive support (
cs@tryprive.com, or the support section of your dashboard) for an API key and the webhook secret token, if you don't have them. Store the token server-side, for example asPRIVE_WEBHOOK_SECRET. - Create one webhook per topic, each with its own URL:
curl -X POST "https://subs.api.tryprive.com/api/v1/webhooks" \
-H "Authorization: Bearer $PRIVE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"address": "https://example.com/webhooks/prive/billing-attempts-failed",
"topic": "billingAttempts/failed"
}'
- The response includes the webhook's
id. CallPOST /api/v1/webhooks/{id}/testto send a test delivery, and use it to see what Prive actually sends.
Updating a webhook with PATCH /api/v1/webhooks/{id} changes only its address. To change a topic, delete the webhook and create a new one. Prive's API rate limit is a leaky bucket of 50 requests that refills at 2 per second, which matters only if you script many webhooks at once.
If you receive Prive webhooks through Hookdeck, create a source with the Prive type and paste the secret token Prive gave you. Prive (now Recurly Commerce) provides the token, so ask Prive support for it if you don't have it. Point each topic's webhook at a path on the source URL, and route on the path.
Securing Prive webhooks
Compute an HMAC-SHA256 of the raw request body with the secret token as the key, base64-encode it, and compare it with x-prive-hmac-sha256 in constant time. Base64 is case-sensitive, so compare the strings exactly.
const crypto = require('crypto');
const express = require('express');
function verifyPriveSignature(rawBody, signatureHeader, secret) {
if (!signatureHeader || !secret) return false;
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('base64');
const received = Buffer.from(signatureHeader.trim(), 'utf8');
const computed = Buffer.from(expected, 'utf8');
return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}
const app = express();
// One route per topic: the path tells you which topic this is.
app.post('/webhooks/prive/:topic', express.raw({ type: '*/*' }), (req, res) => {
const secret = process.env.PRIVE_WEBHOOK_SECRET;
if (!secret) return res.sendStatus(500); // fail closed
if (!verifyPriveSignature(req.body, req.get('x-prive-hmac-sha256'), secret)) {
return res.sendStatus(401);
}
const body = JSON.parse(req.body.toString('utf8'));
// Dispatch on req.params.topic, then process asynchronously.
res.sendStatus(200);
});
The same check in Python:
import base64
import hashlib
import hmac
def verify_prive_signature(raw_body: bytes, signature_header, secret: str) -> bool:
if not signature_header or not secret:
return False
expected = base64.b64encode(hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).digest())
return hmac.compare_digest(signature_header.strip().encode("utf-8"), expected)
# In FastAPI: raw_body = await request.body()
# verify_prive_signature(raw_body, request.headers.get("x-prive-hmac-sha256"),
# os.environ["PRIVE_WEBHOOK_SECRET"])
Prive's own TypeScript sample calls .update(body, 'utf8', 'hex'). The third argument looks like it asks for hex, but update() ignores it, and .digest('base64') decides the encoding. The sample then compares with ===. Use a constant-time comparison instead, as above.
Prive's sample also reads req.rawBody, which most frameworks don't set by default. In Express, express.raw() on the webhook route gives you the bytes as req.body.
Only the body is signed, and there is no timestamp, so there is no freshness check to make.
Make Prive webhooks production-ready. Hookdeck Event Gateway verifies the
x-prive-hmac-sha256header, stores every subscription and billing event, and delivers it to your service with retries you control.
Prive webhook limitations and pain points
Nothing in the request says which topic it is
The Problem: One handler receives several topics and can't tell them apart.
Why It Happens: Prive documents no topic header and no body field for the topic. The only place the topic appears is the webhook you registered.
Workarounds:
- Give each topic its own URL path, and dispatch on the path.
- Use the test endpoint and log a real delivery's headers and body. If you find a field that carries the topic, still keep the paths as a fallback, because it isn't documented and could change.
How Hookdeck Can Help: With Hookdeck, point each topic at its own path on the source URL. Hookdeck filters on the path send each topic to the right destination.
No payload documentation
The Problem: You can't write a parser from the docs.
Why It Happens: Prive publishes the topic names and nothing about the bodies.
Workarounds:
- Send a test delivery for each webhook and build against what arrives.
- Treat the webhook as a notification, and fetch the subscription or order from the Prive API when you need specific fields.
How Hookdeck Can Help: Hookdeck stores every request with its full headers and body, so the first real deliveries for each topic are there to read before you write the handler.
Getting the secret takes a support request
The Problem: You can't start verifying until someone at Prive replies.
Why It Happens: Prive provides both the API key and the webhook secret token through support rather than in the dashboard, and doesn't say whether the token is per merchant or per webhook.
Workarounds:
- Ask for the API key and the webhook secret token in the same request.
- Store the token where every receiver can read it, and ask Prive how to rotate it.
How Hookdeck Can Help: With Hookdeck in front, the token lives on one source. Hookdeck answers Prive with a 200 either way, then records a request that fails the check as failed verification, so it never reaches your destination.
Delivery behaviour is undocumented
The Problem: You don't know whether Prive retries a failed delivery, or for how long.
Why It Happens: Prive doesn't describe timeouts, retries or ordering.
Workarounds:
- Return
200as soon as the request is verified and stored. - Reconcile billing attempts and subscription statuses against the Prive API on a schedule, so a lost webhook doesn't leave your records wrong.
How Hookdeck Can Help: Hookdeck accepts each delivery as soon as it arrives and then delivers it to your service with retries you configure, so your own outages don't depend on whatever Prive does.
Best practices
Verify against the raw body
The signature covers the exact bytes Prive sent. In Express, use express.raw({ type: '*/*' }). In Next.js, use await request.text(). In FastAPI, use await request.body().
One URL per topic
Register each topic at its own path and keep a table in your code of path to topic. It is the only reliable way to route Prive deliveries.
Make handlers idempotent
Prive documents no event ID, so build a key from the topic and the IDs in the body once you have seen real payloads, and skip anything already processed. See our guide to webhook idempotency.
Acknowledge fast and process asynchronously
Verify, store the request, return 200, and do the work afterwards. See why to process webhooks asynchronously.
Conclusion
Prive signs each webhook with a bare base64 HMAC-SHA256 of the raw body in x-prive-hmac-sha256, keyed with the secret token Prive support gives you. Register one webhook per topic at its own URL, because nothing in the request names the topic, and use the test endpoint to learn what each payload contains.
Hookdeck Event Gateway sits in front of your receiver, verifies every Prive signature, and keeps every request so you can see exactly what Prive sends.
Get started with Hookdeck for free and handle Prive webhooks reliably in minutes.