SettleFlow API

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 eventE-PRO OperationTypeE-PRO Status
ATTEMPT_CAPTUREDpaymentcaptured
ATTEMPT_AUTHORIZEDpaymentpending
ATTEMPT_FAILEDpaymentfailed

Refund events

Domain eventE-PRO OperationTypeE-PRO Status
REFUND_SUCCEEDEDrefundcaptured
REFUND_FAILEDrefundfailed

Dispute / chargeback events

Domain eventE-PRO OperationTypeE-PRO Status
DISPUTE_OPENEDchargebackchargeback
DISPUTE_WONchargebackchargeback_reversed
DISPUTE_LOSTchargebackchargeback

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%3A47

Decoded, 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:47

Field mapping

FieldSource
OperationTypepayment, refund, or chargeback
StatusSee the tables above
TidYour Tid from the original payment request
ReferenceThe payment request ID
AmountAmount in major currency units (e.g. 49.99)
UserIdThe Uid supplied on the original payment
MessageHuman-readable outcome
DateServer time, YYYY-MM-DD HH:mm:ss (UTC)

HTTP headers

HeaderDescription
Content-Typeapplication/x-www-form-urlencoded
User-AgentSettleFlow/1.0
X-SettleFlow-SignatureHMAC-SHA256 signature (see below)
X-SettleFlow-TimestampUnix timestamp (seconds) of the delivery
X-SettleFlow-EventDomain 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 the X-SettleFlow-Timestamp header.
  • 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:

AttemptDelay after previous
1st retry5 seconds
2nd retry1 minute
3rd retry5 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

  1. Return 2xx fast. Acknowledge the request, then do the heavy work asynchronously.
  2. Verify signatures and timestamps. Reject anything older than 5 minutes.
  3. Deduplicate on Reference. A payment's Reference (payment request ID) is stable across retries.
  4. Treat the webhook as source of truth. Do not finalize orders solely on the synchronous response of /v1/payment/direct when 3DS is involved — wait for the webhook or the status endpoint to confirm.
  5. Keep your webhook secret secret. Rotate it if you suspect a leak.

On this page