> ## Documentation Index
> Fetch the complete documentation index at: https://docs.taliuphq.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Handle Taliup Payment Webhooks in Your PHP Application

> Learn how to receive, verify, and process Taliup payment webhook events securely in your PHP application using signature verification.

After a payment is captured, Taliup sends a `POST` request to the `webhook_url` you provided when creating the checkout session. The request contains a JSON body describing the payment outcome and an `X-Taliup-Signature` header you must verify before acting on the event. This guide walks you through setting up a reliable, secure webhook receiver.

<Warning>
  Always verify the `X-Taliup-Signature` header before reading or processing any webhook data. Processing unverified payloads exposes your application to spoofed events and fraudulent order fulfilment.
</Warning>

<Steps>
  <Step title="Set webhook_url when creating the checkout session">
    Pass your HTTPS webhook endpoint as `webhook_url` in the `createCheckoutUrl()` payload. Taliup will `POST` the payment event to this URL after a payment is captured.

    ```php theme={null}
    $response = $client->hostedPayments()->createCheckoutUrl([
        'amount'      => 49.99,
        'currency'    => 'CAD',
        'reference'   => 'ORDER-10029',
        'redirect_url' => 'https://yoursite.com/payment/success',
        'cancel_url'  => 'https://yoursite.com/payment/cancel',
        'webhook_url' => 'https://yoursite.com/webhooks/taliup', // <-- your endpoint
    ]);
    ```
  </Step>

  <Step title="Create a webhook endpoint">
    Create a dedicated PHP file at the URL you registered as `webhook_url`. The endpoint must be publicly reachable over HTTPS.

    During local development you can expose your local server using a tunnelling tool such as [ngrok](https://ngrok.com):

    ```bash theme={null}
    ngrok http 8000
    # Use the printed https://xxxx.ngrok.io URL as your webhook_url
    ```

    Make your `TALIUP_MERCHANT_SECRET_KEY` available to the script via an environment variable — never hard-code secrets in source files.
  </Step>

  <Step title="Read the raw body and signature header">
    You must read the **raw** request body with `file_get_contents('php://input')` **before** any other input parsing. Reading from `$_POST` or calling `json_decode()` first will alter the body and invalidate the signature.

    ```php theme={null}
    <?php

    require __DIR__ . '/vendor/autoload.php';

    use Taliup\Sdk\Exceptions\ApiException;
    use Taliup\Sdk\Webhook;

    $secret    = (string) getenv('TALIUP_MERCHANT_SECRET_KEY');
    $payload   = (string) file_get_contents('php://input'); // raw body — read first
    $signature = $_SERVER['HTTP_X_TALIUP_SIGNATURE'] ?? '';
    ```
  </Step>

  <Step title="Verify the signature with Webhook::constructEvent()">
    Pass the raw payload, signature header, and your secret key to `Webhook::constructEvent()`. If verification succeeds it returns the decoded event as an associative array. If the signature does not match it throws an `ApiException` with status code `401`.

    ```php theme={null}
    try {
        $event = Webhook::constructEvent($payload, $signature, $secret);
    } catch (ApiException $e) {
        // Signature invalid — reject the request
        http_response_code(401);
        exit($e->getMessage());
    }
    ```

    If you only need a boolean result and prefer to handle the failure yourself, you can use `Webhook::verify()` instead:

    ```php theme={null}
    $isValid = Webhook::verify($payload, $signature, $secret); // returns bool

    if (! $isValid) {
        http_response_code(401);
        exit('Invalid signature.');
    }

    $event = json_decode($payload, true);
    ```

    <Note>
      `constructEvent()` is the recommended approach because it both verifies and decodes the payload atomically, reducing the risk of accidentally processing an unverified event.
    </Note>
  </Step>

  <Step title="Process the event">
    After successful verification, read the `approved` flag (or `status` field) and update your system accordingly. Use the `reference` field to look up the corresponding order in your database.

    <Tip>
      The `reference` value is the order ID you passed when creating the checkout session. Keying your lookup on `reference` is more reliable than matching on `transaction_id`, which is generated by Taliup.
    </Tip>

    ```php theme={null}
    $reference = $event['reference'] ?? null;
    $approved  = $event['approved']  ?? false;

    if ($approved) {
        // Mark the order as paid in your database
        error_log("Payment approved for order: {$reference}");
        // e.g. Orders::markAsPaid($reference);
    } else {
        // Handle declined or failed payment
        error_log("Payment not approved for order: {$reference}");
        // e.g. Orders::markAsFailed($reference);
    }
    ```
  </Step>

  <Step title="Return 200 OK">
    Respond with HTTP `200` to acknowledge receipt. If Taliup does not receive a `200` response it will retry the webhook. Return the acknowledgement as early as possible — after verification and before any slow database or downstream operations if you can.

    ```php theme={null}
    http_response_code(200);
    echo json_encode(['received' => true]);
    ```
  </Step>
</Steps>

## Complete webhook receiver

The following is a production-ready webhook receiver based on the official SDK example:

```php theme={null}
<?php

/**
 * Taliup webhook receiver.
 *
 * Deploy this file at your webhook_url.
 * Required environment variable: TALIUP_MERCHANT_SECRET_KEY
 */

require __DIR__ . '/vendor/autoload.php';

use Taliup\Sdk\Exceptions\ApiException;
use Taliup\Sdk\Webhook;

$secret = (string) getenv('TALIUP_MERCHANT_SECRET_KEY');

if ($secret === '') {
    http_response_code(500);
    exit('Missing TALIUP_MERCHANT_SECRET_KEY.');
}

// Read the raw body BEFORE any other input parsing
$payload   = (string) file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_TALIUP_SIGNATURE'] ?? '';

// Verify signature and decode payload
try {
    $event = Webhook::constructEvent($payload, $signature, $secret);
} catch (ApiException $e) {
    http_response_code(401);
    exit($e->getMessage());
}

// Extract the fields you need
$reference = $event['reference'] ?? null;
$approved  = $event['approved']  ?? false;

if ($approved) {
    // TODO: mark the order as paid in your system using $reference
    error_log("Payment approved for order: {$reference}");
} else {
    // TODO: handle declined/failed payment
    error_log("Payment not approved for order: {$reference}");
}

// Acknowledge receipt — return 200 so Taliup does not retry
http_response_code(200);
echo json_encode(['received' => true]);
```

## Webhook payload reference

Taliup sends the following JSON body on every `payment.captured` event:

```json theme={null}
{
    "event":              "payment.captured",
    "merchant_site_id":   "your_merchant_site_id",
    "transaction_id":     "abc123",
    "amount":             "49.99",
    "currency":           "CAD",
    "status":             "approved",
    "approved":           true,
    "card_type":          "VISA",
    "card_number_masked": "41**********1111",
    "reference":          "ORDER-10029",
    "timestamp":          "2026-05-14T18:00:00+00:00"
}
```

| Field                | Type    | Description                                               |
| -------------------- | ------- | --------------------------------------------------------- |
| `event`              | string  | Event type. Currently `payment.captured`.                 |
| `merchant_site_id`   | string  | Your Merchant Site ID.                                    |
| `transaction_id`     | string  | Taliup's unique transaction identifier.                   |
| `amount`             | string  | Payment amount as a decimal string.                       |
| `currency`           | string  | Three-letter currency code (`CAD` or `USD`).              |
| `status`             | string  | `approved`, `declined`, or another terminal status.       |
| `approved`           | boolean | `true` when the payment was approved, `false` otherwise.  |
| `card_type`          | string  | Card network (e.g. `VISA`, `MASTERCARD`).                 |
| `card_number_masked` | string  | Masked card number for display purposes.                  |
| `reference`          | string  | The order reference you passed when creating the session. |
| `timestamp`          | string  | ISO 8601 timestamp of the payment event.                  |
