Webhooks
Receive real-time event notifications when changes occur in your YardPay account.
Event Types
Subscribe to the events relevant to your integration. Configure webhook endpoints in your YardPay dashboard under Developers → Webhooks.
| Event | Description |
|---|---|
| payment.succeeded | A payment was approved or captured |
| payment.failed | A payment was declined or errored |
| payment.refunded | A payment was refunded or reversed |
| checkout.session.completed | A hosted or embedded checkout session was paid |
| invoice.paid | An invoice was paid in full |
| invoice.payment_failed | A payment against an invoice failed |
| invoice.past_due | An invoice passed its due date unpaid |
| subscription.created | A subscription was created |
| subscription.activated | A subscription became active (first payment, trial end, or recovery from past due) |
| subscription.renewed | A subscription rolled into a new billing period and its invoice was raised |
| subscription.payment_failed | A subscription payment attempt failed (one event per dunning attempt) |
| subscription.past_due | A subscription is past due |
| subscription.unpaid | Dunning was exhausted and the subscription is unpaid |
| subscription.paused | A subscription was paused (dashboard, API or customer portal) |
| subscription.resumed | A paused subscription was resumed |
| subscription.cancelled | A subscription was cancelled. Sent when a cancellation is scheduled for the end of the period (status still active, cancelAtPeriodEnd: true,cancelAt = the period end) and again when it takes effect (status: "Canceled", canceledAt set) |
| subscription.updated | A subscription changed plan (upgrade or downgrade). Carries previousPlanId and the net proratedAmount (a charge is positive, a credit negative) |
| payout.completed | A payout to your bank account or wallet settled |
| payout.failed | A payout was rejected, cancelled or failed |
| webhook.test | A test delivery you triggered from the dashboard or API |
By default every event is delivered. To receive a subset, set subscribedEvents on your webhook configuration to a JSON array of event names. A family can be named with a trailing wildcard (payment.*) and * means all. Live-mode events go to your live URL signed with your live secret; test-mode events go to your sandbox URL signed with your sandbox secret. Payouts are always live.
Payload Format
All webhook payloads follow a consistent structure. The event is described at the top level; the thing it happened to is under data.object. created andcreatedAt carry the same timestamp.
{
"id": "evt_5f2c9a7b1d3e4c8a9b0f1e2d",
"object": "event",
"type": "checkout.session.completed",
"apiVersion": "2026-09-22",
"created": "2026-09-22T10:32:15.000Z",
"createdAt": "2026-09-22T10:32:15.000Z",
"livemode": true,
"merchantId": 1001,
"data": {
"object": {
"object": "checkout_session",
"id": "cs_live_abc123",
"sessionId": "cs_live_abc123",
"status": "Completed",
"paymentStatus": "paid",
"amount": 99.99,
"currency": "USD",
"paymentId": 48213,
"paymentReferenceNumber": "PAY-48213",
"customerEmail": "[email protected]",
"invoiceId": 7731,
"invoiceReference": "INV-7F3K2Q",
"isLive": true,
"metadata": {
"orderId": "order_12345"
},
"createdAt": "2026-09-22T10:30:02.000Z"
}
}
}Each object carries an object discriminator: payment,checkout_session, invoice,subscription, payout ortest. Payments made through a checkout session carry that session'smetadata, so payment.failed andpayment.refunded can be tied back to your order the same way.
Every subscription.* event carries the whole subscription as it stands after the change, whichever way it was made (dashboard, Gateway API or the customer portal), exactly once per change. Status transitions add previousStatus and reason. A request that is refused, or does not change anything (cancelling twice, pausing a paused subscription), sends nothing.
{
"id": "evt_8a1d0c3f7e2b4a6c9d5e1f0a",
"object": "event",
"type": "subscription.cancelled",
"apiVersion": "2026-09-22",
"created": "2026-09-25T14:03:11.000Z",
"createdAt": "2026-09-25T14:03:11.000Z",
"livemode": true,
"merchantId": 1001,
"data": {
"object": {
"object": "subscription",
"id": 812,
"clientId": 4031,
"clientEmail": "[email protected]",
"status": "Active",
"reason": "Cancelled by the customer in the billing portal",
"startDate": "2026-01-01T00:00:00.000Z",
"currentPeriodStart": "2026-09-01T00:00:00.000Z",
"currentPeriodEnd": "2026-10-01T00:00:00.000Z",
"cancelAtPeriodEnd": true,
"cancelAt": "2026-10-01T00:00:00.000Z",
"items": [
{ "id": 1, "planId": 3, "planName": "Gold", "quantity": 1, "amount": 250.0, "isMetered": false }
],
"isLive": true,
"createdAt": "2026-01-01T00:00:00.000Z"
}
}
}Signature Verification
Every webhook request includes an X-Webhook-Signature header:sha256=followed by the lower-case hex HMAC-SHA256 of the raw request body under your signing secret. Always verify it before processing the event. Because the event'sid and created are inside the signed body, reject events older than a few minutes and de-duplicate on id to defeat replays. Retries of the same event carry the same id.
Three informational headers accompany it: X-Webhook-Id (the event id),X-Webhook-Event (the event type) andX-Webhook-Timestamp (Unix seconds at send time). They are not covered by the signature; use the body.
Using @quickpay/node
import express from 'express';
import { QuickPay } from '@quickpay/node';
const app = express();
const qp = new QuickPay('sk_live_your_server_key');
app.post(
'/webhooks/quickpay',
express.raw({ type: 'application/json' }),
(req, res) => {
let event;
try {
event = qp.webhooks.constructEvent(
req.body, // Raw body (Buffer or string)
req.headers['x-webhook-signature'], // Signature from header
'whsec_your_webhook_secret' // Your signing secret
);
} catch (err) {
console.error('Signature verification failed:', err.message);
return res.status(400).json({ error: 'Invalid signature' });
}
// Process the verified event
switch (event.type) {
case 'checkout.session.completed':
handleCheckoutCompleted(event.data.object);
break;
case 'payment.succeeded':
handlePaymentSucceeded(event.data.object);
break;
case 'subscription.renewed':
handleSubscriptionRenewed(event.data.object);
break;
case 'subscription.payment_failed':
handlePaymentFailed(event.data.object);
break;
}
// Return 200 to acknowledge receipt
res.json({ received: true });
}
);Manual Verification
If you're not using the Node SDK, verify the signature manually using HMAC-SHA256:
import crypto from 'crypto';
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
if (signature.length !== expectedSignature.length) return false;
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}Security
Always use crypto.timingSafeEqual (or equivalent) for signature comparison to prevent timing attacks. Never use === for signature verification.
Retry Policy
If your endpoint does not respond with a 2xx status code within 30 seconds, YardPay will retry the delivery on this schedule:
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry | 8 hours |
| 6th retry (final) | 24 hours |
After the initial attempt and 6 retries (about 34.5 hours in total) the delivery is dead-lettered. Every attempt is visible under Developers → Webhooks → Deliveries, and a failed or dead-lettered delivery can be sent again from there at any time; it goes to your current URL under your current secret.
Best practices
- Return a 200 response immediately, then process the event asynchronously
- Handle duplicate events idempotently (use the event
idfor deduplication) - Log all incoming webhook events for debugging
- Monitor your webhook endpoint's error rate in the YardPay dashboard
Testing Webhooks
Send a webhook.test event to your endpoint from Developers → Webhooks → Send test event, or directly from the API. It goes to your sandbox URL unless you ask for live, is signed like any other event, ignores your subscription filter, and shows up in the delivery log.
curl -X POST https://api.quickpay.example/v1/WebhookConfiguration/{id}/SendTestEvent \
-H "Authorization: Bearer <access token>" \
-H "Content-Type: application/json" \
-d '{ "live": false, "eventType": "payment.succeeded" }'