Webhook signatures

About webhook signatures

All webhooks 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.

All API calls mentioned in this guide require a partner access token.

Get the signature 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 are also different.

The secret is returned to you when you initially create a webhook subscription by calling POST /webhook_settings:

1curl -X POST https://api.sandbox.monite.com/v1/webhook_settings \
2 -H 'X-Monite-Version: 2023-09-01' \
3 -H 'Authorization: Bearer YOUR_PARTNER_TOKEN' \
4 -H 'Content-Type: application/json' \
5 -d '{
6 "object_type": "entity",
7 "event_types": [
8 "created",
9 "updated",
10 "onboarding_requirements.updated",
11 "onboarding_requirements.status_updated"
12 ],
13 "url": "https://yourcompany.com/webhook-listener"
14 }'

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

1{
2 "id": "7e209570-afd5-4078-ac33-b8e7409fa790",
3 ...
4 "secret": "whsec_Iw3mr...",
5 "status": "enabled",
6 "url": "https://yourcompany.com/webhook-listener"
7}

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

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_settings/{webhook_subscription_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.

1curl -X POST https://api.sandbox.monite.com/v1/webhook_settings/7e209...790/regenerate_secret \
2 -H 'X-Monite-Version: 2023-09-01' \
3 -H 'Authorization: Bearer YOUR_PARTNER_TOKEN'

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

1{
2 ...
3 "secret": "whsec_Cw5xp..."
4}

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 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)

To prevent replay attacks, 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 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:

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

1# Usage:
2#
3# secret = "whsec_Iw3xp..." # Replace with your Monite webhook secret
4# header = "t=1710139795,v1=a9618bc1..." # The value of the Monite-Signature header in a webhook
5# raw_request = '{"id":"06c003f1-6b05-415f-be6d-39ecacdddbd3","created_at":"2024-04-14T09:38:55.593225+00:00","action":"counterpart.created",...}'
6#
7# is_webhook_valid = verify_webhook(raw_request, header, secret)
8
9
10from hashlib import sha256
11import hmac
12import time
13
14
15def get_timestamp_and_signature(header: str) -> tuple[int, str]:
16 timestamp = None
17 signature = None
18
19 for item in header.split(","):
20 key, value = item.strip().split("=", 2)
21 # Take only the first occurrences of t and v1
22 if key == "t" and timestamp is None:
23 timestamp = int(value)
24 elif key == "v1" and signature is None:
25 signature = value
26
27 return timestamp, signature
28
29
30def verify_webhook(payload: str, hmac_header: str, secret: str, tolerance: int = 300) -> bool:
31 timestamp, signature = get_timestamp_and_signature(hmac_header)
32
33 if abs(time.time() - timestamp) > tolerance:
34 raise Exception(
35 f"The signature timestamp is outside the tolerance range ({tolerance} seconds).",
36 )
37
38 # Calculate the expected signature
39 payload_to_sign = f"{timestamp}.{payload}"
40 expected_signature = hmac.new(
41 secret.encode("utf-8"),
42 msg=payload_to_sign.encode("utf-8"),
43 digestmod=sha256,
44 ).hexdigest()
45
46 return hmac.compare_digest(expected_signature, signature)