Platform
Webhooks
Receive real-time event notifications for payment lifecycle changes.
How webhooks work
When events happen in your PaymentHooks account (a payment succeeds, an invoice is created, a dispute is opened), we send an HTTP POST request to your configured webhook endpoints. This allows your application to react to events in real-time without polling the API.
Event payload structure
Every webhook POST contains the same envelope. The resource the event is about lives under data.object; on *.updated events the previous values of changed fields are under data.previous_attributes.
{
"id": "evt_1a2b3c4d5e6f",
"object": "event",
"api_version": "2026-04-22",
"created": 1751500800,
"type": "payment.succeeded",
"livemode": true,
"data": {
"object": {
"id": "ph_pay_7k8m2n4p6q",
"object": "payment_intent",
"amount": 9500,
"currency": "usd",
"status": "succeeded",
"customer": "ph_cus_3a5b7c9d1e",
"latest_charge": "ch_ph_pay_7k8m2n4p6q",
"metadata": {},
"created": 1751500800,
"livemode": true
}
},
"request": {
"id": null,
"idempotency_key": null
}
}| Field | Type | Description |
|---|---|---|
| id | string | Unique event id (evt_…). Use it to deduplicate — delivery is at-least-once. |
| object | string | Always "event". |
| api_version | string | API version the payload is serialized against (e.g. 2026-04-22). |
| created | integer | Unix timestamp (seconds) when the event was created. |
| type | string | The event type, e.g. "payment.succeeded" — also sent in the X-PaymentHooks-Event header. |
| livemode | boolean | true for live events, false for test-mode events. |
| data.object | object | The resource the event relates to (payment_intent, invoice, subscription, …). |
| data.previous_attributes | object? | Only on *.updated events — the changed fields with their prior values. |
| request.id | string | null | Id of the API request that caused the event, when applicable. |
| request.idempotency_key | string | null | Idempotency key of the originating request, when one was supplied. |
Per-event resource shapes (payment_intent, charge, invoice, subscription, …) are catalogued on the Webhook Events reference.
Delivery headers
Every webhook POST carries the headers below. The signature header carries the timestamp and HMAC you verify against the shared secret; the event and delivery headers let you route and deduplicate without parsing the JSON body.
| Header | Example | Purpose |
|---|---|---|
| Content-Type | application/json | Body is always a JSON-encoded event envelope. |
| X-PaymentHooks-Signature | t=1751500800,v1=5257a8… | HMAC-SHA256 signature. During a 24h secret rotation a second v1_previous=… scheme is also included. |
| X-PaymentHooks-Event | payment.succeeded | The event type — lets you route without parsing the body first. |
| X-PaymentHooks-Delivery | whd_1a2b3c-4d5e6f | Unique per delivery attempt. Retries of the same event reuse the event id but get a new delivery id. |
| X-PaymentHooks-Api-Version | 2026-04-22 | API version the payload is serialized against. |
| User-Agent | PaymentHooks-Webhook/1.0 | Identifies the PaymentHooks delivery agent. |
Setting up an endpoint
- 1Go to Developers → Webhooks in your merchant dashboard
- 2Click Add endpoint and enter your URL (must be HTTPS in production)
- 3Select which events you want to receive
- 4Copy the signing secret — you will need it to verify webhook signatures
Verifying signatures
Every webhook includes a X-PaymentHooks-Signature header. Always verify this signature to ensure the webhook is authentic.
import crypto from 'crypto';
const header = req.headers['x-paymenthooks-signature'];
const parts = header.split(',').reduce((acc, p) => {
const [k, v] = p.split('=');
if (k && v) acc[k] = v;
return acc;
}, {});
const signedPayload = parts.t + '.' + JSON.stringify(req.body);
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(signedPayload)
.digest('hex');
if (expected !== parts.v1) {
return res.status(401).send('Invalid signature');
}
// Process event
const event = req.body;Endpoints & Responses
Manage your webhook endpoints via the API. Complete payload and response for each operation.
/v1/webhook-endpointsCreate a webhook endpoint{
"url": "https://yoursite.com/webhooks/paymenthooks",
"description": "Production webhook receiver",
"subscribedEvents": ["payment.succeeded", "payment.failed", "invoice.paid"]
}{
"data": {
"id": "we_abc123",
"url": "https://yoursite.com/webhooks/paymenthooks",
"description": "Production webhook receiver",
"subscribedEvents": ["payment.succeeded", "payment.failed", "invoice.paid"],
"secret": "whsec_k8m2n4p6r0t1v3x5z7a9c1e3g5i7",
"status": "active",
"merchantId": "mer_abc123",
"createdAt": "2026-05-16T10:30:00.000Z"
},
"error": null
}/v1/webhook-endpointsList webhook endpoints{
"data": [
{
"id": "we_abc123",
"url": "https://yoursite.com/webhooks/paymenthooks",
"description": "Production webhook receiver",
"subscribedEvents": ["payment.succeeded", "payment.failed", "invoice.paid"],
"status": "active",
"createdAt": "2026-05-16T10:30:00.000Z"
}
],
"meta": { "total": 1, "page": 1, "pageSize": 10 },
"error": null
}/v1/webhook-endpoints/:idRetrieve a webhook endpoint{
"data": {
"id": "we_abc123",
"url": "https://yoursite.com/webhooks/paymenthooks",
"description": "Production webhook receiver",
"subscribedEvents": ["payment.succeeded", "payment.failed", "invoice.paid"],
"secret": "whsec_k8m2n4p6r0t1v3x5z7a9c1e3g5i7",
"status": "active",
"merchantId": "mer_abc123",
"createdAt": "2026-05-16T10:30:00.000Z",
"updatedAt": "2026-05-16T10:30:00.000Z"
},
"error": null
}/v1/webhook-endpoints/:idUpdate a webhook endpoint{
"subscribedEvents": ["payment.succeeded", "payment.failed", "invoice.paid", "subscription.canceled"],
"description": "Updated production webhook"
}{
"data": {
"id": "we_abc123",
"url": "https://yoursite.com/webhooks/paymenthooks",
"description": "Updated production webhook",
"subscribedEvents": ["payment.succeeded", "payment.failed", "invoice.paid", "subscription.canceled"],
"status": "active",
"updatedAt": "2026-05-16T11:00:00.000Z"
},
"error": null
}Event types
| Event | Description |
|---|---|
| payment.created | A new payment was created |
| payment.succeeded | A payment was successfully charged |
| payment.failed | A payment attempt failed |
| payment.refunded | A payment was refunded |
| payment.requires_action | A payment requires additional authentication |
| payment.disputed | A payment was marked as disputed |
| payment_intent.created | A payment intent was created |
| payment_intent.succeeded | A payment intent succeeded |
| payment_intent.payment_failed | A payment intent failed |
| payment_intent.requires_action | A payment intent requires action |
| charge.refunded | A charge was refunded |
| charge.dispute.created | A dispute was created on a charge |
| charge.dispute.closed | A dispute was closed |
| invoice.created | A new invoice was created |
| invoice.finalized | An invoice was finalized |
| invoice.paid | An invoice was paid |
| invoice.payment_succeeded | Invoice payment succeeded |
| invoice.payment_failed | Invoice payment failed |
| invoice.payment_reminder | An invoice payment reminder was sent |
| invoice.overdue | An invoice is past its due date |
| invoice.voided | An invoice was voided |
| invoice.reminder | An invoice payment reminder was sent |
| subscription.created | A new subscription was started |
| subscription.updated | A subscription was updated |
| subscription.cancelled | A subscription was canceled |
| subscription.renewed | A subscription renewed successfully |
| subscription.renewal_due | A subscription renewal is due |
| subscription.renewal_failed | A subscription renewal failed |
| subscription.trial_will_end | A subscription trial period is about to end |
| subscription.paused | A subscription was paused |
| subscription.resumed | A subscription was resumed |
| subscription.deleted | A subscription was deleted |
| customer.subscription.created | A customer subscription was created |
| customer.subscription.updated | A customer subscription was updated |
| customer.subscription.deleted | A customer subscription was deleted |
| customer.subscription.paused | A customer subscription was paused |
| customer.subscription.resumed | A customer subscription was resumed |
| customer.created | A new customer was created |
| customer.updated | A customer was updated |
| customer.deleted | A customer was deleted |
| product.created | A new product was created |
| product.updated | A product was updated |
| product.deleted | A product was deleted |
| price.created | A new price was created |
| price.updated | A price was updated |
| price.deleted | A price was deleted |
| dispute.created | A dispute was opened on a payment |
| dispute.updated | A dispute was updated |
| dispute.won | A dispute was won |
| dispute.lost | A dispute was lost |
| dispute.closed | A dispute was closed |
| dispute.evidence_submitted | Evidence was submitted for a dispute |
| checkout.session.created | A checkout session was created |
| checkout.session.completed | A checkout session was completed |
| checkout.session.payment_failed | A checkout session payment failed |
| checkout.session.expired | A checkout session expired |
| checkout.session.async_payment_succeeded | Async checkout payment succeeded |
| checkout.session.async_payment_failed | Async checkout payment failed |
| payment_link.created | A payment link was created |
| payment_link.updated | A payment link was updated |
| gateway.connected | A payment gateway was connected |
| gateway.disconnected | A payment gateway was disconnected |
| gateway.health_changed | A gateway health status changed |
Retry policy
If your endpoint returns a non-2xx response or times out (30 seconds), we retry with exponential backoff:
- 1After 1 minute
- 2After 5 minutes
- 3After 30 minutes
- 4After 2 hours
- 5After 24 hours