Receive and verify Recharge (subscription commerce) webhooks. Use when setting up Recharge webhook handlers, debugging X-Recharge-Webhook-Signature or legacy X-Recharge-Hmac-Sha256 signature verification, or handling subscription events like charge/paid, charge/failed, subscription/created, subscription/cancelled, and order/created.
Receive and verify Recharge (subscription commerce) webhooks. Use when setting up Recharge webhook handlers, debugging X-Recharge-Webhook-Signature or legacy X-Recharge-Hmac-Sha256 signature verification, or handling subscription events like charge/paid, charge/failed, subscription/created, subscription/cancelled, and order/created.
Why is my X-Recharge-Webhook-Signature or X-Recharge-Hmac-Sha256 verification failing?
How do I handle charge/paid, charge/failed, or subscription/cancelled events?
How do I create a Recharge webhook subscription via the API?
Verification (core)
Every webhook delivery includes two signature schemes: a recommended timestamp-bound scheme
(use this for all new integrations) and a legacy body-only scheme that remains supported.
Recommended: timestamp-bound scheme
Two headers are sent:
X-Recharge-Webhook-Timestamp — Unix epoch seconds (integer) at the time the request was signed.
X-Recharge-Webhook-Signature — comma-separated key/value pairs in the form
t=<epoch>,v1=<hex> (future schemes may add v2=…, so parse by key).
To verify:
Parse t and v1 from X-Recharge-Webhook-Signature (t matches the timestamp header).
Compute HMAC-SHA-256, keyed by the API Client Secret, over "<timestamp>.<payload_json>" —
the timestamp, a literal dot, then the exact raw JSON bytes as transmitted (re-serializing
breaks it).
Compare the hex digest to v1 with a constant-time comparison.
import hashlib, hmac, time
defverify_recharge_webhook_timestamped(raw_body: bytes, signature_header: str, client_secret: str) -> bool:
ifnot signature_header:
returnFalse
parts = dict(pair.partition("=")[::2] for pair in signature_header.split(","))
timestamp, signature = parts.get("t", ""), parts.get("v1", "")
ifnot timestamp.isdigit() ornot signature:
returnFalse# Reject deliveries outside the 48-hour window.ifabs(int(time.time()) - int(timestamp)) > 172800:
returnFalse# HMAC-SHA-256 over "<timestamp>.<raw body>", keyed by the client secret.
digest = hmac.new(
client_secret.encode("utf-8"), f"{timestamp}.".encode("utf-8") + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(digest, signature)
Legacy: body-only scheme (X-Recharge-Hmac-Sha256)
For backward compatibility, every webhook also includes the legacy X-Recharge-Hmac-Sha256
header. Fall back to it only when the new header is absent.
The biggest gotcha: despite the header name, this is NOT a true HMAC. It is a plain
SHA-256 hash of the API Client Secret concatenated with the raw request body — secret first,
then body — hex-encoded. Use sha256(secret + rawBody), not hmac(secret, rawBody). Always hash
the raw body bytes; verification fails "even if one space is lost".
Node:
functionverifyRechargeWebhookLegacy(rawBody, signatureHeader, clientSecret) {
if (!signatureHeader) returnfalse;
// Plain SHA-256 of (clientSecret + rawBody), NOT HMAC. Secret is prepended.const digest = crypto.createHash('sha256').update(clientSecret).update(rawBody).digest('hex');
try {
return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(signatureHeader));
} catch {
returnfalse; // length mismatch = invalid
}
}
Python:
defverify_recharge_webhook_legacy(raw_body: bytes, signature_header: str, client_secret: str) -> bool:
ifnot signature_header:
returnFalse# Plain SHA-256 of (client_secret + raw_body), NOT HMAC. Secret is prepended.
digest = hashlib.sha256(client_secret.encode("utf-8") + raw_body).hexdigest()
return hmac.compare_digest(digest, signature_header)
There is no official Recharge SDK for webhook verification (@rechargeapps/storefront-client covers
the Storefront API only), so verify manually as above.
Dispatching events
Recharge does not send a documented topic/action header. Payloads wrap the resource by a
top-level key — {"charge": {…}}, {"order": {…}}, {"subscription": {…}} — so dispatch on that
key. If your handler needs the exact action (created vs updated vs paid), register a
distinct endpoint path per topic when creating the webhook subscription (POST /webhooks with a
different address per topic).
Respond with 200 within 5 seconds. No response, 408, 429, or 5xx counts as failure.
Recharge retries the same webhook 20 times over 48 hours, then deletes the subscription. Do slow
work asynchronously and return 200 immediately.
For complete handlers with route wiring, event dispatch, and tests, see:
# API Client Secret from the Recharge Dashboard → Integrations → API Tokens →# click your token (Edit API Token page). This is NOT the API access token.
RECHARGE_API_CLIENT_SECRET=your_api_client_secret_here
Creating a Webhook Subscription
Webhooks are registered via the Admin API (one subscription per topic):
We recommend installing the webhook-handler-patterns skill alongside this one for handler sequence, idempotency, error handling, and retry logic. Key references (open on GitHub):
Handler sequence — Verify first, parse second, handle idempotently third
Idempotency — Prevent duplicate processing (Recharge retries up to 20 times)
Error handling — Return codes, logging, dead letter queues
hookdeck-event-gateway - Webhook infrastructure that replaces your queue — guaranteed delivery, automatic retries, replay, rate limiting, and observability for your webhook handlers