Webhooks
Receiving and verifying signed event notifications.
Tidepay sends signed HTTP POST requests to your configured webhook URL whenever a subscription or payment event happens, so you don't have to poll the API for state changes.
Configuring your endpoint
Set your webhook URL from the dashboard: Settings → Business, under "Webhook URL". Your Signing Secret (Settings → API keys) is generated automatically the first time you save a webhook URL.
One secret for both environments
Unlike API keys, there is a single Signing Secret shared by both sandbox and live events — it doesn't come in test/live pairs. Use the payload's isSandbox field (see below) to tell them apart if you point both environments at the same endpoint.
Events
| Event | Fired when |
|---|---|
subscription.created | A subscription is created, before the subscriber has approved an allowance |
subscription.active | The first charge on a subscription settles successfully |
subscription.canceled | A subscription is canceled |
payment.succeeded | Any charge (recurring or one-off) settles successfully |
payment.failed | A charge fails (insufficient allowance/balance, a reverted transaction, etc.) |
customer.created | A customer record is created |
customer.updated | A customer record is updated |
customer.deleted | A customer record is deleted |
Payload shape
Every webhook body has the same envelope:
{
"id": "evt_...",
"event": "payment.succeeded",
"createdAt": "2026-08-03T14:00:00.000Z",
"isSandbox": false,
"data": {
"subscriptionId": "sub_...",
"chargeId": "chg_..."
}
}data varies by event — it carries the relevant resource IDs (subscription, charge, customer, etc.) needed to look up the full object via the API, rather than duplicating the entire resource inline.
Verifying the signature
Each request includes two headers:
X-Tidepay-Timestamp: 1735689600
X-Tidepay-Signature: 5f4dcc3b5aa765d61d8327deb882cf99...The signature is an HMAC-SHA256 of the timestamp and the raw request body (before any JSON parsing), signed with your Signing Secret:
signature = HMAC-SHA256(webhookSecret, `${timestamp}.${rawBody}`)Recompute it on your end and compare using a constant-time comparison — never === on the raw strings, which leaks timing information an attacker can use to guess the signature byte by byte.
import { createHmac, timingSafeEqual } from "node:crypto";
function isValidSignature(
secret: string,
timestamp: string,
rawBody: string,
signature: string,
): boolean {
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && timingSafeEqual(a, b);
}Read the raw body
Your HTTP framework's JSON body parser typically re-serializes the parsed
object, which can silently change field order or spacing and break the
signature check. Verify against the exact bytes Tidepay sent — read the
raw body before parsing it as JSON, or configure your framework to
expose it separately (e.g. Express's express.raw(), or reading the
Request stream directly in a Next.js route handler).
Retries
If your endpoint doesn't respond with a 2xx status (or the request fails outright — timeout, connection refused, etc.), Tidepay retries with exponential backoff: 1, 5, 30, 120, and 360 minutes after the previous attempt, for up to 6 total attempts. After the last attempt fails, the delivery is marked failed and not retried again.
Handle deliveries idempotently — use the payload's id field to detect and ignore duplicates, since a retry can occur even after your endpoint successfully processed the event but the response was lost in transit.
Rotating your Signing Secret
If your secret is ever exposed (e.g. committed to a public repo, logged somewhere insecure), rotate it from Settings → API keys → "Rotate signing secret". This immediately invalidates the old secret — every subsequent webhook is signed with the new one, so update your endpoint's verification code first if you want zero missed/rejected deliveries during the switch.
Sandbox webhooks
Sandbox events use the exact same signature scheme as live events — no special verification logic needed. See Sandbox mode for how sandbox and live events are distinguished.