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

1

Configure Webhook URL

Set a notification_url when creating payments or checkout sessions

2

Payment Event Occurs

Customer completes payment, payment fails, or refund is issued

3

Cashier Sends POST Request

Your webhook URL receives a POST request with event data

4

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:

Include notification_url
json
{
  "amount": 100.00,
  "currency": "BRL",
  "notification_url": "https://yoursite.com/webhooks/cashier",
  ...
}

Webhook Events

EventDescription
payment.successPayment completed successfully
payment.failedPayment failed (declined, error)
payment.pendingPayment is pending (awaiting action)
payment.expiredPayment expired (PIX timeout)
payment.refundedPayment was refunded
checkout.completedCheckout session completed
checkout.expiredCheckout session expired

Webhook Payload

All webhook events are sent as HTTP POST requests with a JSON body:

Payment Success Event

payment.success
json
{
  "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

payment.failed
json
{
  "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

Express.js Webhook Handler
javascript
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

FastAPI Webhook Handler
python
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:

AttemptDelay
1st retry5 minutes
2nd retry30 minutes
3rd retry2 hours
4th retry8 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:

Testing with ngrok
bash
# 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