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

# Webhook signatures

> Verify Tesouro webhook signatures using HMAC-SHA256 to ensure incoming events are authentic and untampered.

## About webhook signatures

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

The `Tesouro-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:

```lines theme={null}
Tesouro-Signature: t=1710139795,v1=a9618bc1d19bf749467d59db36c9a7c2eac23052bb45cf5415aa77933ff31f87
```

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

<Info>
  All API calls mentioned in this guide require a [partner access token](/finops/guides/authentication/client-credentials).
</Info>

## Get the signature secret

Tesouro 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](/finops/api/concepts/environments) are also different.

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

```sh lines theme={null}
curl -X POST https://api.sandbox.tesouro.com/v1/webhook-subscriptions \
     -H 'X-Finops-Version: 2025-06-23' \
     -H 'Authorization: Bearer YOUR_ACCESS_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} lines theme={null}
{
  "id": "7e209570-afd5-4078-ac33-b8e7409fa790",
  ...
  "secret": "whsec_Iw3mr...",
  "status": "enabled",
  "url": "https://yourcompany.com/webhook-listener"
}
```

<Danger>
  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.
</Danger>

## Regenerate a 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`](/finops/reference/openapi/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 Tesouro.

```sh lines theme={null}
curl -X POST https://api.sandbox.tesouro.com/v1/webhook-subscriptions/7e209...790/regenerate-secret \
     -H 'X-Finops-Version: 2025-06-23' \
     -H 'Authorization: Bearer YOUR_ACCESS_TOKEN'
```

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

```json lines theme={null}
{
  ...
  "secret": "whsec_Cw5xp..."
}
```

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

## How to verify webhook signatures

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

Split the `Tesouro-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)

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

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

```lines theme={null}
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 Tesouro webhook subscription.

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

### 4. Compare the signatures

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

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

## Example

Below is sample Python code that demonstrates how to verify Tesouro 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.
</Note>

```python expandable lines theme={null}
# Usage:
#
# secret = "whsec_Iw3xp..."               # Replace with your Tesouro webhook secret
# header = "t=1710139795,v1=a9618bc1..."  # The value of the Tesouro-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)
```
