Webhooks Overview
A55 sends HTTP POST webhooks to your server whenever a payment event occurs, giving you real-time visibility into charges, refunds, and subscription changes.
Why webhooks
| Aspect | Webhooks (push) | Polling (pull) |
|---|---|---|
| Latency | Seconds | Minutes (depends on poll interval) |
| Reliability | Guaranteed delivery with retries | Events missed between intervals |
| Server load | Event-driven, minimal | Constant requests regardless of activity |
Configuration
| Setting | Description |
|---|---|
Per-charge webhook_url | Set on each charge request to override the default |
| Default URL | Configured in the A55 dashboard under Settings > Webhooks |
| Property | Value |
|---|---|
| Method | POST |
| Content-Type | application/json |
| Timeout | 30 seconds |
| TLS | HTTPS required |
HMAC signature verification
Every webhook includes two headers for verification:
| Header | Description |
|---|---|
X-Webhook-Signature | HMAC-SHA256 hex digest of the payload |
X-Webhook-Timestamp | Unix timestamp when the webhook was sent |
Concatenate timestamp.body (dot-separated, raw bytes), compute HMAC-SHA256 with your webhook secret, compare using a timing-safe function, and reject timestamps older than 5 minutes for replay protection.
- Python
- JavaScript
import hmac, hashlib, time
def verify_webhook(body: bytes, signature: str, timestamp: str, secret: str) -> bool:
if abs(time.time() - int(timestamp)) > 300:
return False
expected = hmac.new(
secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
const crypto = require("crypto");
function verifyWebhook(body, signature, timestamp, secret) {
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = crypto.createHmac("sha256", secret)
.update(`${timestamp}.${body}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
Verify HMAC always
Never process a webhook payload without verifying the HMAC signature. Unsigned payloads may be forged.
Retry policy
| Attempt | Delay | Cumulative time |
|---|---|---|
| 1 | Immediate | 0 |
| 2 | 1 minute | 1 min |
| 3 | 5 minutes | 6 min |
| 4 | 30 minutes | 36 min |
| 5 | 2 hours | 2 h 36 min |
| 6 | 6 hours | 8 h 36 min |
| 7 | 24 hours | 32 h 36 min |
| Your response | Retried? | |
| --- | --- | |
2xx | No | |
4xx | No (permanent rejection) | |
5xx | Yes | |
| Timeout (>30 s) | Yes | |
| Connection refused | Yes |
Idempotency
Async processing
Return 200 OK immediately after validating the signature and enqueue processing in a background job.
processed = set()
def handle_webhook(payload):
event_id = payload["charge_uuid"]
if event_id in processed:
return {"status": "already_processed"}, 200
processed.add(event_id)
queue.enqueue(process_event, payload)
return {"status": "accepted"}, 200
Event types
| Domain | Event | Trigger |
|---|---|---|
| Charges | charge.confirmed | Payment captured |
| Charges | charge.error | Payment failed or declined |
| Charges | charge.refunded | Refund completed |
| Charges | charge.chargeback | Chargeback opened |
| PIX | pix.confirmed | PIX payment received |
| PIX | pix.expired | QR code expired |
| Boleto | boleto.confirmed | Boleto paid |
| Boleto | boleto.expired | Past due date |
| Subscriptions | subscription.created | New subscription started |
| Subscriptions | subscription.cancelled | Subscription cancelled |
| Subscriptions | subscription.charge_failed | Recurring charge failed |
Testing
| Tool | Usage |
|---|---|
| webhook.site | Inspect payloads without running a server |
| ngrok | Expose local server for live sandbox webhooks |
| A55 sandbox | Send test events from Settings > Webhooks > Test |