Webhooks

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

  1. You create a transaction (charge or payment) with a URL in the webhook field
  2. The PixToPay API monitors the transaction
  3. When the status changes, the API sends a POST to your URL
  4. Your server processes the notification and responds
  5. You update your system with the new status

Configuration

Endpoint Requirements

Your webhook endpoint must:

  • ✅ Accept POST requests
  • ✅ Respond with 200 OK within 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 response

PIX 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

FieldTypeDescription
paid_atstringPayment date and time
payerobjectPayer information
payer.namestringPayer name (from bank account)
payer.document_numberstringPayer CPF/CNPJ
first_depositbooleantrue 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:

typemethodDescription
transactionpixPIX charge (cash-in)
withdrawalpayout_pixPIX 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

  1. webhook.site: Test webhooks without code

  2. ngrok: Expose localhost publicly

    ngrok http 3000
  3. 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

  1. Respond quickly: Maximum 5 seconds
  2. Process asynchronously: Use queues/workers
  3. Be idempotent: Handle duplicate webhooks
  4. Validate data: Never trust blindly
  5. Verify with the API: Confirm critical statuses
  6. Log everything: Keep a full history
  7. Monitor failures: Set up alerts
  8. Use HTTPS: Protect sensitive data
  9. Handle errors gracefully: Avoid unnecessary retries
  10. Document behavior: Ease maintenance

Troubleshooting

Webhook not arriving

  1. Check that the URL is publicly accessible
  2. Confirm the endpoint accepts POST
  3. Check firewall/security groups
  4. Test with webhook.site first

Duplicate webhooks

  1. Implement idempotency
  2. Use a unique key (id + status)
  3. Keep a record of processed webhooks

Timeout

  1. Return 200 immediately
  2. Process in the background
  3. Optimize database queries
  4. Use cache when possible

Support

If you have webhook issues:

  1. Check your server logs
  2. Confirm the transaction exists in the API
  3. Contact technical support
  4. Provide: transaction_id, timestamp, error logs