Webhooks — the only source of truth
How to receive, verify and process Aventopay callbacks reliably.
return_url — a user can manipulate the URL. Always wait for the signed callback.Overview
After each payment event, Aventopay sends a POST to your callback_url with a JSON payload signed HMAC-SHA256. You reply 200 OK to acknowledge; any other code triggers a retry.
Emitted statuses
| status | Description | Typical action |
|---|---|---|
success | Payment confirmed and funds credited. | Fulfill order / grant access. |
pending | In progress (3DS, processing) — another callback will follow. | Mark as pending, do not debit anything. |
cancelled | Payment cancelled by customer or expired. | Cleanup, notify UX. |
Payload
{
"event": "payment.updated",
"status": "success",
"transaction_id": "TXN_12345",
"external_user_id": "client_123",
"amount": 5000,
"currency": "EUR",
"provider_reference": "apy_ref_a1b2c3d4e5f6",
"occurred_at": "2026-07-22T14:23:11Z"
}Verify the signature
The X-Aventopay-Signature header contains the HMAC-SHA256 of the raw body, signed with your webhook_secret. Verify it before any JSON.parse — otherwise you're letting forged payloads through.
import crypto from 'crypto';
function verifyAventopaySignature(rawBody, signatureHeader, webhookSecret) {
const expected = crypto
.createHmac('sha256', webhookSecret)
.update(rawBody, 'utf8')
.digest('hex');
const a = Buffer.from(signatureHeader, 'utf8');
const b = Buffer.from(expected, 'utf8');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Idempotency
Aventopay retries on error: your handler must be idempotent. Use provider_reference as unique key — it's stable across the transaction's lifetime.
-- Simple SQL idempotency table
CREATE TABLE processed_webhooks (
reference VARCHAR(64) PRIMARY KEY,
processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- In the handler:
INSERT INTO processed_webhooks (reference) VALUES (?)
ON DUPLICATE KEY UPDATE reference = reference;
-- If the row already existed → skip the processing.Retry queue
If your server does not respond 2xx, we retry with exponential backoff:
| Attempt | Delay from 1st |
|---|---|
| 1 | Immediate |
| 2 | +30 s |
| 3 | +2 min |
| 4 | +10 min |
| 5 | +30 min |
| 6 | +1 h |
| 7 | +2 h |
| 8 | +6 h |
| — | Abandonment after 12 h |
Test locally
Use a temporary HTTPS tunnel (ngrok, cloudflared) to expose your local server. Configure callback_url with the tunnel URL. Events arrive on your localhost in real time.
Replay from the dashboard
From the Transactions view of the dashboard, a Replay webhook button lets you manually resend a callback if your system was unavailable.
