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

# Webhooks

> Receive signed real-time notifications for payments and security deposits

# Webhooks

PayRequest can send signed HTTP POST requests when a payment succeeds or a security deposit changes status. Use these events to automate fulfillment, synchronize bookings, or trigger downstream workflows without polling.

## Setup

1. Go to **Settings → API & MCP** in your PayRequest dashboard
2. Scroll down to the **Webhooks** section
3. Enter your endpoint URL (must be publicly reachable over HTTPS)
4. Click **Save** — a signing secret is generated automatically
5. Copy the **Signing Secret** and store it securely in your environment

***

## Verifying signatures

Every request includes an `X-PayRequest-Signature` header containing an HMAC-SHA256 signature of the raw request body. **Always verify this before processing the event.**

```
X-PayRequest-Signature: sha256=a1b2c3d4...
```

<CodeGroup>
  ```php PHP theme={null}
  $secret   = $_ENV['PAYREQUEST_WEBHOOK_SECRET'];
  $body     = file_get_contents('php://input');
  $received = $_SERVER['HTTP_X_PAYREQUEST_SIGNATURE'] ?? '';
  $expected = 'sha256=' . hash_hmac('sha256', $body, $secret);

  if (!hash_equals($expected, $received)) {
      http_response_code(401);
      exit;
  }

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

  ```python Python theme={null}
  import hashlib, hmac, os
  from flask import request, abort

  secret = os.environ['PAYREQUEST_WEBHOOK_SECRET'].encode()
  body   = request.get_data()
  sig    = request.headers.get('X-PayRequest-Signature', '')

  expected = 'sha256=' + hmac.new(secret, body, hashlib.sha256).hexdigest()
  if not hmac.compare_digest(expected, sig):
      abort(401)

  event = request.get_json()
  ```

  ```javascript Node.js theme={null}
  const crypto = require('crypto');

  const secret = process.env.PAYREQUEST_WEBHOOK_SECRET;
  const body   = req.rawBody; // use raw body, not parsed JSON
  const sig    = req.headers['x-payrequest-signature'] ?? '';

  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(body);
  ```
</CodeGroup>

<Warning>
  Always use a timing-safe comparison (`hash_equals`, `hmac.compare_digest`, `timingSafeEqual`) to prevent timing attacks.
</Warning>

***

## Event: `payment.succeeded`

Fired when a transaction status changes to `paid`. Delivered asynchronously via the queue (typically within seconds).

### Payload

```json theme={null}
{
  "event": "payment.succeeded",
  "timestamp": "2026-05-30T12:00:00+02:00",
  "data": {
    "id": 1234,
    "amount": 49.00,
    "currency": "EUR",
    "description": "JMIT-githubuser",
    "payment_link_id": 42,
    "payment_method": "ideal",
    "reference": "tr_abc123",
    "paid_at": "2026-05-30T11:59:45+02:00",
    "customer": {
      "id": 99,
      "name": "Jane Doe",
      "email": "jane@example.com"
    }
  }
}
```

### Fields

| Field | Type | Description |
| - | - | - |
| `event` | string | Always `payment.succeeded` |
| `timestamp` | ISO 8601 | When the event was dispatched |
| `data.id` | integer | Transaction ID |
| `data.amount` | decimal | Amount paid (e.g. `49.00`) |
| `data.currency` | string | 3-letter ISO currency code |
| `data.description` | string | Your custom reference (e.g. `JMIT-githubuser`) |
| `data.payment_link_id` | integer\|null | ID of the Smart Link used, if any |
| `data.payment_method` | string | Payment method used (e.g. `ideal`, `creditcard`) |
| `data.reference` | string | Payment provider transaction reference |
| `data.paid_at` | ISO 8601 | Exact time the payment was confirmed |
| `data.customer` | object\|null | Customer name, email, and ID |

## Security deposit events

Security deposit integrations can receive:

* `deposit.created`
* `deposit.authorized`
* `deposit.partially_captured`
* `deposit.captured`
* `deposit.released`
* `deposit.expired`
* `deposit.failed`

The request also includes `X-PayRequest-Event` with the event name. See the [Security Deposits API](/api/security-deposits) for the payload and integration flow.

***

## Retries

Security deposit webhook delivery uses **5 attempts** with increasing backoff intervals. Payment webhook delivery uses the existing payment queue policy.

Return any `2xx` status code to acknowledge receipt.

***

## Rotating the secret

If your secret is compromised, go to **Settings → API & MCP → Webhooks** and click **Regenerate**. Your old secret stops working immediately — update your environment variable before rotating.

***

## Testing locally

Use a tunnel tool to expose your local server during development:

```bash theme={null}
# Using ngrok
ngrok http 8000

# Then set your webhook URL to:
# https://abc123.ngrok.io/webhooks/payrequest
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.