# Validating Webhook Signatures

Every webhook request sent by Plannr includes an `X-Signature` header. This is an HMAC-SHA256 signature of the raw request body, computed using your webhook subscription's `signing_secret`. By verifying this signature before processing a webhook, you can be confident the payload was sent by Plannr and has not been tampered with in transit.

***

## Step 1 — Retrieve your signing secret

A unique 60-character `signing_secret` is automatically generated when a webhook subscription is created. It is returned in the response body whenever you create, update, or retrieve a webhook subscription through the API.

{% hint style="info" %}
The signing secret is not currently displayed in the Plannr UI. Retrieve it via the API and store it securely in your application.
{% endhint %}

Make a `GET` request to retrieve all of your webhook subscriptions and their secrets:

```
GET /api/v2/webhook-subscriptions
```

Example response:

```json
{
  "data": [
    {
      "uuid": "53f22fe4-3731-4cdc-aa12-e65bd0b15e52",
      "created_at": "2024-01-15T10:30:00+00:00",
      "updated_at": "2024-01-15T10:30:00+00:00",
      "signing_secret": "AbCdEfGhIjKlMnOpQrStUvWxYz0123456789AbCdEfGhIjKlMnOpQrSt",
      "url": "https://api.example.com/webhooks",
      "events": ["account.created", "client.updated"],
      "last_outgoing_webhook_call_at": "2024-03-01T14:22:11+00:00"
    }
  ]
}
```

{% hint style="danger" %}
Treat your `signing_secret` like a password. Never expose it in client-side code, logs, or version control. Store it securely using environment variables or a secrets manager.
{% endhint %}

***

## Step 2 — Understand the signature

When Plannr sends a webhook to your endpoint, the request will include:

* **`X-Signature`** — an HMAC-SHA256 hex digest of the raw request body, keyed with your `signing_secret`
* **`Content-Type: application/json`** — the body is a JSON payload

Example headers received on your endpoint:

```
POST /webhooks HTTP/1.1
Content-Type: application/json
X-Signature: 3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4
```

***

## Step 3 — Verify the signature

To validate the webhook, compute your own HMAC-SHA256 signature of the **raw request body** using your `signing_secret`, then compare it against the `X-Signature` header. If they match, the webhook is genuine.

{% hint style="warning" %}
Always use the **raw, unparsed request body** when computing the signature — not a re-serialised version of the parsed JSON. Parsing and re-serialising can alter whitespace or key ordering, causing the signature check to fail.
{% endhint %}

{% tabs %}
{% tab title="Python" %}

```python
import hmac
import hashlib

raw_body = request.body
x_signature = request.headers.get("X-Signature", "")
signing_secret = "your-signing-secret-here"

expected = hmac.new(
    key=signing_secret.encode("utf-8"),
    msg=raw_body,
    digestmod=hashlib.sha256,
).hexdigest()

if expected != x_signature:
    raise Exception("Invalid signature")

payload = json.loads(raw_body)
```

{% endtab %}

{% tab title="JavaScript" %}

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

const rawBody = req.body; // use express.raw() middleware
const xSignature = req.headers["x-signature"];
const signingSecret = "your-signing-secret-here";

const expected = crypto
  .createHmac("sha256", signingSecret)
  .update(rawBody)
  .digest("hex");

if (expected !== xSignature) {
  throw new Error("Invalid signature");
}

const payload = JSON.parse(rawBody.toString("utf8"));
```

{% endtab %}

{% tab title=".NET (C#)" %}

```csharp
using System.Security.Cryptography;
using System.Text;

var rawBody = await new StreamReader(request.Body).ReadToEndAsync();
var xSignature = request.Headers["X-Signature"].FirstOrDefault() ?? string.Empty;
var signingSecret = "your-signing-secret-here";

using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(signingSecret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(rawBody));
var expected = Convert.ToHexString(hash).ToLowerInvariant();

if (expected != xSignature.ToLowerInvariant())
    return Results.Unauthorized();

var payload = JsonSerializer.Deserialize<JsonElement>(rawBody);
```

{% endtab %}

{% tab title="PHP" %}

```php
$rawBody       = file_get_contents('php://input');
$xSignature    = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$signingSecret = 'your-signing-secret-here';

$expected = hash_hmac('sha256', $rawBody, $signingSecret);

if ($expected !== $xSignature) {
    http_response_code(401);
    exit('Invalid signature');
}

$payload = json_decode($rawBody, true);
```

{% endtab %}

{% tab title="Ruby" %}

```ruby
require "openssl"

raw_body       = request.raw_post
x_signature    = request.headers["X-Signature"]
signing_secret = "your-signing-secret-here"

expected = OpenSSL::HMAC.hexdigest("SHA256", signing_secret, raw_body)

raise "Invalid signature" unless expected == x_signature

payload = JSON.parse(raw_body)
```

{% endtab %}
{% endtabs %}

***

## Security recommendations

| Recommendation                         | Detail                                                                                                                                                                                                                         |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Use a timing-safe comparison           | Always compare signatures using a constant-time function (e.g. `hmac.compare_digest`, `CryptographicOperations.FixedTimeEquals`, `hash_equals`). Standard string equality leaks timing information that attackers can exploit. |
| Read the raw body                      | Compute the HMAC over the **raw** request bytes, before any JSON parsing.                                                                                                                                                      |
| Store secrets in environment variables | Never hardcode your `signing_secret` in source code. Use environment variables or a dedicated secrets manager.                                                                                                                 |
| Rotate secrets if compromised          | If you suspect a secret has been exposed, delete the webhook subscription and create a new one to obtain a fresh secret.                                                                                                       |
| Respond promptly                       | Return a `2xx` response as quickly as possible — ideally before doing any heavy processing. Plannr may retry deliveries that do not receive a timely response.                                                                 |


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://api-how-to.plannrcrm.com/webhooks/validating-webhooks.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
