> ## 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.

# Webhooks: Receive Payment Notifications from Taliup

> Taliup sends a signed POST request to your webhook URL after every payment is captured. Learn how to verify signatures and safely process payment events.

Webhooks are how Taliup notifies your server when a payment outcome is available. After a payment is captured — whether approved or declined — Taliup sends an HTTP `POST` request to the `webhook_url` you provided when creating the checkout session. The request body contains a JSON payload describing the event, and the `X-Taliup-Signature` header lets you verify that the request genuinely came from Taliup.

## Webhook payload

Every webhook Taliup sends has the following JSON structure:

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

| Field                | Type   | Description                                                           |
| -------------------- | ------ | --------------------------------------------------------------------- |
| `event`              | string | The event type. Currently always `payment.captured`.                  |
| `merchant_site_id`   | string | Your Merchant Site ID, for confirming the event targets your account. |
| `transaction_id`     | string | Taliup's unique identifier for this transaction.                      |
| `amount`             | string | The transaction amount as a decimal string (e.g. `"49.99"`).          |
| `currency`           | string | The currency code (`CAD` or `USD`).                                   |
| `status`             | string | Payment outcome — see [Status values](#status-values) below.          |
| `card_type`          | string | Card network (e.g. `VISA`, `MASTERCARD`).                             |
| `card_number_masked` | string | Masked card number for display purposes.                              |
| `reference`          | string | The `reference` value you passed when creating the checkout session.  |
| `timestamp`          | string | ISO 8601 timestamp of when the event was created.                     |

## Signature verification

Every request from Taliup includes an `X-Taliup-Signature` header with the value:

```
sha256=<HMAC-SHA256 of raw request body signed with your Merchant Secret Key>
```

You must verify this signature using the **raw request body** — before it is parsed or decoded. Verifying the signature confirms that the webhook came from Taliup and that the payload has not been tampered with in transit.

<Warning>
  Always verify the `X-Taliup-Signature` before processing a webhook event or fulfilling an order. Processing unverified webhooks can expose your application to spoofed payment notifications.
</Warning>

## Verification methods

The SDK provides two ways to verify a webhook:

<CodeGroup>
  ```php constructEvent (recommended) theme={null}
  <?php
  // Webhook::constructEvent() verifies the signature and decodes the payload
  // in one step. It throws ApiException if verification fails.

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

  $secret    = getenv('TALIUP_MERCHANT_SECRET_KEY');
  $payload   = file_get_contents('php://input');
  $signature = $_SERVER['HTTP_X_TALIUP_SIGNATURE'] ?? '';

  try {
      $event = Webhook::constructEvent($payload, $signature, $secret);
  } catch (ApiException $e) {
      // 401 — invalid signature; 400 — payload is not valid JSON
      http_response_code($e->getStatusCode());
      exit($e->getMessage());
  }

  // $event is the decoded payload array — safe to use
  $reference = $event['reference'];
  $status    = $event['status'];
  ```

  ```php verify (boolean check) theme={null}
  <?php
  // Webhook::verify() returns a bool — use this when you want to
  // handle the failure branch yourself without a try/catch.

  use Taliup\Sdk\Webhook;

  $secret    = getenv('TALIUP_MERCHANT_SECRET_KEY');
  $payload   = file_get_contents('php://input');
  $signature = $_SERVER['HTTP_X_TALIUP_SIGNATURE'] ?? '';

  $isValid = Webhook::verify($payload, $signature, $secret);

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

  $event = json_decode($payload, true);
  ```
</CodeGroup>

| Method                      | Signature                                                     | Behaviour on failure  |
| --------------------------- | ------------------------------------------------------------- | --------------------- |
| `Webhook::constructEvent()` | `(string $payload, string $signature, string $secret): array` | Throws `ApiException` |
| `Webhook::verify()`         | `(string $payload, string $signature, string $secret): bool`  | Returns `false`       |

## Complete webhook receiver example

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

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

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

$secret    = getenv('TALIUP_MERCHANT_SECRET_KEY');
$payload   = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_TALIUP_SIGNATURE'] ?? '';

// Step 1: Verify signature and decode payload
try {
    $event = Webhook::constructEvent($payload, $signature, $secret);
} catch (ApiException $e) {
    // 401 — invalid signature; 400 — payload is not valid JSON
    http_response_code($e->getStatusCode());
    exit($e->getMessage());
}

// Step 2: Handle the event
$reference     = $event['reference'];          // Your order ID
$status        = $event['status'];             // 'approved', 'declined', etc.
$transactionId = $event['transaction_id'];     // Taliup's transaction ID
$amount        = $event['amount'];             // e.g. '49.99'
$currency      = $event['currency'];           // 'CAD' or 'USD'

if ($status === 'approved') {
    // Mark the order as paid in your database
    // fulfillOrder($reference, $transactionId);
}

// Step 3: Acknowledge receipt
http_response_code(200);
echo json_encode(['received' => true]);
```

<Note>
  Respond with HTTP `200` as quickly as possible. If your webhook endpoint takes too long to respond, Taliup may retry the delivery. Perform any heavy processing (e.g. sending a confirmation email) asynchronously after acknowledging the webhook.
</Note>

## Status values

The `status` field in the webhook payload indicates the payment outcome:

| Status     | Description                                                             |
| ---------- | ----------------------------------------------------------------------- |
| `approved` | The payment was authorized and captured successfully. Fulfil the order. |
| `declined` | The payment was declined by the card issuer. Do not fulfil the order.   |

## Idempotency

Taliup may deliver the same webhook more than once in rare retry scenarios. Use the `transaction_id` field as an idempotency key — check whether you have already processed a transaction with that ID before taking action, and skip duplicate deliveries.

<Tip>
  Store the `transaction_id` in your database when you fulfil an order. Before processing any webhook, query for an existing record with that `transaction_id` and skip the event if one is found.
</Tip>
