Charge Webhook
A55 sends a POST webhook to your configured URL whenever a charge changes status. Verify the signature before processing any payload.
Payload fields
| Field | Type | Description |
|---|---|---|
charge_uuid | UUID | Unique charge identifier |
status | string | confirmed, error, refunded, chargeback |
transaction_reference | string | Acquirer transaction reference |
subscription_uuid | UUID | Subscription identifier (if recurring) |
amount | decimal | Charge amount |
currency | string | ISO 4217 currency code |
created_at | datetime | When the charge was created |
updated_at | datetime | When the status last changed |
Payload examples
- Confirmed
- Error / Declined
- Refunded
- Chargeback
{
"charge_uuid": "chg-001", "status": "confirmed",
"transaction_reference": "txn-abc-123",
"amount": "199.90", "currency": "BRL"
}
{
"charge_uuid": "chg-002", "status": "error",
"error_code": "05", "error_message": "Do not honor",
"amount": "199.90", "currency": "BRL"
}
{
"charge_uuid": "chg-003", "status": "refunded",
"amount": "199.90", "refund_amount": "199.90",
"currency": "BRL"
}
{
"charge_uuid": "chg-004", "status": "chargeback",
"amount": "199.90", "chargeback_reason": "fraud",
"currency": "BRL"
}
Handling code
- Python (Flask)
- JavaScript (Express)
from flask import Flask, request, jsonify
import hmac, hashlib
app = Flask(__name__)
processed = set()
@app.route("/webhook", methods=["POST"])
def charge_webhook():
sig, ts = request.headers["X-Webhook-Signature"], request.headers["X-Webhook-Timestamp"]
expected = hmac.new(SECRET.encode(), f"{ts}.".encode() + request.data, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
return jsonify(error="invalid signature"), 401
data = request.get_json()
if data["charge_uuid"] in processed:
return jsonify(status="duplicate"), 200
processed.add(data["charge_uuid"])
actions = {"confirmed": fulfill_order, "error": notify_customer,
"refunded": credit_customer, "chargeback": flag_for_review}
if data["status"] in actions:
actions[data["status"]](data)
return jsonify(status="accepted"), 200
const express = require("express"), crypto = require("crypto");
const app = express();
app.use(express.raw({ type: "application/json" }));
const processed = new Set();
app.post("/webhook", (req, res) => {
const sig = req.headers["x-webhook-signature"], ts = req.headers["x-webhook-timestamp"];
const expected = crypto.createHmac("sha256", SECRET).update(`${ts}.${req.body}`).digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) return res.sendStatus(401);
const data = JSON.parse(req.body);
if (processed.has(data.charge_uuid)) return res.sendStatus(200);
processed.add(data.charge_uuid);
const actions = {confirmed: fulfillOrder, error: notifyCustomer,
refunded: creditCustomer, chargeback: flagForReview};
if (actions[data.status]) actions[data.status](data);
res.sendStatus(200);
});
Best practices
| Practice | Why |
|---|---|
| Verify HMAC first | Prevents processing forged payloads |
Return 200 immediately | Avoids timeouts and duplicate deliveries |
| Process in background | Keeps response time under 30 s |
Deduplicate by charge_uuid | Handles retried webhooks safely |
| Log raw payloads | Enables debugging without re-delivery |
| Never trust redirect URL alone | Redirect can be spoofed; webhook is authoritative |