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

# Webhooks

> Receive verification and wallet events in real time, and verify they came from Pruva.

Webhooks push events to your server as they happen, so you can react to a completed check without polling. Manage them in the dashboard under Settings.

## Events

A webhook subscribes to one or more events:

| Event                    | Fires when                                     |
| ------------------------ | ---------------------------------------------- |
| `verification.completed` | A verification finishes.                       |
| `verification.failed`    | A verification could not be completed.         |
| `wallet.credited`        | Your wallet is topped up.                      |
| `wallet.low_balance`     | Your wallet balance crosses the low threshold. |
| `team.member_added`      | A member is added to your organization.        |

## Setting one up

<Steps>
  <Step title="Add an endpoint">
    In the dashboard, add your `https` URL and choose the events to subscribe to.
  </Step>

  <Step title="Store the signing secret">
    A secret (`whsec_...`) is shown **once** when you create the webhook. Copy it and store it safely; it is what you use to verify incoming events.
  </Step>

  <Step title="Receive and verify">
    On each event, Pruva sends a signed POST to your URL. Verify the signature before trusting the payload.
  </Step>
</Steps>

## Verifying the signature

Every delivery carries two headers:

| Header              | Meaning                                             |
| ------------------- | --------------------------------------------------- |
| `X-Pruva-Signature` | An HMAC SHA-256 signature of the request.           |
| `X-Pruva-Timestamp` | The time the event was signed, as a Unix timestamp. |

The signature is computed over the timestamp and the raw request body, joined by a dot:

```text theme={null}
signature = HMAC_SHA256(secret, "{timestamp}.{rawBody}")
```

Recompute it on your side with your signing secret and compare. If it matches, the event is authentic and unmodified.

<CodeGroup>
  ```javascript Node theme={null}
  import crypto from "crypto";

  function verify(rawBody, headers, secret) {
    const timestamp = headers["x-pruva-timestamp"];
    const signature = headers["x-pruva-signature"];

    const expected = crypto
      .createHmac("sha256", secret)
      .update(`${timestamp}.${rawBody}`)
      .digest("hex");

    // Constant time compare to avoid leaking timing information.
    return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  }
  ```

  ```python Python theme={null}
  import hmac
  import hashlib

  def verify(raw_body: str, headers: dict, secret: str) -> bool:
      timestamp = headers["X-Pruva-Timestamp"]
      signature = headers["X-Pruva-Signature"]

      expected = hmac.new(
          secret.encode(),
          f"{timestamp}.{raw_body}".encode(),
          hashlib.sha256,
      ).hexdigest()

      return hmac.compare_digest(signature, expected)
  ```

  ```php PHP theme={null}
  function verify(string $rawBody, array $headers, string $secret): bool {
      $timestamp = $headers['X-Pruva-Timestamp'];
      $signature = $headers['X-Pruva-Signature'];

      $expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);

      return hash_equals($expected, $signature);
  }
  ```
</CodeGroup>

<Warning>
  Verify against the **raw** request body, exactly as received. If you parse the JSON first and re-serialize it, the bytes can change and the signature will not match. Read the raw body, verify, then parse.
</Warning>

## Responding

Return a `2xx` quickly to acknowledge receipt. Do the real work, updating your records, notifying a user, after you have acknowledged, so a slow task on your side does not hold the delivery open.

<Note>
  Only one webhook is needed to receive all your events; subscribe it to everything you care about. Webhooks are managed in Settings, not as a separate area.
</Note>
