> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.monite.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.monite.com/_mcp/server.

# Webhook signatures

## About webhook signatures \[#about]

All [webhooks](./index) that Monite sends to your endpoints include a `Monite-Signature` header. You can verify the signature included in this header to ensure that the received webhooks were actually sent by Monite and haven't been tampered in transit.

The `Monite-Signature` header contains a Unix timestamp and a signature, separated by a comma. The timestamp is prefixed by `t=`, and the signature is prefixed by `v1=`. For example:

```
Monite-Signature: t=1710139795,v1=a9618bc1d19bf749467d59db36c9a7c2eac23052bb45cf5415aa77933ff31f87
```

Monite generates signatures using a hash-based message authentication code (HMAC) with SHA-256, and using a secret key that only you and Monite know.

> **Info**
>
> All API calls mentioned in this guide require a [partner access token](../../get-started/credentials#generate-partner-token).

## Get the signature secret \[#get-secret]

Monite generates a unique signing secret for each webhook subscription. If you create multiple subscription for the same event, these subscriptions will have different secrets. The secrets in the production and sandbox [environments](/api/concepts/environments) are also different.

The secret is returned to you when you initially create a webhook subscription by calling [`POST /webhook_subscriptions`](/api/webhook-subscriptions/post-webhook-subscriptions):

```sh
curl -X POST https://api.sandbox.monite.com/v1/webhook_subscriptions \
     -H 'X-Monite-Version: 2024-05-25' \
     -H 'Authorization: Bearer YOUR_PARTNER_TOKEN' \
     -H 'Content-Type: application/json' \
     -d '{
       "object_type": "entity",
       "event_types": [
         "created",
         "updated",
         "onboarding_requirements.updated",
         "onboarding_requirements.status_updated"
       ],
       "url": "https://yourcompany.com/webhook-listener"
     }'
```

Note down the `secret` from the response, as this is the only time you see it.

```json {4}
{
  "id": "7e209570-afd5-4078-ac33-b8e7409fa790",
  ...
  "secret": "whsec_Iw3mr...",
  "status": "enabled",
  "url": "https://yourcompany.com/webhook-listener"
}
```

> **Error**
>
> Keep the secret safe and secure. Do not hard-code it in your application's source code. Instead, use environment variables or secure secrets management tools.

## Regenerate a secret \[#regenerate-secret]

For improved security, we recommend that you generate a new signing secret periodically. You can also regenerate the secret if you suspect it has been compromised.

To generate a new secret for a webhook subscription, call [`POST /webhook_subscriptions/{webhook_subscription_id}/regenerate_secret`](/api/webhook-subscriptions/post-webhook-subscriptions-id-regenerate-secret). The subscription ID can be retrieved, for example, from the `webhook_subscription_id` field in the webhook events that you receive from Monite.

```sh
curl -X POST https://api.sandbox.monite.com/v1/webhook_subscriptions/7e209...790/regenerate_secret \
     -H 'X-Monite-Version: 2024-05-25' \
     -H 'Authorization: Bearer YOUR_PARTNER_TOKEN'
```

The old secret expires immediately, and a new secret is returned in the response:

```json
{
  ...
  "secret": "whsec_Cw5xp..."
}
```

New webhooks triggered by this subscription from now on will be signed using the new secret.

## How to verify webhook signatures \[#verify]

To verify a webhook's signature, use the secret key to generate your own signature for the given webhook payload. Follow these steps:

### 1. Extract the timestamp and signature from the header \[#extract-timestamp-and-signature]

Split the `Monite-Signature` header value by the `,` character to get a list of elements. Next, split each element by the `=` character to get key-value pairs.

The timestamp is the value of the `t` key, and the signature is the value of the `v1` key. You can ignore any other keys.

### 2. Verify the timestamp (Recommended) \[#verify-timestamp]

To prevent [replay attacks](https://en.wikipedia.org/wiki/Replay_attack), we recommend that you check the signature timestamp `t` against the current time and reject webhooks whose timestamp is too far off in the past or future from the current time. The acceptable time difference can be, for example, 5 minutes or less.

This requires that your server's time be synchronized using [NTP](https://en.wikipedia.org/wiki/Network_Time_Protocol) or other means.

### 3. Calculate the expected signature \[#calculate-expected-signature]

Concatenate the value of the `t` timestamp with the character `.` and the contents of the webhook's raw request body. For example:

```
1713173964.{"id":"06c003f1-6b05-415f-be6d-39ecacdddbd3","created_at":"2024-04-14T09:38:55.593225+00:00","action":"counterpart.created",...}
```

Next, calculate the HMAC signature of the resulting string using the SHA-256 hash function and the signing secret of your Monite webhook subscription.

This gives you the expected signature for the specified timestamp and webhook event payload.

### 4. Compare the signatures \[#compare-signatures]

Compare your calculated expected signature with the actual signature (`v1` value) extracted from the webhook's `Monite-Signature` header.

If the signatures match, the webhook can be considered a legitimate Monite webhook. If the signatures do not match, the webhook should be rejected.

## Example

Below is sample Python code that demonstrates how to verify Monite webhook signatures. The default acceptable time difference is 5 minutes (300 seconds). Error handling is omitted for simplicity.

> **Note**
>
> In this code example, `raw_request` value must be a *string* (not a `dict`) containing the exact request body text received in the webhook.
>
> **Do not pretty-print or otherwise format the webhook request body** (for example, convert it between string and `dict`) because this can result in a signature mismatch.

```python
# Usage:
#
# secret = "whsec_Iw3xp..."               # Replace with your Monite webhook secret
# header = "t=1710139795,v1=a9618bc1..."  # The value of the Monite-Signature header in a webhook
# raw_request = '{"id":"06c003f1-6b05-415f-be6d-39ecacdddbd3","created_at":"2024-04-14T09:38:55.593225+00:00","action":"counterpart.created",...}'
#
# is_webhook_valid = verify_webhook(raw_request, header, secret)


from hashlib import sha256
import hmac
import time


def get_timestamp_and_signature(header: str) -> tuple[int, str]:
    timestamp = None
    signature = None

    for item in header.split(","):
        key, value = item.strip().split("=", 2)
        # Take only the first occurrences of t and v1
        if key == "t" and timestamp is None:
            timestamp = int(value)
        elif key == "v1" and signature is None:
            signature = value

    return timestamp, signature


def verify_webhook(payload: str, hmac_header: str, secret: str, tolerance: int = 300) -> bool:
    timestamp, signature = get_timestamp_and_signature(hmac_header)

    if abs(time.time() - timestamp) > tolerance:
        raise Exception(
            f"The signature timestamp is outside the tolerance range ({tolerance} seconds).",
        )

    # Calculate the expected signature
    payload_to_sign = f"{timestamp}.{payload}"
    expected_signature = hmac.new(
        secret.encode("utf-8"),
        msg=payload_to_sign.encode("utf-8"),
        digestmod=sha256,
    ).hexdigest()

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