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"
}

Automatic Pix Webhooks

Automatic Pix sends two kinds of notification to the webhook URL of the recurrence:

  • Payments arrive as the PIX charge webhook above ("type": "transaction", "status": 1). Every paid cycle, and the activation payment of journey 3, is an ordinary deposit.
  • Lifecycle events ("type": "automatic_pix") report changes to the recurrence and its charges, such as recurrence.approved, charge.created or charge.expired.

See Automatic Pix notifications for every event and its full body.


Identifying Webhook Types

Use the type and method fields to identify the webhook:

typemethodDescription
transactionpixPIX charge (cash-in)
withdrawalpayout_pixPIX payout (cash-out)
automatic_pixpixAutomatic Pix lifecycle event (use resource and event)

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