Events

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:

webhook-payload.json
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.success

Payment completed successfully

payment.failed

Payment failed or was declined

payment.pending

Payment is pending (e.g., awaiting PIX confirmation)

payment.processing

Payment is being processed

payment.cancelled

Payment was cancelled by customer

Payload Fields

FieldTypeDescription
eventstringEvent type (see above)
payment_idstringOur payment identifier (pi_xxx)
checkout_session_idstringCheckout session identifier (cs_xxx)
merchant_idstringYour merchant identifier
reference_idstring | nullYour order_number from the checkout session creation. Use this to match webhooks to your orders. Will be null if not provided.
statusstringPayment status
amountnumberTransaction amount
currencystringCurrency code (BRL, PEN, COP, USD, etc.)
countrystringCountry code (BRA, PER, COL, ECU, etc.)
paid_atstringISO 8601 timestamp when paid (success only)
error_codestringNormalized error code (failed only). E.g., invalid_account, insufficient_funds
error_messagestringHuman-readable error description (failed only)
payment_methodstringPayment method type (pix, card, boleto, etc.)
metadataobjectYour custom metadata from checkout session
timestampstringISO 8601 timestamp of the webhook

Example: Success Webhook

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

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

HeaderDescription
X-SignatureHMAC-SHA256 signature of the request body
X-Signature-AlgorithmAlways "HMAC-SHA256"

Verification (Python)

verify_webhook.py
1import hmac
2import hashlib
3
4def 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")
22async 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)

verify_webhook.js
1const crypto = require('crypto');
2
3function 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:
15app.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

1
Respond quickly - Return HTTP 200 within 30 seconds
2
Process asynchronously - Queue webhooks for later processing if needed
3
Handle duplicates - Use payment_id to deduplicate
4
Verify signature - Check X-Signature header to confirm authenticity

Example Handler (Node.js)

handler.js
1app.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)

handler.py
1@app.post("/webhooks/cashier")
2async 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:

Retry Schedule
1st
1 min
2nd
5 min
3rd
15 min
4th
1 hour
5th
4 hours

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.