Webhooks

Receive real-time notifications when invoices are completed.

Webhooks notify your server when an invoice reaches its final state. Instead of polling for updates, you receive a push notification with the completed invoice.

How It Works

  1. Include webhook_url when creating an invoice
  2. Bitrefill delivers the invoice to your URL when complete
  3. Your server processes the webhook and retrieves order details
sequenceDiagram
    participant Your Server
    participant Bitrefill
    Your Server->>Bitrefill: POST /invoices (with webhook_url)
    Bitrefill-->>Your Server: Invoice created
    Note over Bitrefill: Payment received...
    Note over Bitrefill: Orders delivered...
    Bitrefill->>Your Server: POST webhook_url (invoice payload)
    Your Server-->>Bitrefill: 200 OK
    Your Server->>Bitrefill: GET /orders/{id}
    Bitrefill-->>Your Server: Order with redemption_info

Setting Up Webhooks

Include webhook_url when creating an invoice:

const response = await fetch(`${BASE_URL}/invoices`, {
  method: 'POST',
  headers,
  body: JSON.stringify({
    products: [{ product_id: 'amazon-us', value: 50 }],
    payment_method: 'balance',
    webhook_url: 'https://your-server.com/webhooks/bitrefill',
    auto_pay: true
  })
});

Webhook Payload

When the invoice reaches a final state, Bitrefill POSTs the full invoice object. See Core Concepts for status definitions.

Invoice Statuses

The webhook is sent when the invoice reaches one of these final states:

StatusMeaning
completeAll orders processed (check individual order status)
deniedPayment rejected
payment_errorPayment processing failed

A complete status means the invoice is finalized, not that all orders succeeded. Always check individual order statuses.

Implementing Your Webhook Endpoint

const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhooks/bitrefill', async (req, res) => {
  const invoice = req.body;
  
  // Respond immediately
  res.status(200).send('OK');
  
  // Process asynchronously
  processInvoice(invoice);
});

async function processInvoice(invoice) {
  console.log(`Invoice ${invoice.id} status: ${invoice.status}`);
  
  for (const order of invoice.orders) {
    if (order.status === 'delivered') {
      const orderDetails = await fetchOrder(order.id);
      await deliverToUser(orderDetails);
    } else {
      await handleFailedOrder(order);
    }
  }
}

Best Practices

1. Respond Quickly

Return a 2xx response within 5 seconds. Process the webhook asynchronously:

app.post('/webhooks/bitrefill', (req, res) => {
  res.status(200).send('OK');
  queue.add('process-invoice', req.body);
});

2. Handle Retries (Idempotency)

Bitrefill retries failed webhook deliveries. Ensure your handler is idempotent:

async function processInvoice(invoice) {
  const existing = await db.invoices.findOne({ id: invoice.id });
  if (existing) {
    console.log(`Invoice ${invoice.id} already processed`);
    return;
  }
  
  await handleInvoice(invoice);
  await db.invoices.insertOne({ id: invoice.id, processedAt: new Date() });
}

3. Handle Partial Failures

Some orders may fail while others succeed:

for (const order of invoice.orders) {
  switch (order.status) {
    case 'delivered':
      await deliverToUser(order);
      break;
    case 'failed':
      await refundUser(order);
      await notifySupport(order);
      break;
    case 'refunded':
      await updateUserBalance(order);
      break;
  }
}

4. Log Everything

Log webhook payloads for debugging.

Troubleshooting

Webhook not received
  • Verify your webhook_url is publicly accessible
  • Check your server logs for incoming requests
  • Ensure your server accepts POST requests at the URL
  • Verify there's no firewall blocking Bitrefill's servers
Receiving duplicate webhooks

Retries can cause duplicates. Make your handler idempotent by storing processed invoice IDs.

Webhook timing out
  • Respond with 200 OK before processing
  • Use a message queue for async processing

Fallback: Polling

If webhooks don't work for your architecture, poll for status:

async function pollInvoice(invoiceId, maxAttempts = 60) {
  for (let i = 0; i < maxAttempts; i++) {
    const invoice = await fetchInvoice(invoiceId);
    
    if (['complete', 'denied', 'payment_error'].includes(invoice.status)) {
      return invoice;
    }
    
    await sleep(5000);
  }
  
  throw new Error('Invoice did not complete in time');
}

Webhooks are preferred over polling for better reliability and faster notifications.