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
- Include
webhook_urlwhen creating an invoice - Bitrefill delivers the invoice to your URL when complete
- 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:
| Status | Meaning |
|---|---|
complete | All orders processed (check individual order status) |
denied | Payment rejected |
payment_error | Payment processing failed |
A
completestatus 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_urlis 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.
Updated about 2 months ago