# Guide to Recharge Webhooks: Features and Best Practices

Recharge webhooks notify your systems about subscription commerce activity: a subscription is created, a charge is paid, an order is processed. If you're provisioning access, syncing subscription state, or triggering fulfillment off Recharge, webhooks are how you react without polling the API.

This guide covers how Recharge webhooks work, the topics you can subscribe to, and one detail that trips up almost everyone: the `X-Recharge-Hmac-Sha256` header is not a true HMAC. Get that wrong and every delivery fails validation. We also cover the retry behavior and the best practices for production.

## What are Recharge webhooks?

Recharge webhooks are HTTP POSTs sent to an address you register, each carrying a JSON payload about a subscription, charge, or order event. You create webhooks through the Recharge Admin API, subscribing each to a specific topic.

The validation scheme is unusual, so read the security section carefully before you write the check.

## Recharge webhook features

| Feature | Details |
| --- | --- |
| Configuration | Admin API: `POST https://api.rechargeapps.com/webhooks` |
| Validation header | `X-Recharge-Hmac-Sha256` |
| Validation scheme | Plain SHA-256 of `client_secret + raw_body` (secret first), hex-encoded |
| API versions | `2021-01` and `2021-11` share tokens and topic names |
| Acknowledgement | Respond 200 within 5 seconds |
| Retries | 20 times over 48 hours, then the webhook subscription is deleted |
| Failure conditions | No response, 408, 429, or 5xx |
| SDK | No webhook-relevant official SDK |

## Common topics

Recharge topics are slash-separated and scoped by resource. Subscription, charge, and order topics are the common ones:

| Topic | Fires when |
| --- | --- |
| `subscription/created` | A new subscription is created |
| `subscription/cancelled` | A subscription is cancelled |
| `charge/created` | A charge is created |
| `charge/paid` | A charge is successfully paid |
| `charge/failed` | A charge fails |
| `order/created` | An order is created |
| `order/processed` | An order is processed |

Note the paid-charge topic is `charge/paid`. There is no `charge/success` topic; if you see it referenced anywhere, it's a legacy name. Subscribe only to the topics you process, and treat the Recharge events reference as authoritative for the full list.

## Setting up Recharge webhooks

Webhooks are created via the Admin API with an `address` and a `topic`:

```bash
curl -X POST "https://api.rechargeapps.com/webhooks" \
  -H "X-Recharge-Access-Token: $RECHARGE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "address": "https://your-app.example.com/webhook", "topic": "charge/paid" }'

```

API versions `2021-01` and `2021-11` share the same token and topic names. Respond to deliveries with a 200 within 5 seconds.

## Securing Recharge webhooks

Here's the trap. Despite the header name `X-Recharge-Hmac-Sha256`, the value is not an HMAC. It's a plain SHA-256 hash of your API client secret concatenated with the raw request body, with the secret first, then the body, hex-encoded. Reach for `hmac(secret, body)` (the natural default) and you'll reject every legitimate delivery.

```javascript
const crypto = require("crypto");

const CLIENT_SECRET = process.env.RECHARGE_CLIENT_SECRET;

function verify(rawBody, signature) {
  // NOT an HMAC: sha256(client_secret + raw_body), secret first
  const expected = crypto
    .createHash("sha256")
    .update(CLIENT_SECRET)
    .update(rawBody)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req.body, req.headers["x-recharge-hmac-sha256"])) {
    return res.sendStatus(401);
  }

  res.sendStatus(200); // acknowledge within 5 seconds
  processQueue.add(JSON.parse(req.body)); // process asynchronously
});

```

The same check in Python, matching Recharge's own example:

```python
import hashlib
import hmac
import os

CLIENT_SECRET = os.environ["RECHARGE_CLIENT_SECRET"]

def verify(raw_body: bytes, signature: str) -> bool:
    # secret first, then body; plain SHA-256, hex
    h = hashlib.sha256()
    h.update(CLIENT_SECRET.encode("utf-8"))
    h.update(raw_body)
    return hmac.compare_digest(h.hexdigest(), signature or "")

```

The raw body must be byte-exact; validation fails "even if one space is lost," so verify before any parsing or re-serialization.

## Recharge webhook limitations and pain points

### The signature is not an HMAC

The Problem: The header is named `X-Recharge-Hmac-Sha256`, but it's a plain SHA-256 of `client_secret + raw_body`. Every developer who assumes a standard HMAC gets a validation that never passes.

Why It Happens: Recharge's scheme concatenates the secret and body and hashes them, rather than using HMAC's keyed construction.

Workarounds:

* Use `sha256(client_secret + raw_body)` with the secret first; do not use `hmac()`.
* Verify against the exact raw bytes and compare in constant time.

How Hookdeck Can Help: Hookdeck verifies the Recharge signature at the edge, so your application receives pre-verified events and you never have to encode this non-standard scheme yourself.

### A 5-second timeout and deletion after failures

The Problem: Recharge waits 5 seconds for a 200 and retries a failed delivery 20 times over 48 hours, then deletes the webhook subscription. A persistently failing endpoint doesn't just miss events; it loses the subscription and stops receiving anything.

Why It Happens: Recharge prunes subscriptions whose endpoints look permanently broken.

Workarounds:

* Acknowledge with a 200 fast and process asynchronously so transient issues don't burn the retry budget.
* Monitor for missing deliveries and be ready to recreate a deleted webhook.

How Hookdeck Can Help: Hookdeck always accepts deliveries from Recharge and absorbs downstream failures itself, keeping the subscription alive while it retries to your service, so an outage never costs you the webhook.

### Duplicates over the retry window

The Problem: With up to 20 retries over 48 hours, the same event can arrive multiple times. Acting on duplicates double-provisions or double-charges downstream.

Why It Happens: At-least-once delivery favors not losing events over exactly-once semantics.

Workarounds:

* Dedupe on an event or resource identifier from the payload.
* Make side effects idempotent.

How Hookdeck Can Help: Hookdeck deduplicates deliveries at the edge, so the 48-hour retry window doesn't turn into repeated processing. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### No delivery log to recover from

The Problem: Once retries are exhausted (or the subscription is deleted), missed events aren't recoverable from Recharge.

Why It Happens: Recharge doesn't retain a durable, replayable delivery log for consumers.

Workarounds:

* Log every received delivery so you have your own record.
* Reconcile against the Recharge API for critical state.

How Hookdeck Can Help: Hookdeck durably stores every delivery and supports replay, so recovery doesn't depend on Recharge's retry window.

## Best practices

### Validate with sha256(secret + body), not HMAC

Hash the client secret followed by the raw body with plain SHA-256, hex-encode, and compare in constant time. This is the single most important detail for Recharge.

### Acknowledge within five seconds, process asynchronously

Return 200 fast and defer work to a queue so transient slowness doesn't push you toward the 20-failure deletion. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Use the correct topic names

Subscribe to `charge/paid` for paid charges; `charge/success` does not exist.

### Dedupe and reconcile

Dedupe on a payload identifier, make side effects idempotent, and reconcile against the API for anything you can't afford to miss.

## Conclusion

Recharge webhooks give you subscription, charge, and order activity in real time, but two details decide whether the integration works: the `X-Recharge-Hmac-Sha256` header is a plain `sha256(client_secret + raw_body)`, not an HMAC, and a persistently failing endpoint gets its subscription deleted after 20 retries over 48 hours. Add duplicates and no recoverable delivery log, and reliability rests on you.

That means correct validation, fast acknowledgement, deduplication, and reconciliation. [Hookdeck](https://hookdeck.com) verifies the signature, deduplicates, and durably queues every event with replay at the edge, so your application only ever processes verified, unique events and a bad day downstream never deletes your Recharge webhook.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Recharge webhooks reliably in minutes.