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.

EventDescription
payment.succeededA payment was approved or captured
payment.failedA payment was declined or errored
payment.refundedA payment was refunded or reversed
checkout.session.completedA hosted or embedded checkout session was paid
invoice.paidAn invoice was paid in full
invoice.payment_failedA payment against an invoice failed
invoice.past_dueAn invoice passed its due date unpaid
subscription.createdA subscription was created
subscription.activatedA subscription became active (first payment, trial end, or recovery from past due)
subscription.renewedA subscription rolled into a new billing period and its invoice was raised
subscription.payment_failedA subscription payment attempt failed (one event per dunning attempt)
subscription.past_dueA subscription is past due
subscription.unpaidDunning was exhausted and the subscription is unpaid
subscription.pausedA subscription was paused (dashboard, API or customer portal)
subscription.resumedA paused subscription was resumed
subscription.cancelledA 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.updatedA subscription changed plan (upgrade or downgrade). Carries previousPlanId and the net proratedAmount (a charge is positive, a credit negative)
payout.completedA payout to your bank account or wallet settled
payout.failedA payout was rejected, cancelled or failed
webhook.testA 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

webhook-handler.ts
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:

manual-verification.ts
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:

AttemptDelay
1st retry1 minute
2nd retry5 minutes
3rd retry30 minutes
4th retry2 hours
5th retry8 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 id for 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" }'