Webhooks
Receive real-time notifications when payment events occur.
Overview
Webhooks allow your application to receive automatic notifications when payment events happen. Instead of polling the API for status updates, webhooks push data to your server in real-time.
Best Practice: Always use webhooks to track payment status changes rather than relying solely on redirect URLs, which users might not complete.
How It Works
Configure Webhook URL
Set a notification_url when creating payments or checkout sessions
Payment Event Occurs
Customer completes payment, payment fails, or refund is issued
Cashier Sends POST Request
Your webhook URL receives a POST request with event data
Your Server Processes Event
Update order status, send confirmation email, fulfill order, etc.
Setting Up Webhooks
Include the notification_url parameter when creating a payment or checkout session:
{
"amount": 100.00,
"currency": "BRL",
"notification_url": "https://yoursite.com/webhooks/cashier",
...
}Webhook Events
| Event | Description |
|---|---|
payment.success | Payment completed successfully |
payment.failed | Payment failed (declined, error) |
payment.pending | Payment is pending (awaiting action) |
payment.expired | Payment expired (PIX timeout) |
payment.refunded | Payment was refunded |
checkout.completed | Checkout session completed |
checkout.expired | Checkout session expired |
Webhook Payload
All webhook events are sent as HTTP POST requests with a JSON body:
Payment Success Event
{
"event": "payment.success",
"timestamp": "2024-12-12T15:05:30Z",
"data": {
"payment_id": "pi_abc123xyz789",
"merchant_id": "merchant_001",
"amount": 100.00,
"currency": "BRL",
"status": "success",
"payment_method": "pix",
"customer": {
"name": "Maria Silva",
"email": "maria@example.com",
"document": "12345678900"
},
"metadata": {
"order_id": "12345"
},
"paid_at": "2024-12-12T15:05:30Z",
"created_at": "2024-12-12T15:00:00Z"
}
}Payment Failed Event
{
"event": "payment.failed",
"timestamp": "2024-12-12T15:05:30Z",
"data": {
"payment_id": "pi_def456uvw123",
"merchant_id": "merchant_001",
"amount": 250.00,
"currency": "BRL",
"status": "failed",
"payment_method": "card",
"error_code": "card_declined",
"error_message": "Insufficient funds",
"created_at": "2024-12-12T15:00:00Z"
}
}Handling Webhooks
Your webhook endpoint should:
- Accept POST requests with JSON body
- Return a 200 status code to acknowledge receipt
- Process events asynchronously if needed
- Be idempotent (handle duplicate events gracefully)
Node.js Example
const express = require('express');
const app = express();
app.post('/webhooks/cashier', express.json(), async (req, res) => {
const { event, data } = req.body;
// Always respond quickly to acknowledge receipt
res.status(200).send('OK');
// Process the event
switch (event) {
case 'payment.success':
await handlePaymentSuccess(data);
break;
case 'payment.failed':
await handlePaymentFailed(data);
break;
case 'checkout.completed':
await handleCheckoutCompleted(data);
break;
default:
console.log('Unhandled event:', event);
}
});
async function handlePaymentSuccess(data) {
const { payment_id, amount, metadata } = data;
// Update your order status
await updateOrder(metadata.order_id, 'paid');
// Send confirmation email
await sendConfirmationEmail(data.customer.email, data);
console.log(`Payment ${payment_id} successful: R$ ${amount}`);
}
async function handlePaymentFailed(data) {
const { payment_id, error_message } = data;
// Log the failure
console.error(`Payment ${payment_id} failed: ${error_message}`);
// Notify customer
await sendPaymentFailedEmail(data.customer.email, data);
}Python Example
from fastapi import FastAPI, Request
import asyncio
app = FastAPI()
@app.post("/webhooks/cashier")
async def handle_webhook(request: Request):
payload = await request.json()
event = payload.get("event")
data = payload.get("data")
# Process event asynchronously
asyncio.create_task(process_event(event, data))
# Return immediately
return {"status": "ok"}
async def process_event(event: str, data: dict):
if event == "payment.success":
await handle_payment_success(data)
elif event == "payment.failed":
await handle_payment_failed(data)
elif event == "checkout.completed":
await handle_checkout_completed(data)
else:
print(f"Unhandled event: {event}")
async def handle_payment_success(data: dict):
payment_id = data["payment_id"]
amount = data["amount"]
# Update order status in database
# await db.orders.update(order_id, status="paid")
print(f"Payment {payment_id} successful: R$ {amount}")Retry Policy
If your webhook endpoint doesn't respond with a 2xx status code, Cashier will retry the request with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 5 minutes |
| 2nd retry | 30 minutes |
| 3rd retry | 2 hours |
| 4th retry | 8 hours |
| 5th retry (final) | 24 hours |
Important: After 5 failed attempts, the webhook is marked as failed. Make sure your endpoint is reliable and responds within 30 seconds.
Security Best Practices
- ✓Use HTTPS for your webhook endpoint
- ✓Verify the payment status by calling the API after receiving a webhook
- ✓Make your webhook handler idempotent (same event processed once)
- ✓Store the payment_id and check for duplicates before processing
- ✓Return 200 quickly, then process the event asynchronously
- ✗Don't trust webhook data alone for critical operations - verify via API
Testing Webhooks
During development, you can use tools like ngrok or webhook.site to receive webhooks on your local machine:
# Start ngrok to expose your local server
ngrok http 3000
# Use the generated URL as your notification_url
# Example: https://abc123.ngrok.io/webhooks/cashier