Skip to Content

Webhooks

Webhooks allow you to receive real-time notifications about payment events in your account. When an event occurs (like an on-chain payment confirmation or subscription renewal), we send an HTTP POST request to your configured endpoint.

🔔

Webhooks are essential for keeping your system in sync with on-chain payment events, especially since blockchain transactions require confirmation time.

How It Works

  1. Configure your endpoint - Set up a publicly accessible URL to receive webhook events
  2. Receive events - We POST event data to your endpoint when events occur
  3. Process and respond - Your server processes the event and returns a 2xx status code
  4. Retry on failure - Failed deliveries are automatically retried

Setting Up Webhooks

Configure your webhook endpoint in your dashboard settings. You’ll need to provide:

  • Endpoint URL: A publicly accessible HTTPS URL
  • Events to subscribe: Select which events you want to receive
  • Secret key: Used to verify webhook signatures

Webhook Events

Payment Events

EventDescription
payment.succeededPayment confirmed on-chain (direct mode)
payment.failedPayment attempt failed
payment.authorizedPayment received, awaiting capture (two-step mode)
payment.capturedFunds captured to settlement wallet (two-step mode)
payment.refundedPayment refunded to customer (two-step mode)

Subscription Events

EventDescription
subscription.createdNew subscription was created
subscription.pausedSubscription was paused
subscription.past_dueRenewal payment attempt failed
subscription.cancelledSubscription was cancelled

Event Payload Structure

All webhook events follow a consistent structure:

payment.succeeded
{
  "id": "evt_1234567890abcdef",
  "object": "event",
  "type": "payment.succeeded",
  "created_at": "2024-01-15T12:30:00Z",
  "data": {
    "object": {
      "id": "pay_0987654321fedcba",
      "object": "payment",
      "amount": 2999,
      "currency": "USD",
      "status": "succeeded",
      "stablecoin": "USDC",
      "network": "ethereum",
      "tx_hash": "0x8a9c67b2d1e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9"
    }
  }
}
payment.captured
{
  "id": "evt_abcdef1234567890",
  "object": "event",
  "type": "payment.captured",
  "created_at": "2024-01-15T14:00:00Z",
  "data": {
    "object": {
      "id": "pay_0987654321fedcba",
      "object": "payment",
      "amount": 5000,
      "currency": "USD",
      "status": "captured",
      "payment_mode": "two_step",
      "stablecoin": "USDC",
      "network": "ethereum",
      "tx_hash": "0x8a9c67b2d1e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9",
      "created_at": "2024-01-15T12:30:00Z",
      "captured_at": "2024-01-15T14:00:00Z"
    }
  }
}
payment.refunded
{
  "id": "evt_fedcba0987654321",
  "object": "event",
  "type": "payment.refunded",
  "created_at": "2024-01-20T15:30:00Z",
  "data": {
    "object": {
      "id": "pay_0987654321fedcba",
      "object": "payment",
      "amount": 5000,
      "currency": "USD",
      "status": "refunded",
      "payment_mode": "two_step",
      "stablecoin": "USDC",
      "network": "ethereum",
      "created_at": "2024-01-15T12:30:00Z",
      "refunded_at": "2024-01-20T15:30:00Z"
    }
  }
}

Verifying Webhook Signatures

Every webhook request includes a signature in the X-Signature header. Verify this signature to ensure the request came from our servers.

Signature Verification

The signature is computed using HMAC-SHA256 with your webhook secret:

import crypto from 'crypto';
 
function verifyWebhookSignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('hex');
 
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}
 
// In your webhook handler
app.post('/webhooks/payments', (req, res) => {
  const signature = req.headers['x-signature'];
  const payload = JSON.stringify(req.body);
 
  if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
 
  // Process the event
  const event = req.body;
  console.log('Received event:', event.type);
 
  res.status(200).send('OK');
});
🔐

Always verify webhook signatures in production to prevent malicious requests.

Handling Webhooks

Best Practices

  1. Return quickly: Respond with a 2xx status code within 30 seconds
  2. Process asynchronously: Queue events for background processing if needed
  3. Handle duplicates: Events may be delivered more than once; use event.id to deduplicate
  4. Verify signatures: Always verify the webhook signature before processing

Example Handler

app.post('/webhooks/payments', async (req, res) => {
  const signature = req.headers['x-signature'];
 
  // Verify signature
  if (!verifyWebhookSignature(JSON.stringify(req.body), signature, WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
 
  const event = req.body;
 
  // Handle the event
  switch (event.type) {
    case 'payment.succeeded':
      const payment = event.data.object;
      console.log('Payment confirmed:', payment.tx_hash);
      await fulfillOrder(payment.metadata.order_id);
      break;
 
    case 'payment.authorized':
      const authorizedPayment = event.data.object;
      console.log('Payment authorized:', authorizedPayment.id);
      await reserveInventory(authorizedPayment.metadata.order_id);
      break;
 
    case 'payment.captured':
      const capturedPayment = event.data.object;
      console.log('Payment captured:', capturedPayment.id);
      await fulfillOrder(capturedPayment.metadata.order_id);
      break;
 
    case 'payment.refunded':
      const refundedPayment = event.data.object;
      console.log('Payment refunded:', refundedPayment.id);
      await updateOrderStatus(refundedPayment.metadata.order_id, 'refunded');
      break;
 
    case 'subscription.created':
      const subscription = event.data.object;
      console.log('Subscription created:', subscription.id);
      await grantAccess(subscription.customer_id);
      break;
 
    case 'subscription.past_due':
      const pastDueSub = event.data.object;
      await notifyPaymentFailure(pastDueSub.customer_email);
      break;
 
    case 'subscription.cancelled':
      const cancelledSub = event.data.object;
      await revokeAccess(cancelledSub.customer_id);
      break;
 
    default:
      console.log(`Unhandled event type: ${event.type}`);
  }
 
  res.status(200).send('OK');
});

Testing Webhooks

Test Mode Events

Events from test mode transactions are sent to your configured webhook endpoint just like live events. Use test mode to verify your integration.

Local Development

For local development, use a tool like ngrok to expose your local server:

ngrok http 3000

Then configure the ngrok URL as your webhook endpoint in the dashboard.

Webhook Headers

Each webhook request includes the following headers:

HeaderDescription
Content-Typeapplication/json
X-SignatureHMAC-SHA256 signature of the request body
X-Event-IdUnique identifier for the event
X-Event-TypeThe type of event (e.g., payment.succeeded)
X-TimestampUnix timestamp when the event was created

Troubleshooting

Events not being received

  • Verify your endpoint URL is correct and publicly accessible
  • Check that your endpoint accepts POST requests
  • Ensure your firewall allows requests from our IP addresses

Signature verification failing

  • Confirm you’re using the correct webhook secret
  • Ensure you’re verifying against the raw request body (before JSON parsing)
  • Check that you’re using the correct hashing algorithm (SHA256)

Duplicate events

  • Use the event.id field to deduplicate events
  • Implement idempotent event handlers

Start building

XO

Request Demo

Schedule a call with our team

Select a product
Arrow right

Start building
Grateful

Contact Us

We're here to help