# Webhooks

PayLater sends **real-time notifications** to your server via `HTTP POST` with a JSON body whenever a key transaction event occurs — so you don’t have to poll [Check Payment Status](https://docs.paylaterapp.com/check-payment-status.html).

## Before you start

To receive webhooks you need:

1. A **public HTTPS endpoint** that accepts `POST` requests.
2. A **webhook secret** — request one from your PayLater account manager. It’s used to verify that each notification genuinely came from PayLater.

**POST** `https://your-server.com/paylater/webhook · your endpoint`

## The webhook request

#### Headers

| Header | Value |
| --- | --- |
| `Content-Type` | `application/json` |

#### Body

**POST body**

```json
{
  "merchantId": "<merchant_id>",
  "orderId": "<order_id>",
  "paylaterRef": "PL1746499849330726",
  "status": "<status>",
  "timestamp": "<timestamp>",
  "signature": "<signature>",
  "txHash": "<tx_hash>",
  "comments": "<comments>"
}
```

**Payload fields**

- `merchantId` (String, optional): Your unique merchant identifier.
- `orderId` (String, optional): The order identifier you supplied when creating the payment.
- `paylaterRef` (String, optional): PayLater’s internal reference for the order.
- `status` (String, optional): Order status — one of `success`, `failed`, `pending`.
- `timestamp` (Long, optional): Epoch timestamp of the event.
- `txHash` (String, optional): MD5 hash of the concatenated payload (see below).
- `signature` (String, optional): HMAC SHA-256 of `txHash`, keyed with your webhook secret.
- `comments` (String, optional): Optional comments / metadata.

## Verifying a webhook

> **Danger: Always verify before trusting a webhook**
>
> Never act on a webhook until **both** checks below pass. An unverified request could come from anyone. Respond `200` only after verification succeeds.

Verification is two steps:

1. **Reconstruct `txHash`** — concatenate `merchantId + orderId + status + timestamp + comments`, uppercase the result, and take its **MD5** hash. It must equal the `txHash` field.
2. **Validate `signature`** — compute the **HMAC SHA-256** of `txHash` using your `merchantWebhookSecret`. It must equal the `signature` field.

**Node.js**

```javascript
const crypto = require('crypto');

function verifyWebhook(req) {
  const { merchantId, orderId, status, timestamp, txHash, signature, comments } = req.body;

  // 1. Reconstruct and compare txHash
  const data = `${merchantId}${orderId}${status}${timestamp}${comments}`.toUpperCase();
  const computedTxHash = crypto.createHash('md5').update(data).digest('hex');
  if (computedTxHash !== txHash) return false;

  // 2. Validate the signature
  const computedSignature = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(txHash)
    .digest('hex');

  return computedSignature === signature;
}
```

**PHP**

```php
<?php
$MERCHANT_SECRET = getenv('WEBHOOK_SECRET');
$request = json_decode(file_get_contents("php://input"), true);

// 1. Reconstruct txHash
$dataString = strtoupper(
    $request['merchantId'] . $request['orderId'] . $request['status'] .
    $request['timestamp'] . $request['comments']
);
$computedTxHash = md5($dataString);

// 2. Validate signature
$computedSignature = hash_hmac("sha256", $computedTxHash, $MERCHANT_SECRET);

if ($computedTxHash === $request['txHash'] && $computedSignature === $request['signature']) {
    http_response_code(200);
    echo json_encode(["message" => "Webhook received successfully"]);
} else {
    http_response_code(403);
    echo json_encode(["message" => "Invalid signature"]);
}
```

> **Tip: Respond quickly, process asynchronously**
>
> Return `200 OK` as soon as verification passes, then do heavy work (order fulfilment, emails) on a background queue. PayLater may retry if it doesn’t receive a timely `2xx`.
