Send Pay Links
API Reference

Webhooks

Receive real-time notifications for checkout events

Webhooks

Webhooks allow you to receive real-time notifications when events occur in your Send Pay Links account.

Events

EventDescription
checkout.createdA new checkout session was created
checkout.completedCheckout was successfully completed
checkout.abandonedCheckout was abandoned (expired without completing)
payment.succeededPayment was successfully processed
payment.failedPayment processing failed
order.confirmedOrder was confirmed by payment provider
upsell.acceptedCustomer accepted an upsell offer
upsell.declinedCustomer declined an upsell offer

Webhook Payload

All webhook payloads follow this structure:

{
  "id": "evt_abc123",
  "type": "payment.succeeded",
  "timestamp": "2024-01-01T12:00:00Z",
  "data": {
    "checkoutId": "chk_xyz789",
    "orderId": "order_123",
    "amount": 9999,
    "currency": "USD",
    "customer": {
      "email": "customer@example.com",
      "firstName": "John",
      "lastName": "Doe"
    },
    "metadata": {
      "plan": "premium"
    }
  }
}

Configuring Webhooks

In the Dashboard

  1. Go to Settings > Webhooks
  2. Click Add Endpoint
  3. Enter your webhook URL
  4. Select the events you want to receive
  5. Click Save

Per-Checkout Webhooks

You can also specify a webhook URL for individual checkouts:

const token = await createCheckoutToken({
  orderId: 'order_123',
  amount: 9999,
  webhookUrl: 'https://your-server.com/webhooks/checkout',
});

Verifying Webhooks

All webhooks are signed using HMAC-SHA256. Verify the signature to ensure the webhook is authentic:

import crypto from 'crypto';

function verifyWebhook(payload: string, signature: string, secret: string): boolean {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// In your webhook handler
app.post('/webhooks/checkout', (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const payload = JSON.stringify(req.body);

  if (!verifyWebhook(payload, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  // Process the webhook
  const event = req.body;
  console.log('Received event:', event.type);

  res.status(200).send('OK');
});

Webhook Headers

Each webhook request includes these headers:

HeaderDescription
X-Webhook-IDUnique ID for this webhook delivery
X-Webhook-SignatureHMAC-SHA256 signature
X-Webhook-TimestampUnix timestamp when webhook was sent
Content-TypeAlways application/json

Retry Policy

If your endpoint returns a non-2xx response, we'll retry the webhook:

  • Retry 1: After 5 minutes
  • Retry 2: After 30 minutes
  • Retry 3: After 2 hours
  • Retry 4: After 8 hours
  • Retry 5: After 24 hours

After 5 failed attempts, the webhook is marked as failed and won't be retried.

Best Practices

Respond quickly: Return a 2xx response within 30 seconds. Process the webhook asynchronously if needed.

  1. Verify signatures - Always verify the webhook signature before processing
  2. Handle duplicates - Use the event ID to detect and handle duplicate deliveries
  3. Respond quickly - Return a 200 immediately, then process async
  4. Log everything - Log webhook payloads for debugging
  5. Use HTTPS - Always use HTTPS endpoints

Example: Complete Handler

import express from 'express';
import crypto from 'crypto';

const app = express();
app.use(express.json());

const processedEvents = new Set();

app.post('/webhooks/checkout', async (req, res) => {
  // 1. Verify signature
  const signature = req.headers['x-webhook-signature'] as string;
  const payload = JSON.stringify(req.body);

  const expectedSignature = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET!)
    .update(payload)
    .digest('hex');

  if (signature !== expectedSignature) {
    console.error('Invalid webhook signature');
    return res.status(401).send('Invalid signature');
  }

  // 2. Check for duplicates
  const event = req.body;
  if (processedEvents.has(event.id)) {
    return res.status(200).send('Already processed');
  }
  processedEvents.add(event.id);

  // 3. Respond immediately
  res.status(200).send('OK');

  // 4. Process async
  try {
    switch (event.type) {
      case 'payment.succeeded':
        await handlePaymentSuccess(event.data);
        break;
      case 'checkout.abandoned':
        await sendAbandonedCartEmail(event.data);
        break;
      default:
        console.log('Unhandled event type:', event.type);
    }
  } catch (error) {
    console.error('Error processing webhook:', error);
  }
});

async function handlePaymentSuccess(data: any) {
  // Fulfill the order, send confirmation email, etc.
  console.log('Order completed:', data.orderId);
}

async function sendAbandonedCartEmail(data: any) {
  // Send recovery email
  console.log('Abandoned cart:', data.checkoutId);
}

Testing Webhooks

Use the webhook tester in the dashboard to send test events:

  1. Go to Settings > Webhooks
  2. Click on your endpoint
  3. Click Send Test Event
  4. Select an event type
  5. Click Send

For local development, use a tunnel service like ngrok:

ngrok http 3000
# Use the ngrok URL as your webhook endpoint

On this page