--- title: Verifying Signatures | SafetyKit description: Deploy AI agents to automate risk reviews, onboarding, and investigations. Trusted by leading marketplaces and fintech platforms. --- ## Why Verify Webhooks Because of the way webhooks work, attackers can impersonate services by simply sending a fake webhook to an endpoint. Think about it: it’s just an HTTP POST from an unknown source. This is a potential security hole for many applications, or at the very least, a source of problems. In order to prevent it, SafetyKit signs every webhook and its metadata with a unique key for each endpoint. This signature can then be used to verify the webhook indeed comes from SafetyKit, and only process it if it is. Another potential security hole is [replay attacks](https://en.wikipedia.org/wiki/Replay_attack). Imagine if an attacker intercepts a valid payload (including the signature), and then re-transmits it to your endpoint. This payload will pass signature validation, and will therefore be acted upon by your system. To mitigate this attack, SafetyKit includes a timestamp for when the webhook attempt occurred. Our libraries automatically reject webhooks with a timestamp that are more than five minutes away (past or future) from the current time. This requires your server’s clock to be synchronised and accurate, and it’s recommended that you use [NTP](https://en.wikipedia.org/wiki/Network_Time_Protocol) to achieve this. ## Where to find your signing secret In order to verify your webhook signature, you first need your webhook signing secret. You can find the webhook signing secret in the SafetyKit dashboard webhook settings for each endpoint. Each endpoint has its own unique secret. This value is typically formatted as `whsec_...` and is different from your API key. Recommended setup: - copy the endpoint signing secret from the webhook settings page - store it server-side as an environment variable (for example, `SAFETYKIT_WEBHOOK_SECRET`) - use that value with the Svix `Webhook(...)` verifier in your webhook handler ## Verifying Webhooks with a Library The simplest way to verify SafetyKit webhooks is with the [svix open source library](https://docs.svix.com/receiving/verifying-payloads/how). - [ TypeScript](#tab-panel-24) - [ Python](#tab-panel-25) ``` import { Webhook } from "svix"; const verifier = new Webhook(process.env.SAFETYKIT_WEBHOOK_SECRET!); const event = verifier.verify(rawBody, { "webhook-id": req.headers["webhook-id"] as string, "webhook-timestamp": req.headers["webhook-timestamp"] as string, "webhook-signature": req.headers["webhook-signature"] as string, }); ``` ``` from svix.webhooks import Webhook verifier = Webhook("whsec_your_signing_secret") event = verifier.verify(raw_body, request_headers) ``` ## Verifying Webhooks Manually Each webhook call includes three headers with additional information that are used for verification: - `webhook-id`: the unique message identifier for the webhook message. This identifier is unique across all messages, but will be the same when the same webhook is being resent (e.g. due to a previous failure). - `webhook-timestamp`: timestamp in [seconds since epoch](https://en.wikipedia.org/wiki/Unix_time). - `webhook-signature`: the [Base64](https://en.wikipedia.org/wiki/Base64) encoded list of signatures (space delimited). ### Constructing the signed content The content to sign is composed by concatenating the id, timestamp and payload, separated by the full-stop character (`.`). In code, it will look something like: ``` signedContent = "${webhook_id}.${webhook_timestamp}.${body}" ``` Where `body` is the raw body of the request. The signature is sensitive to any changes, so even a small change in the body will cause the signature to be completely different. This means that you should not change the body in any way before verifying. ### Determining the expected signature SafetyKit uses an [HMAC](https://en.wikipedia.org/wiki/Hash-based_message_authentication_code) with [SHA-256](https://en.wikipedia.org/wiki/SHA-2) to sign its webhooks. So to calculate the expected signature, you should HMAC the `signed_content` from above using the base64 portion of your signing secret (this is the part after the `whsec_` prefix) as the key. For example, given the secret `whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw` you will want to use `MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw`. For example, this is how you can calculate the signature: - [ TypeScript](#tab-panel-26) - [ Python](#tab-panel-27) ``` import crypto from "crypto"; const signedContent = `${webhook_id}.${webhook_timestamp}.${body}`; const secret = "whsec_5WbX5kEWLlfzsGNjH64I8lOOqUB6e8FH"; // Need to base64 decode the secret const secretBytes = Buffer.from(secret.split("_")[1], "base64"); const signature = crypto .createHmac("sha256", secretBytes) .update(signedContent) .digest("base64"); console.log(signature); ``` ``` import base64 import hashlib import hmac signed_content = f"{webhook_id}.{webhook_timestamp}.{body}" secret = "whsec_5WbX5kEWLlfzsGNjH64I8lOOqUB6e8FH" # Need to base64 decode the secret secret_bytes = base64.b64decode(secret.split("_", 1)[1]) signature = base64.b64encode( hmac.new(secret_bytes, signed_content.encode("utf-8"), hashlib.sha256).digest() ).decode("utf-8") print(signature) ``` This generated signature should match one of the ones sent in the `webhook-signature` header. The `webhook-signature` header is composed of a list of space delimited signatures and their corresponding version identifiers. The signature list is most commonly of length one. Though there could be any number of signatures. For example: ``` v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE= v1,bm9ldHUjKzFob2VudXRob2VodWUzMjRvdWVvdW9ldQo= v2,MzJsNDk4MzI0K2VvdSMjMTEjQEBAQDEyMzMzMzEyMwo= ``` Make sure to remove the version prefix and delimiter (e.g. `v1,`) before verifying the signature. Please note that to compare the signatures it’s recommended to use a constant-time string comparison method in order to prevent [timing attacks](https://en.wikipedia.org/wiki/Timing_attack). ### Verify timestamp As mentioned above, SafetyKit also sends the timestamp of the attempt in the webhook-timestamp header. You should compare this timestamp against your system timestamp and make sure it’s within your tolerance in order to prevent replay attacks.