Webhooks
Webhooks are automatic notifications sent by the PixToPay API to inform you about changes in the transaction lifecycle. When you provide a URL in the webhook parameter while creating a transaction, you will receive POST requests to that URL whenever the transaction status changes.
How They Work
- You create a transaction (charge or payment) with a URL in the
webhookfield - The PixToPay API monitors the transaction
- When the status changes, the API sends a
POSTto your URL - Your server processes the notification and responds
- You update your system with the new status
Configuration
Endpoint Requirements
Your webhook endpoint must:
- ✅ Accept
POSTrequests - ✅ Respond with
200 OKwithin 5 seconds - ✅ Be publicly accessible (HTTPS recommended)
- ✅ Process requests idempotently
Endpoint Example
// Node.js/Express
app.post("/webhooks/pixtopay", (req, res) => {
const webhook = req.body;
// Process webhook
console.log("Webhook received:", webhook);
// Respond quickly
res.status(200).send("OK");
// Process in the background
processWebhook(webhook);
});# Python/Flask
@app.route('/webhooks/pixtopay', methods=['POST'])
def webhook_handler():
webhook = request.json
# Process webhook
print('Webhook received:', webhook)
# Respond quickly
response = make_response('OK', 200)
# Process in the background
process_webhook.delay(webhook)
return responsePIX Charge Webhooks (Cash-in)
PIX - Paid (Status 1)
Sent when a PIX charge is paid by the customer.
{
"id": 123456789,
"transaction_id": "brand_123456789",
"currency": "BRL",
"amount": 20,
"type": "transaction",
"method": "pix",
"status": 1,
"created_at": "2025-12-16T23:54:36.000Z",
"paid_at": "2025-12-16T23:55:08.000Z",
"name": "John Cena",
"document_number": "12345678910",
"phone_number": "9999999999",
"email": "johncena@wwe.com",
"payer": { "name": "John Cena", "document_number": "12345678910" },
"e2eId": "E18236120202512170254s090902ad25",
"external_id": "",
"first_deposit": true
}Additional Fields
| Field | Type | Description |
|---|---|---|
paid_at | string | Payment date and time |
payer | object | Payer information |
payer.name | string | Payer name (from bank account) |
payer.document_number | string | Payer CPF/CNPJ |
first_deposit | boolean | true if this is the payer's first payment |
PIX - Expired (Status 3)
Sent when a PIX charge expires without being paid.
{
"id": 123456789,
"transaction_id": "brand_123456789",
"currency": "BRL",
"amount": 45,
"type": "transaction",
"method": "pix",
"status": 3,
"created_at": "2025-12-16T13:50:33.000Z",
"paid_at": null,
"name": "John Cena",
"document_number": "12345678910",
"phone_number": "9999999999",
"email": "johncena@wwe.com",
"payer": { "name": null, "document_number": null },
"e2eId": null,
"external_id": "123456789"
}PIX - Refunded (Status 4)
Sent when a paid PIX charge is refunded.
{
"id": 123456789,
"transaction_id": "brand_123456789",
"currency": "BRL",
"amount": 7.61,
"type": "transaction",
"method": "pix",
"status": 4,
"created_at": "2025-12-16T21:35:49.000Z",
"paid_at": "2025-12-16T21:36:33.000Z",
"name": "John Cena",
"document_number": "12345678910",
"phone_number": "9999999999",
"email": "johncena@wwe.com",
"payer": { "name": "John Cena", "document_number": "12345678910" },
"e2eId": "E60746948202512170036a5246dhgtda",
"external_id": "123456789"
}Payment Webhooks (Cash-out)
Payout PIX - Approved (Status 1)
Sent when a PIX payout is approved and completed.
{
"id": 123456789,
"transaction_id": "brand_123456789",
"currency": "BRL",
"amount": 316.32,
"type": "withdrawal",
"method": "payout_pix",
"status": 1,
"created_at": "2025-12-16T21:36:50.000Z",
"paid_at": "2025-12-16T21:36:52.000Z",
"name": "John Cena",
"document_number": "9999999999",
"phone_number": null,
"email": "johncena@wwe.com",
"manual_withdrawal": 0,
"external_id": "123456789"
}Payout PIX - Rejected (Status 2)
Sent when a PIX payout is rejected.
{
"id": 123456789,
"transaction_id": "brand_123456789",
"currency": "BRL",
"amount": 65.24,
"type": "withdrawal",
"method": "payout_pix",
"status": 2,
"created_at": "2025-12-16T21:39:01.000Z",
"paid_at": null,
"name": "John Cena",
"document_number": "12345678910",
"phone_number": "9999999999",
"email": "johncena@wwe.com",
"manual_withdrawal": 0,
"external_id": "123456789",
"cancel_reason": "invalid_pix_key",
"cancel_details": "Invalid PIX key"
}Payout PIX - Rejected (Status 3)
Sent when a PIX payout is rejected by the bank.
{
"id": 123456789,
"transaction_id": "brand_123456789",
"currency": "BRL",
"amount": 25,
"type": "withdrawal",
"method": "payout_pix",
"status": 3,
"created_at": "2025-12-16T23:25:53.000Z",
"paid_at": "2025-12-16T23:25:56.000Z",
"name": "John Cena",
"document_number": "12345678910",
"phone_number": null,
"email": "johncena@wwe.com",
"manual_withdrawal": 0,
"external_id": "123456789",
"cancel_reason": "refunded",
"cancel_details": "Full amount refunded"
}Identifying Webhook Types
Use the type and method fields to identify the webhook:
| type | method | Description |
|---|---|---|
transaction | pix | PIX charge (cash-in) |
withdrawal | payout_pix | PIX payout (cash-out) |
Security
1. Origin validation
Verify the request source IP:
const PIXTOPAY_IPS = ["IP_DA_PIXTOPAY"]; // Check with support
function validateOrigin(req) {
const clientIp = req.ip;
return PIXTOPAY_IPS.includes(clientIp);
}2. Data validation
Always validate incoming data:
function validateWebhook(webhook) {
return (
webhook.id &&
webhook.transaction_id &&
webhook.status !== undefined &&
webhook.type &&
webhook.method
);
}3. Idempotency
Process each webhook only once:
const processedWebhooks = new Set();
function processWebhook(webhook) {
const webhookKey = `${webhook.id}_${webhook.status}`;
if (processedWebhooks.has(webhookKey)) {
console.log("Webhook already processed");
return;
}
processedWebhooks.add(webhookKey);
// Process webhook
updateTransaction(webhook);
}4. Additional verification
After receiving a webhook, query the API to confirm:
async function verifyWebhook(webhook) {
const response = await fetch(
`https://api.pixtopay.com.br/v2/transactions?id=${webhook.id}`,
{
headers: { Authorization: "YOUR_API_KEY" },
}
);
const transaction = await response.json();
return transaction.status === webhook.status;
}Error Handling
Automatic retries
If your endpoint does not respond or returns an error, PixToPay will retry sending the webhook:
- Attempts: Up to 5 retries
- Interval: Exponential (1min, 5min, 15min, 1h, 6h)
- Timeout: 5 seconds per attempt
Robust implementation
app.post("/webhooks/pixtopay", async (req, res) => {
try {
const webhook = req.body;
// Validate
if (!validateWebhook(webhook)) {
return res.status(400).send("Invalid webhook");
}
// Respond quickly
res.status(200).send("OK");
// Process asynchronously
await processWebhookAsync(webhook);
} catch (error) {
console.error("Error processing webhook:", error);
// Still return 200 to avoid unnecessary retries
// if the error is business logic
res.status(200).send("Error processed");
}
});
async function processWebhookAsync(webhook) {
try {
// Check if already processed
const exists = await checkIfProcessed(webhook);
if (exists) return;
// Verify with API
const valid = await verifyWebhook(webhook);
if (!valid) {
console.warn("Invalid webhook");
return;
}
// Process
await updateDatabaseTransacao(webhook);
// Notify user
await notifyUser(webhook);
// Mark as processed
await markAsProcessed(webhook);
} catch (error) {
console.error("Error in async processing:", error);
// Log for later analysis
await logError(webhook, error);
}
}Use Cases
1. Real-time status updates
async function updateTransaction(webhook) {
await database.transactions.update(
{ transaction_id: webhook.transaction_id },
{
status: webhook.status,
paid_at: webhook.paid_at,
updated_at: new Date(),
}
);
}2. Customer notification
async function notifyUser(webhook) {
if (webhook.status === 1 && webhook.type === "transaction") {
await sendEmail(webhook.email, {
subject: "Payment Confirmed",
body: `Your payment of R$ ${webhook.amount} was confirmed!`,
});
}
}3. Product or service fulfillment
async function handlePayment(webhook) {
if (webhook.status === 1 && webhook.type === "transaction") {
const order = await getOrderByTransactionId(webhook.transaction_id);
// Release product
await releaseProduct(order.id);
// Send invoice
await sendInvoice(order.id);
// Notify customer
await notifyProductReady(order.user_id);
}
}4. Fraud control
async function checkFraud(webhook) {
if (webhook.first_deposit && webhook.status === 1) {
// First deposit — verify
const fraudScore = await analyzeFraud({
payer: webhook.payer,
amount: webhook.amount,
receiver: webhook.document_number,
});
if (fraudScore > 0.8) {
// High risk — refund
await refundTransaction(webhook.id);
}
}
}Testing Webhooks
Useful tools
-
webhook.site: Test webhooks without code
- URL: https://webhook.site (opens in a new tab)
- Generates a temporary URL
- View requests in real time
-
ngrok: Expose localhost publicly
ngrok http 3000 -
Postman: Simulate webhooks manually
- Configure a mock server
- Test your processing logic
Local test example
// test-webhook.js
const axios = require("axios");
async function testWebhook() {
const webhookData = {
id: 12345,
transaction_id: "test-123",
status: 1,
type: "transaction",
method: "pix",
amount: 100,
currency: "BRL",
};
try {
const response = await axios.post(
"http://localhost:3000/webhooks/pixtopay",
webhookData
);
console.log("Test successful:", response.status);
} catch (error) {
console.error("Test error:", error.message);
}
}
testWebhook();Best Practices
- ✅ Respond quickly: Maximum 5 seconds
- ✅ Process asynchronously: Use queues/workers
- ✅ Be idempotent: Handle duplicate webhooks
- ✅ Validate data: Never trust blindly
- ✅ Verify with the API: Confirm critical statuses
- ✅ Log everything: Keep a full history
- ✅ Monitor failures: Set up alerts
- ✅ Use HTTPS: Protect sensitive data
- ✅ Handle errors gracefully: Avoid unnecessary retries
- ✅ Document behavior: Ease maintenance
Troubleshooting
Webhook not arriving
- Check that the URL is publicly accessible
- Confirm the endpoint accepts POST
- Check firewall/security groups
- Test with webhook.site first
Duplicate webhooks
- Implement idempotency
- Use a unique key (id + status)
- Keep a record of processed webhooks
Timeout
- Return 200 immediately
- Process in the background
- Optimize database queries
- Use cache when possible
Support
If you have webhook issues:
- Check your server logs
- Confirm the transaction exists in the API
- Contact technical support
- Provide: transaction_id, timestamp, error logs