Webhooks
Receive real-time notifications when payment status changes.
Overview
When you create a checkout session with a notification_url, we send webhook notifications to that URL when the payment status changes.
Important
Always verify webhook payloads and respond with HTTP 200 within 30 seconds to acknowledge receipt.
Matching webhooks to your orders
Send an order_number when creating the checkout session, and it will be returned as reference_id in all webhooks for that payment. This lets you match webhook notifications to your internal orders without storing our payment_id.
Webhook Payload
All webhooks are sent as POST requests with JSON body:
POST https://your-server.com/webhooks
Content-Type: application/json
X-Signature: a1b2c3d4e5f6...
{
"event": "payment.success",
"payment_id": "pi_abc123xyz",
"checkout_session_id": "cs_abc123xyz",
"merchant_id": "your_merchant_id",
"reference_id": "ORD-12345",
"status": "success",
"amount": 150.00,
"currency": "BRL",
"country": "BRA",
"payment_method": "pix",
"paid_at": "2026-01-04T12:30:00Z",
"metadata": {
"custom_field": "your_value"
},
"timestamp": "2026-01-04T12:30:01Z"
}Event Types
payment.successPayment completed successfully
payment.failedPayment failed or was declined
payment.pendingPayment is pending (e.g., awaiting PIX confirmation)
payment.processingPayment is being processed
payment.cancelledPayment was cancelled by customer
Payload Fields
| Field | Type | Description |
|---|---|---|
event | string | Event type (see above) |
payment_id | string | Our payment identifier (pi_xxx) |
checkout_session_id | string | Checkout session identifier (cs_xxx) |
merchant_id | string | Your merchant identifier |
reference_id | string | null | Your order_number from the checkout session creation. Use this to match webhooks to your orders. Will be null if not provided. |
status | string | Payment status |
amount | number | Transaction amount |
currency | string | Currency code (BRL, PEN, COP, USD, etc.) |
country | string | Country code (BRA, PER, COL, ECU, etc.) |
paid_at | string | ISO 8601 timestamp when paid (success only) |
error_code | string | Normalized error code (failed only). E.g., invalid_account, insufficient_funds |
error_message | string | Human-readable error description (failed only) |
payment_method | string | Payment method type (pix, card, boleto, etc.) |
metadata | object | Your custom metadata from checkout session |
timestamp | string | ISO 8601 timestamp of the webhook |
Example: Success Webhook
{
"event": "payment.success",
"payment_id": "pi_abc123xyz",
"checkout_session_id": "cs_abc123xyz",
"merchant_id": "your_merchant_id",
"reference_id": "ORD-12345",
"status": "success",
"amount": 150.00,
"currency": "BRL",
"country": "BRA",
"payment_method": "pix",
"paid_at": "2026-01-04T12:30:00Z",
"timestamp": "2026-01-04T12:30:01Z"
}Example: Failed Webhook
{
"event": "payment.failed",
"payment_id": "pi_abc123xyz",
"checkout_session_id": "cs_abc123xyz",
"merchant_id": "your_merchant_id",
"reference_id": "ORD-12345",
"status": "failed",
"amount": 150.00,
"currency": "BRL",
"country": "BRA",
"payment_method": "card",
"error_code": "card_declined",
"error_message": "Card was declined by issuer",
"timestamp": "2026-01-04T12:30:01Z"
}Verifying Webhooks
All webhooks include an X-Signature header with an HMAC-SHA256 signature that you can use to verify authenticity:
Headers
| Header | Description |
|---|---|
X-Signature | HMAC-SHA256 signature of the request body |
X-Signature-Algorithm | Always "HMAC-SHA256" |
Verification (Python)
1 import hmac 2 import hashlib 3 4 def verify_webhook(payload: bytes, webhook_secret: str, received_signature: str) -> bool: 5 """ 6 Verify webhook signature using HMAC-SHA256. 7 8 Args: 9 payload: Raw request body bytes 10 webhook_secret: Your webhook secret from dashboard 11 received_signature: X-Signature header value 12 """ 13 expected = hmac.new( 14 webhook_secret.encode('utf-8'), 15 payload, 16 hashlib.sha256 17 ).hexdigest() 18 return hmac.compare_digest(expected, received_signature) 19 20 # Usage in Flask/FastAPI: 21 @app.post("/webhooks/cashier") 22 async def handle_webhook(request: Request): 23 payload = await request.body() 24 signature = request.headers.get("X-Signature") 25 26 if not verify_webhook(payload, WEBHOOK_SECRET, signature): 27 raise HTTPException(status_code=401, detail="Invalid signature") 28 29 # Process webhook...
Verification (Node.js)
1 const crypto = require('crypto'); 2 3 function verifyWebhook(payload, webhookSecret, receivedSignature) { 4 const expected = crypto 5 .createHmac('sha256', webhookSecret) 6 .update(payload) 7 .digest('hex'); 8 return crypto.timingSafeEqual( 9 Buffer.from(expected), 10 Buffer.from(receivedSignature) 11 ); 12 } 13 14 // Usage in Express: 15 app.post('/webhooks/cashier', express.raw({type: '*/*'}), (req, res) => { 16 const signature = req.headers['x-signature']; 17 18 if (!verifyWebhook(req.body, WEBHOOK_SECRET, signature)) { 19 return res.status(401).send('Invalid signature'); 20 } 21 22 const payload = JSON.parse(req.body); 23 // Process webhook... 24 });
Important: Signature verification
The signature is computed over the raw request body as received (compact JSON without spaces). Always verify against the raw bytes, not a re-parsed/re-serialized version of the payload.
Tip
Get your webhook_secret from the Dashboard under Configuration → Webhooks.
Handling Webhooks
Best Practices
Example Handler (Node.js)
1 app.post('/webhooks/cashier', async (req, res) => { 2 const { event, payment_id, reference_id, status } = req.body; 3 4 // Acknowledge immediately 5 res.status(200).send('OK'); 6 7 // Process webhook 8 if (event === 'payment.success') { 9 await updateOrderStatus(reference_id, 'paid'); 10 await sendConfirmationEmail(reference_id); 11 } else if (event === 'payment.failed') { 12 await updateOrderStatus(reference_id, 'payment_failed'); 13 } 14 });
Example Handler (Python)
1 @app.post("/webhooks/cashier") 2 async def handle_webhook(payload: dict): 3 event = payload.get("event") 4 payment_id = payload.get("payment_id") 5 reference_id = payload.get("reference_id") 6 7 if event == "payment.success": 8 await update_order_status(reference_id, "paid") 9 await send_confirmation_email(reference_id) 10 elif event == "payment.failed": 11 await update_order_status(reference_id, "payment_failed") 12 13 return {"status": "received"}
Retry Policy
If your server doesn't respond with HTTP 200-299, we retry the webhook:
After 5 failed attempts, the webhook is marked as failed and no more retries are sent.
Testing Webhooks
Use tools like webhook.site or ngrok to test webhook integration during development.