Webhooks
Receive asynchronous E-PRO-formatted notifications for payment events
Overview
We notify your server asynchronously when a payment, refund or chargeback reaches a final state.
The delivered payload uses the E-PRO "standard response" parameters, sent as a form POST (application/x-www-form-urlencoded) exactly like the legacy E-PRO notifier — so it plugs straight into an existing E-PRO integration that reads the fields from $_POST.
Configuring the webhook endpoint
Webhooks are configured at the account level, in the merchant dashboard — not per payment request. In the dashboard Configuration section you register the endpoint URL (and the events you want), separately for each mode: a Sandbox endpoint receives only sandbox events, a Live endpoint only live events. See Live & Sandbox.
Once an endpoint is configured for a mode, every matching event is posted to it — no per-request parameter is needed.
Requirements:
- HTTPS only in production.
- Must respond with HTTP 2xx within 10 seconds.
- Should be idempotent — webhooks may be delivered more than once.
Delivered events
V1 delivers these event types, all serialized to E-PRO format:
Payment events
| Domain event | E-PRO OperationType | E-PRO Status |
|---|---|---|
ATTEMPT_CAPTURED | payment | captured |
ATTEMPT_AUTHORIZED | payment | pending |
ATTEMPT_FAILED | payment | failed |
Refund events
| Domain event | E-PRO OperationType | E-PRO Status |
|---|---|---|
REFUND_SUCCEEDED | refund | captured |
REFUND_FAILED | refund | failed |
Dispute / chargeback events
| Domain event | E-PRO OperationType | E-PRO Status |
|---|---|---|
DISPUTE_OPENED | chargeback | chargeback |
DISPUTE_WON | chargeback | chargeback_reversed |
DISPUTE_LOST | chargeback | chargeback |
Payload
The notification is delivered as an application/x-www-form-urlencoded POST. The standard-response parameters are sent flat in the body — there is no Code / Result wrapper (that envelope is only used by the synchronous API responses). This matches the legacy E-PRO push notification, so PHP integrations can read the fields straight from $_POST.
OperationType=payment&Status=captured&Tid=order-2026-001&Reference=pr_abc123&Amount=49.99&UserId=customer-42&Message=Payment+was+successful&Date=2026-04-22+14%3A30%3A47Decoded, the fields are:
OperationType = payment
Status = captured
Tid = order-2026-001
Reference = pr_abc123
Amount = 49.99
UserId = customer-42
Message = Payment was successful
Date = 2026-04-22 14:30:47Field mapping
| Field | Source |
|---|---|
OperationType | payment, refund, or chargeback |
Status | See the tables above |
Tid | Your Tid from the original payment request |
Reference | The payment request ID |
Amount | Amount in major currency units (e.g. 49.99) |
UserId | The Uid supplied on the original payment |
Message | Human-readable outcome |
Date | Server time, YYYY-MM-DD HH:mm:ss (UTC) |
HTTP headers
| Header | Description |
|---|---|
Content-Type | application/x-www-form-urlencoded |
User-Agent | SettleFlow/1.0 |
X-SettleFlow-Signature | HMAC-SHA256 signature (see below) |
X-SettleFlow-Timestamp | Unix timestamp (seconds) of the delivery |
X-SettleFlow-Event | Domain event type (e.g. ATTEMPT_CAPTURED) |
Signature verification
Every webhook is signed with your webhook secret (starts with whsec_). Verify signatures on every request — it's the only way to confirm a payload actually came from us.
Signature scheme
signature = "sha256=" + hex( HMAC-SHA256(webhookSecret, timestamp + "." + rawBody) )Where:
webhookSecret— your app's webhook secret (whsec_...).timestamp— value from theX-SettleFlow-Timestampheader.rawBody— the request body as a raw string (capture it before form-parsing; do not reconstruct it from the parsed fields).
Node.js example
import crypto from "node:crypto";
function verifyWebhook(secret, signature, timestamp, rawBody) {
// 1. Reject timestamps older than 5 minutes to prevent replay.
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - Number(timestamp)) > 300) {
throw new Error("Webhook timestamp too old");
}
// 2. Compute the expected signature.
const data = `${timestamp}.${rawBody}`;
const expected = crypto.createHmac("sha256", secret).update(data).digest("hex");
// 3. Timing-safe compare.
const provided = signature.replace(/^sha256=/, "");
const a = Buffer.from(provided, "hex");
const b = Buffer.from(expected, "hex");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
throw new Error("Invalid webhook signature");
}
}
// Express / Hono example (requires raw body middleware)
app.post("/webhooks/settleflow", (req, res) => {
try {
verifyWebhook(
process.env.SETTLEFLOW_WEBHOOK_SECRET,
req.headers["x-settleflow-signature"],
req.headers["x-settleflow-timestamp"],
req.rawBody,
);
} catch {
return res.status(401).send("Invalid signature");
}
// Body is form-urlencoded — parse the flat fields from the raw string.
const params = new URLSearchParams(req.rawBody);
// …handle params.get("Status")…
res.status(200).send("OK");
});PHP example
<?php
function verify_webhook(string $secret, string $signature, string $timestamp, string $rawBody): bool {
if (abs(time() - (int)$timestamp) > 300) {
return false; // replay
}
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
$provided = preg_replace('/^sha256=/', '', $signature);
return hash_equals($expected, $provided);
}
$raw = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_SETTLEFLOW_SIGNATURE'] ?? '';
$ts = $_SERVER['HTTP_X_SETTLEFLOW_TIMESTAMP'] ?? '';
if (!verify_webhook(getenv('SETTLEFLOW_WEBHOOK_SECRET'), $sig, $ts, $raw)) {
http_response_code(401);
exit('Invalid signature');
}
// Body is form-urlencoded — the fields are available directly in $_POST.
// …handle $_POST['Status']…
http_response_code(200);
echo 'OK';Retry policy
A webhook delivery is considered failed when your endpoint returns a non-2xx status, times out after 10 seconds, or is unreachable. We retry up to 3 times with increasing delays:
| Attempt | Delay after previous |
|---|---|
| 1st retry | 5 seconds |
| 2nd retry | 1 minute |
| 3rd retry | 5 minutes |
After the third retry, the webhook is marked FAILED. The merchant dashboard exposes a delivery log where you can inspect payloads and response status codes.
Best practices
- Return 2xx fast. Acknowledge the request, then do the heavy work asynchronously.
- Verify signatures and timestamps. Reject anything older than 5 minutes.
- Deduplicate on
Reference. A payment'sReference(payment request ID) is stable across retries. - Treat the webhook as source of truth. Do not finalize orders solely on the synchronous response of
/v1/payment/directwhen 3DS is involved — wait for the webhook or the status endpoint to confirm. - Keep your webhook secret secret. Rotate it if you suspect a leak.