> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.epaygames.io/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.epaygames.io/_mcp/server.

# Webhooks & Redirects

Both the Payments API and the Disbursement API support asynchronous, real-time notification via a `callback_webhook_url`, in addition to (or instead of) polling `Get Transactions`.

Webhook notifications — also known as **real-time payment notifications** or **callbacks** — are automated messages sent to your server when a payment or disbursement event occurs. They let you **instantly update your system** (e.g., credit a user's account) without polling or manual intervention.

When an event is processed, a **`POST`** request with a **JSON payload** is sent to your specified `callback_webhook_url`, so your backend can handle the result immediately and reliably.

The mechanics below (signature algorithm, data format, IP addresses, payload shape, and status values) **differ between the two APIs**. Don't reuse Payments API webhook-verification code for Disbursement API webhooks, or vice versa.

## Redirects (Payments API only)

`Generate Transaction` additionally accepts two optional browser-redirect fields, which have no Disbursement API equivalent since a disbursement has no customer browser involved:

| Field                  | Purpose                                                                              |
| ---------------------- | ------------------------------------------------------------------------------------ |
| `success_redirect_url` | Where the customer's browser is redirected after a **successful** payment.           |
| `failure_redirect_url` | Where the customer's browser is redirected if the payment **fails or is cancelled**. |

The **redirect URLs** are a browser-side experience — they only fire if the customer's browser is still open on the payment page. The **webhook** is a server-side notification that fires regardless of what the customer's browser is doing, which makes it the more reliable mechanism for updating your system's records.

## Example Webhook Payloads

#### Payments API

```json title="Example webhook payload"
{
  "message": "Transaction has been updated.",
  "data": {
    "reference_no": "TESTQRZLXXX2OPGV",
    "amount": 1,
    "status": "completed",
    "signature": "6c37ddeea4503d2a526a300fe00d0d745caca989edcf4fcc226ae08414f061c8",
    "transaction": {
      "subtotal_amount": 1,
      "service_fee": 0,
      "total_amount": 1,
      "reference_no": "TESTQRZLXXX2OPGV",
      "provider_reference_no": "a1138921-3291-4783-8fa1-7041e6c98ae2",
      "status": "completed",
      "link_url": null,
      "is_expired": false,
      "expires_at": "2025-07-19T20:03:04+08:00",
      "date": "2025-07-18T20:03:04+08:00",
      "channel": {
        "name": "GCash",
        "desciption": null,
        "slug": "gcash-direct"
      },
      "sub_biller": null,
      "extra": null
    }
  }
}
```

Note the payload is flatter at the top level (`data.amount`, `data.reference_no`, `data.status`, `data.signature`) with a nested `data.transaction` object carrying the full transaction detail.

#### Disbursement API

```json title="Example webhook payload"
{
  "message": "Transaction has been updated.",
  "data": {
    "reference_no": "TESTINGREFNO001",
    "subtotal_amount": 1,
    "service_fee": 0,
    "total_amount": 1,
    "status": "completed",
    "channel": {
      "name": "PESONet",
      "description": null,
      "slug": "pesonet",
      "code": "PESONET"
    },
    "signature": "a1b23b705ec5565cdf43c172635bd8b32d832d11881505089c3e17740cfeffdf6753fc464fe36a3e7d31688b7d7eb02adcaa25b5f996621878ea8371a381fae0"
  }
}
```

Note the payload is flat — there's no nested `transaction` object like on the Payments API. All fields (`subtotal_amount`, `service_fee`, `total_amount`, `channel`, `signature`) sit directly under `data`.

## Possible Status Values

Both APIs use the same three status values: `pending`, `cancelled`, and `completed`.

#### Payments API

| Status      | Meaning                                                                                                  |
| ----------- | -------------------------------------------------------------------------------------------------------- |
| `pending`   | Payment has been initiated but is not yet completed. The customer may still be in the process of paying. |
| `cancelled` | Payment was either failed, expired, or manually canceled. No funds were received.                        |
| `completed` | Payment was successfully processed and confirmed. Funds have been received.                              |

#### Disbursement API

| Status      | Meaning                                                                             |
| ----------- | ----------------------------------------------------------------------------------- |
| `pending`   | Disbursement has been initiated but is not yet completed.                           |
| `cancelled` | Disbursement was either failed or cancelled. No funds were transferred.             |
| `completed` | Disbursement was successfully processed and confirmed. Funds have been transferred. |

Always use these values to determine how your system should respond to an update — don't infer status from HTTP response codes alone.

## Webhook Signature Verification

To ensure that incoming webhook notifications are truly from your payment provider and not a malicious third party, both APIs sign the payload with a **signature** — a cryptographic hash that acts as a tamper-proof seal. Even if someone intercepts the webhook, they cannot fake a valid signature without access to the secret key.

The two APIs use **different HMAC algorithms and different data formats** to compute the signature. Verify against the correct one for the webhook you received.

#### Payments API

Uses **HMAC-SHA256**.

* **Data format:** `amount + "@" + reference_no`
  Example: `100@EPLKZT2OH319WBEF`
* **Signature key:** provided by your payment facilitator. For sandbox/staging (`test-merchant` credentials), use the Signature Key from the [Create Token](/authentication) section: `9XAgVZzU4WhqrXA6jEvMtkF85j8wKZZhQ5RE2hsDPwa5rjPT7RpCmJceu8jNt4Wy`
* **HMAC output:** hexadecimal string

#### JavaScript

```javascript
const crypto = require('crypto');
const signatureKey = 'your_signature_key';
const data = '100@EPLKZT2OH319WBEF';
const signature = crypto
  .createHmac('sha256', signatureKey)
  .update(data)
  .digest('hex');
console.log(signature);
```

#### PHP

```php
$signatureKey = 'your_signature_key';
$data = '100@EPLKZT2OH319WBEF';
$signature = hash_hmac('sha256', $data, $signatureKey);
echo $signature;
```

#### Python

```python
import hmac
import hashlib
signature_key = b'your_signature_key'
data = b'100@EPLKZT2OH319WBEF'
signature = hmac.new(signature_key, data, hashlib.sha256).hexdigest()
print(signature)
```

#### Java

```java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Formatter;

public class HMACExample {
    public static void main(String[] args) throws Exception {
        String signatureKey = "your_signature_key";
        String data = "100@EPLKZT2OH319WBEF";
        SecretKeySpec keySpec = new SecretKeySpec(signatureKey.getBytes("UTF-8"), "HmacSHA256");
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(keySpec);
        byte[] hmacBytes = mac.doFinal(data.getBytes("UTF-8"));
        StringBuilder hexString = new StringBuilder();
        for (byte b : hmacBytes) {
            hexString.append(String.format("%02x", b));
        }
        System.out.println(hexString.toString());
    }
}
```

#### Disbursement API

Uses **HMAC-SHA512**.

* **Data format:** the contents of the `data` object, **excluding** the `signature` field, serialized as **minified JSON** (no extra whitespace).
  Example: `{"reference_no":"TESTINGREFNO001","subtotal_amount":1,"service_fee":0,"total_amount":1,"status":"completed","channel":{"name":"PESONet","description":null,"slug":"pesonet","code":"PESONET"}}`
* **Signature key:** provided by your payment facilitator. For sandbox/staging (`test-merchant` credentials), use: `vQJO2035r1iQAdgSGLx0cV9Ex6yxPCPyxfi09j7OO0F7HR88KxdRjv96i6RIb5ENRUxqe09FoTIIlLASOtyEa9SJTv5ybNkZXQDgVrgYSNFg2xC3DQ7s8Tw7muk3Qocb` (refer to [Create Access Token](/authentication) for `test-merchant` credentials)
* **HMAC output:** hexadecimal string

#### JavaScript

```javascript
function getSignature() {
  var data =
    '{"reference_no":"TESTINGREFNO001","subtotal_amount":1,"service_fee":0,"total_amount":1,"status":"completed","channel":{"name":"PESONet","description":null,"slug":"pesonet","code":"PESONET"}}';
  var key =
    'vQJO2035r1iQAdgSGLx0cV9Ex6yxPCPyxfi09j7OO0F7HR88KxdRjv96i6RIb5ENRUxqe09FoTIIlLASOtyEa9SJTv5ybNkZXQDgVrgYSNFg2xC3DQ7s8Tw7muk3Qocb';
  // test-merchant@example.com key
  return CryptoJS.HmacSHA512(data, key);
  // Returns:
  // a1b23b705ec5565cdf43c172635bd8b32d832d11881505089c3e17740cfeffdf6753fc464fe36a3e7d31688b7d7eb02adcaa25b5f996621878ea8371a381fae0
}
```

#### PHP

```php
function getSignature()
{
    $data = '{"reference_no":"TESTINGREFNO001","subtotal_amount":1,"service_fee":0,"total_amount":1,"status":"completed","channel":{"name":"PESONet","description":null,"slug":"pesonet","code":"PESONET"}}';
    $key = 'vQJO2035r1iQAdgSGLx0cV9Ex6yxPCPyxfi09j7OO0F7HR88KxdRjv96i6RIb5ENRUxqe09FoTIIlLASOtyEa9SJTv5ybNkZXQDgVrgYSNFg2xC3DQ7s8Tw7muk3Qocb';
    // test-merchant@example.com key
    return hash_hmac('sha512', $data, $key);
    // Returns:
    // a1b23b705ec5565cdf43c172635bd8b32d832d11881505089c3e17740cfeffdf6753fc464fe36a3e7d31688b7d7eb02adcaa25b5f996621878ea8371a381fae0
}
```

## IP Whitelisting

IP whitelisting limits incoming requests to only those from approved, trusted sources, reducing the risk of spoofed webhooks or unauthorized actions. This works **in tandem with HMAC signature verification** — together, they ensure the webhook is both authentic and coming from the correct origin.

Do not process any webhook request unless the HMAC signature is verified **and** the request originates from one of the whitelisted IP addresses below. Checking only one of the two can lead to spoofed or fraudulent transactions being processed.

#### Payments API

**Sandbox/Staging:** `43.198.4.7`

**Production:**

* `18.166.179.109`
* `18.166.252.124`

#### Disbursement API

**Sandbox/Staging:** `43.198.4.7`

**Production:**

* `16.162.178.209`
* `18.167.239.15`

Both APIs share the same sandbox/staging IP (`43.198.4.7`), but each has its **own, distinct pair of production IPs**. Whitelist the correct set for the API you're integrating.

## Webhook Retry Mechanism

If your server responds with a **4xx** or **5xx** status code (or doesn't respond in time), the system automatically retries delivery using a backoff schedule. This behavior is **identical for both APIs**.

* Webhook delivery is retried up to **17 times** over a span of roughly **7 days**.
* Retries follow this interval schedule (in minutes):

```json
[2, 10, 10, 60, 120, 360, 900]
```

* Retry #1 — after 2 minutes
* Retry #2 — after 10 minutes
* Retry #3 — after another 10 minutes
* Retry #4 — after 60 minutes
* Retry #5 — after 120 minutes
* Retry #6 — after 360 minutes (6 hours)
* Retry #7 — after 900 minutes (15 hours)
* Retry #8 to #17 — every 900 minutes (15 hours)

The retry count resets **only when a `2xx` response is received** from your `callback_webhook_url`.

## Webhook Timeout & Latency Guidelines

These are also identical for both APIs:

* Up to **3000ms (3 seconds)** is allowed for your server to respond.
* If your server doesn't respond within this window, the attempt is considered failed and will be retried.
* Keep your webhook processing latency **below 300ms** to account for network overhead, JSON parsing, and response handling.

Best practice: acknowledge the webhook immediately (return a `200 OK`), then process the payload asynchronously in the background — don't do slow work before responding.

## Testing Webhook Notifications

For testing and debugging, you can temporarily use [webhook.site](https://webhook.site) as your `callback_webhook_url` for either API. This lets you:

* Instantly see the actual JSON payload sent by the system.
* Inspect and verify the HMAC signature.
* Debug your integration before going live.

This is the same approach shown in the collection's own example requests, and works for both payment and disbursement flows without needing to stand up your own server endpoint first.

## Quick Comparison

|                       | Payments API                                         | Disbursement API                                |
| --------------------- | ---------------------------------------------------- | ----------------------------------------------- |
| Redirect URLs         | Yes (`success_redirect_url`, `failure_redirect_url`) | No — backend-only flow                          |
| Signature algorithm   | HMAC-SHA256                                          | HMAC-SHA512                                     |
| Signature data format | `amount + "@" + reference_no`                        | Minified JSON of `data` (excluding `signature`) |
| Payload shape         | Flat top-level fields + nested `data.transaction`    | Flat — no nested transaction object             |
| Status values         | `pending`, `cancelled`, `completed`                  | `pending`, `cancelled`, `completed`             |
| Sandbox IP            | `43.198.4.7`                                         | `43.198.4.7`                                    |
| Production IPs        | `18.166.179.109`, `18.166.252.124`                   | `16.162.178.209`, `18.167.239.15`               |
| Retry schedule        | 17 retries over \~7 days                             | 17 retries over \~7 days                        |
| Timeout               | 3000ms                                               | 3000ms                                          |