Handling callbacks
Saq sends real-time callbacks to your callbackUrl every time a transaction changes status. The main rule: respond 2xx within 5 seconds. Anyone who doesn't respond quickly ends up in the retry queue (up to 72 attempts, exponential backoff).
Recommended pattern: enqueue and return 2xx
Do heavy processing outside the handler. The handler only receives, enqueues, and responds.
import express from 'express';
const app = express();
app.post('/webhooks/saq', express.json(), async (req, res) => {
await queue.enqueue('saq-callback', req.body);
res.status(204).end();
});from flask import Flask, request
app = Flask(__name__)
@app.post('/webhooks/saq')
def saq_webhook():
queue.enqueue('saq_callback', request.get_json())
return '', 204http.HandleFunc("/webhooks/saq", func(w http.ResponseWriter, r *http.Request) {
var payload map[string]any
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
queue.Enqueue("saq-callback", payload)
w.WriteHeader(http.StatusNoContent)
})<?php
$payload = json_decode(file_get_contents('php://input'), true);
$queue->enqueue('saq-callback', $payload);
http_response_code(204);Testing locally
Expose your localhost via ngrok or Cloudflare Tunnel and trigger the payload manually:
curl -X POST https://your-tunnel.ngrok.io/webhooks/saq \
-H "Content-Type: application/json" \
-d '{
"id": "SAQ20251123104518DF75D20A8F",
"type": "DEPOSIT",
"status": "COMPLETED",
"amount": 99.90,
"clientReference": "order-1234",
"virtualAccount": "loja-rj-01",
"paidAt": "2025-11-23T10:46:26.986Z"
}'await fetch('https://your-tunnel.ngrok.io/webhooks/saq', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: 'SAQ20251123104518DF75D20A8F',
type: 'DEPOSIT',
status: 'COMPLETED',
amount: 99.90,
clientReference: 'order-1234',
virtualAccount: 'loja-rj-01',
paidAt: '2025-11-23T10:46:26.986Z',
}),
});import requests
requests.post(
'https://your-tunnel.ngrok.io/webhooks/saq',
headers={'Content-Type': 'application/json'},
json={
'id': 'SAQ20251123104518DF75D20A8F',
'type': 'DEPOSIT',
'status': 'COMPLETED',
'amount': 99.90,
'clientReference': 'order-1234',
'virtualAccount': 'loja-rj-01',
'paidAt': '2025-11-23T10:46:26.986Z',
},
)Resending a real callback
To reprocess a callback that failed on your side (after fixing the handler), use the resend endpoints:
POST /user/callbacks/resend/{transactionId}, a specific transactionPOST /user/callbacks/resend, in batch
Inspecting history
Saq stores every delivery attempt. Useful for investigating failures:
GET /user/callbacks, paginated listGET /user/callbacks/{id}, detail with status code, response body, response time
Common pitfalls
| Pitfall | Symptom |
|---|---|
| Synchronous processing in the handler | Timeouts, retries, double bookkeeping |
| Returning 4xx due to internal validation error | Saq doesn't retry, callback lost |
Dedupe by id only | Refund (REFUNDED) is ignored |
| Endpoint with no IP protection | Endpoint may receive forged payloads |
Logger prints payload without masking payerDocument | LGPD risk |
Multi-tenant com virtualAccount
Uma só conta Saq pode atender N tenants (lojas, filiais, marketplaces) usando virtualAccount. Volta em todo callback, permite filtrar listagens e dispensa criar contas filhas.
Paginação
Endpoints de listagem (transações, callbacks, infrações) usam paginação clássica por page + limit com flag hasNextPage. Para janelas grandes prefira o relatório assíncrono em CSV.