Skip to content

Guides

Webhooks

When a payment completes, fails, is refunded or charged back, e-Payouts sends a JSON POST to the module's notification URL (your merchant panel → Sites → module). It is the only valid confirmation of a payment.

When it is sent

A notification is sent automatically in these cases, always with the same format:

event / statusWhenWhat to do
payment.completed
completed
The customer paid and the provider confirmed it.If sandbox is false and amount and currency match your order, mark it paid and deliver it.
payment.failed
failed
The payment was declined or not completed (e.g. the bank declined it or the customer did not confirm in the app).Do not deliver the order. If you want, offer to pay again: create a new payment (you may reuse the ucode; it will get another code).
payment.refunded
refunded · completed
A refund of the payment was made (requested by you or by support). status is refunded when the full amount was returned, or completed when partial. Details in adjustment.Record the refund on your order (adjustment.amount). The amount has already been deducted from your balance.
payment.charged_back
charged_back · completed
The customer disputed the charge with their bank and the chargeback was processed. status is charged_back when it covered the full amount, or completed when partial.Treat it as an unrequested refund: if you have not delivered the order yet, do not deliver it.

No notifications are sent for intermediate statuses (created, pending), nor when a refund is requested: only once it is done. Refunds and chargebacks can also be queried with GET /v2/adjustments, and we let you know by email.

Manual resend (your merchant panel → Payments → details): repeats the payment's last notification exactly as it was sent (same event and status). If the payment never had a notification, it sends one with its current status, which can be any of the API statuses (e.g. payment.pending or payment.refunded).

Fields

The body is a JSON object (Content-Type: application/json) with these fields, always in this order:

FieldTypeDescription
eventstringWhat happened: payment.completed, payment.failed, payment.refunded or payment.charged_back. Also sent in the X-Epayouts-Event header.
sandboxbooleantrue for test payments: never deliver a real order for a notification with sandbox true.
codestringPayment code (UUID): the same one the API returns. Use it to get the details: GET /v2/payments/{code}.
ucodestring | nullYour order code, as received by the paymentbox; null if you did not send one.
methodstringMethod the payment was started with (the code your integration uses): process the notification with this one.
effective_methodstringMethod that actually charged it. It differs from method only when that method is routed (e.g. started as ppal and charged with 16cards); informational.
amountstringAmount you requested, with two decimals and a dot ("49.90"). Compare it with your order.
currencystringCurrency you requested (ISO 4217).
statusstringPayment status at the time of the notification: completed, failed, refunded or charged_back (on partial refunds and chargebacks it stays completed).
created_atstringWhen the payment was created (ISO 8601, UTC). It is not the notification time.
charged_amountstringOnly when the chosen method does not accept your currency (e.g. MB WAY and Multibanco charge in EUR): what the customer was charged, at the rate of the moment. Do not use it to validate the order.
charged_currencystringCurrency of charged_amount (only together with charged_amount).
adjustmentobjectOnly in payment.refunded and payment.charged_back: id (the one in GET /v2/adjustments), type (refund | chargeback), amount and currency (what was refunded or disputed, in the charged currency) and reason (or null).

Decide what to do based on event, not status: on a partial refund or chargeback, status stays completed. The notification does not include customer data, commission or settlement: if you need them, fetch the payment by code.

Examples

Completed payment

JSON
{
  "event": "payment.completed",
  "sandbox": false,
  "code":        "3f6b2d10-9a4c-4e21-8b7f-5c0d1e2f3a4b",
  "ucode": "PEDIDO-1042",
  "method": "sofort",
  "effective_method": "sofort",
  "amount": "49.90",
  "currency": "EUR",
  "status": "completed",
  "created_at": "2026-09-18T14:33:22+00:00"
}

Failed payment

JSON
{
  "event": "payment.failed",
  "sandbox": false,
  "code":        "5e7a9c1b-3d5f-4a2c-8e4b-6d8f0a2c4e6a",
  "ucode": "PEDIDO-1044",
  "method": "ppal",
  "effective_method": "ppal",
  "amount": "35.00",
  "currency": "EUR",
  "status": "failed",
  "created_at": "2026-09-29T09:12:05+00:00"
}

Completed payment charged in another currency

Order in USD paid with MB WAY, which charges in EUR: amount and currency are your order's; charged_amount and charged_currency, what the customer paid.

JSON
{
  "event": "payment.completed",
  "sandbox": false,
  "code":        "8a2c4e6f-1b3d-4f5a-9c7e-0d2f4a6b8c1e",
  "ucode": "PEDIDO-1043",
  "method": "mbway",
  "effective_method": "mbway",
  "amount": "12.00",
  "currency": "USD",
  "status": "completed",
  "created_at": "2026-09-30T14:35:10+00:00",
  "charged_amount": "10.65",
  "charged_currency": "EUR"
}

Payment with a routed method

Your integration uses ppal, but that method is routed and the payment was charged with 16cards: process it as ppal.

JSON
{
  "event": "payment.completed",
  "sandbox": false,
  "code":        "c2e4a6b8-0d1f-4e3a-9b5c-7d9e1f3a5b7c",
  "ucode": "PEDIDO-1045",
  "method": "ppal",
  "effective_method": "16cards",
  "amount": "20.00",
  "currency": "EUR",
  "status": "completed",
  "created_at": "2026-09-30T11:02:48+00:00"
}

Full refund

JSON
{
  "event": "payment.refunded",
  "sandbox": false,
  "code":        "3f6b2d10-9a4c-4e21-8b7f-5c0d1e2f3a4b",
  "ucode": "PEDIDO-1042",
  "method": "sofort",
  "effective_method": "sofort",
  "amount": "49.90",
  "currency": "EUR",
  "status": "refunded",
  "created_at": "2026-09-18T14:33:22+00:00",
  "adjustment": {
    "id": "c4d5e6f7-0a1b-4c2d-8e3f-4a5b6c7d8e9f",
    "type": "refund",
    "amount": "49.90",
    "currency": "EUR",
    "reason": "El cliente canceló el pedido"
  }
}

Partial chargeback

The customer's bank disputed €30.00 of an €80.00 payment: the payment stays completed.

JSON
{
  "event": "payment.charged_back",
  "sandbox": false,
  "code":        "9b8a7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
  "ucode": "PEDIDO-0988",
  "method": "16cards",
  "effective_method": "16cards",
  "amount": "80.00",
  "currency": "EUR",
  "status": "completed",
  "created_at": "2026-09-02T18:20:11+00:00",
  "adjustment": {
    "id": "d1e2f3a4-5b6c-4d7e-8f9a-0b1c2d3e4f5a",
    "type": "chargeback",
    "amount": "30.00",
    "currency": "EUR",
    "reason": null
  }
}

Test payment notifications have the same format, with "sandbox": true: see the example in Test mode →

Headers

HTTP
POST /tu-url-de-notificacion HTTP/1.1
Content-Type: application/json
X-Epayouts-Event: payment.completed
X-Epayouts-Delivery: 1234
X-Epayouts-Signature: t=1790380000,v1=5d41402abc4b2a76b9719d911017c592…
X-Epayouts-Sandbox: 1        (test payments only)
  • X-Epayouts-Delivery — notification number; it is the same on every retry of the same notification.
  • X-Epayouts-Signature — t = Unix seconds of the delivery; v1 = signature (see below). It changes on every attempt.

Verify the signature

v1 = HMAC-SHA256("<t>.<raw body>", webhook signing key). The key is in your merchant panel → API and is different from the API secret. Reject notifications whose t is older than 5 minutes.

PHP
<?php
$raw = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_EPAYOUTS_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $parts);   // t=…&v1=…
$expected = hash_hmac('sha256', ($parts['t'] ?? '') . '.' . $raw, getenv('EPAYOUTS_WEBHOOK_KEY'));

if (!hash_equals($expected, $parts['v1'] ?? '') || abs(time() - (int) ($parts['t'] ?? 0)) > 300) {
    http_response_code(400);
    exit;
}
$event = json_decode($raw, true);
if ($event['sandbox'] === false) {
    switch ($event['event']) {   // usá event, no status: en devoluciones parciales status sigue en completed
        case 'payment.completed':    // marcar el pedido $event['ucode'] como pagado (si ya lo estaba, no hagas nada)
            break;
        case 'payment.failed':       // dejar el pedido sin pagar
            break;
        case 'payment.refunded':     // registrar la devolución: $event['adjustment']['amount']
        case 'payment.charged_back': // registrar el contracargo: $event['adjustment']['amount']
            break;
    }
}
http_response_code(200);

Retries

Answer HTTP 2xx within 10 seconds. Otherwise we retry with increasing delays (by default 1, 2, 4, 8… minutes, up to 6 hours between attempts) up to a maximum number of attempts; when exhausted, a support case is opened. The same notification can arrive more than once: check by code and status whether you already handled it, so the same order is never processed twice.

The notification URL must be public https. You can request a resend from your merchant panel → Payments → details.