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

# Verify webhook events

> Confirm that an event really came from Pila before you act on it — by signature, by IP, or both.

Your webhook URL is public, so anything on the internet can post to it. Never credit a customer, release goods, or mark an order paid until you have verified the event.

There are two ways to check, and they work well together.

<Warning>
  Treat an unverified webhook as untrusted input. A forged "deposit" event that your code trusts is a direct path to losing money.
</Warning>

## Signature validation

Every event Pila sends carries an `x-pila-signature` header. Its value is an HMAC SHA256 signature of the event payload, signed with your secret key.

Recompute the signature over the **raw request body** and compare it to the header before doing anything else.

<CodeGroup>
  ```javascript Node.js theme={null}
  const crypto = require("crypto");
  const secret = process.env.PILA_SECRET_KEY;

  // Using Express
  app.post("/webhookurl", (req, res) => {
    const hash = crypto
      .createHmac("sha256", secret)
      .update(JSON.stringify(req.body))
      .digest("hex");

    const signature = req.headers["x-pila-signature"];

    if (
      !signature ||
      !crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(signature))
    ) {
      return res.sendStatus(401);
    }

    const event = req.body;
    // Do something with event

    res.sendStatus(200);
  });
  ```

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

  from flask import Flask, request, abort

  app = Flask(__name__)
  SECRET = os.environ["PILA_SECRET_KEY"].encode()


  @app.post("/webhookurl")
  def pila_webhook() -> tuple[str, int]:
      signature = request.headers.get("x-pila-signature", "")
      expected = hmac.new(SECRET, request.get_data(), hashlib.sha256).hexdigest()

      if not hmac.compare_digest(expected, signature):
          abort(401)

      event = request.get_json()
      # Do something with event

      return "", 200
  ```

  ```php PHP theme={null}
  <?php
  $secret = getenv('PILA_SECRET_KEY');
  $payload = file_get_contents('php://input');
  $signature = $_SERVER['HTTP_X_PILA_SIGNATURE'] ?? '';

  $expected = hash_hmac('sha256', $payload, $secret);

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

  $event = json_decode($payload, true);
  // Do something with event

  http_response_code(200);
  ```
</CodeGroup>

<Info>
  Compare with a constant-time function — `crypto.timingSafeEqual`, `hmac.compare_digest`, `hash_equals` — rather than `==`. A plain comparison leaks timing information an attacker can use to guess the signature.
</Info>

## IP allowlisting

You can also restrict your webhook endpoint so only Pila can reach it. Pila sends webhooks from a single address:

```
20.112.64.208
```

Allow that address at your firewall, load balancer, or in application middleware, and reject everything else.

<Note>
  IP allowlisting is a useful second layer, but it is not a substitute for signature validation. Verify the signature even if the request came from the right address.
</Note>

## Responding to events

<Steps>
  <Step title="Verify first">
    Check the signature before parsing or acting on the payload.
  </Step>

  <Step title="Reply 200 quickly">
    Acknowledge the event, then do the slow work — database writes, emails, ledger updates — in a background job. A slow endpoint looks like a failed one.
  </Step>

  <Step title="Handle duplicates">
    The same event can arrive more than once. Key your processing on the transaction or event identifier so a repeat delivery is a no-op.
  </Step>

  <Step title="Log without PII">
    Record the event type, identifier and outcome. Do not log full payloads — they contain names, phone numbers and account details.
  </Step>
</Steps>

## Next

<CardGroup cols={2}>
  <Card title="Subscribe to webhooks" icon="webhook" href="/api-reference/webhooks/subscribe">
    Register the URL that will receive these events.
  </Card>

  <Card title="Card events" icon="credit-card" href="/api-reference/webhooks/card-events">
    Every card event payload, field by field.
  </Card>
</CardGroup>


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