Webhooks security and signature verification
To ensure webhook events are genuine and untampered, all events are cryptographically signed. Verifying the signature confirms the event came from our platform and that the payload hasn't been modified.
This guide covers:
- Signature details: The cryptographic signature of the event.
- Signing secret: Your unique key used to verify incoming signatures.
- Signature verification: How to validate events in your webhook handler.
Signature details
Webhook payloads are signed using HMAC with SHA256. The resulting Base64-encoded signature is included in the webhook-signature header. Use this value to verify that the request is genuine.
Header structure:
A single signature appears as:
webhook-signature: v1,<signature>
Example:
webhook-signature: v1,U5HnozIIxoqswxyYsplgMpo1w5JaUjaPDlg5dm8n1SE=
If more than one signature is included—for example, during a secret rotation—they are space-delimited. Each entry uses the same v1 format.
Example:
webhook-signature: v1,<new_signature> v1,<legacy_signature>
Signing secret
Each webhook subscription has a unique signing secret. This is a confidential key used to generate and verify webhook signatures. Keep this secret secure and never share it publicly.
Find your signing secret
You can retrieve the secret using the API or directly within the 360Learning platform:
Via API:
Use the Retrieve the signing secret of a subscription endpoint.
Request example
curl --request GET \
--url https://app.360learning.com/api/v2/webhooks/subscriptions/507f1f77bcf86cd799439011/secrets \
--header '360-api-version: v2.0' \
--header 'accept: application/json' \
--header 'authorization: Bearer your_access_token' Response example
{
"secret": "password123"
}
Via your 360Learning platform
- Log in ↗ to your 360Learning admin account.
- In the left sidebar, hover over the platform group and click Settings (gear icon).
- Click Webhooks.
- Locate your subscription and click See secret (lock icon).
Rotate the signing secret
Webhook signing secrets do not automatically expire or rotate. If you need to update a secret—whether as part of routine hygiene or in response to a security event— use the Rotate the signing secret of a webhook subscription endpoint.
When you rotate a secret, the new secret takes effect immediately. For the next 24 hours, webhook events are signed with both the new and previous secret. This grace period gives your systems time to deploy configuration changes without breaking signature verification. After 24 hours, the old secret is retired and no longer valid.
Signature verification
We strongly recommend verifying the signature for every incoming event.
Verify signatures via Svix SDKs
We use Svix to deliver webhook events.
To make signature verification easier and more secure, you can use Svix's official SDKs. These libraries automatically handle verifying webhook signatures using the HMAC-SHA256 algorithm, helping ensure that incoming webhook payloads are authentic and untampered.
Using the SDKs is optional, but it can reduce errors and avoid implementing verification logic manually.
Verify signatures manually
You can verify webhook signatures manually to confirm authenticity. This section walks through the conceptual steps. At the end, there’s an optional Node.js example showing how to implement these steps in code.
Before starting, make sure you have:
- An active webhook subscription
- At least one received webhook event
Step 1: Get your signing secret
Each webhook subscription has a unique signing secret. You'll need this to validate the signature.
- When you Create a webhook subscription via our API, you receive
_id, a subscription ID. - Use this ID to Retrieve the signing secret of a webhook subscription.
Save this secret securely for verification. Do not hardcode or expose it publicly.
You’ll use this secret in Step 4 to compute the HMAC for signature verification.
Step 2: Extract values from the request
From the incoming HTTP POST request, extract the following:
webhook-idheader- Example:
msg_35bE2UOtsaBIqUl7VW4mLuR9q2B - You'll use this ID in Step 3 to build the signed content
- Example:
webhook-timestampheader- Example:
1749816652 - You'll use this timestamp in Step 3 to build the signed content
- Example:
- Raw request body exactly as received
- Example:
{"type":"user.created","timestamp":1749816600,"data":{"userId":"684c154ab06eb37545e3cbc0","createdAt":"2025-06-13T12:10:50.568Z","firstName":"Jane","lastName":"Grace","mail":"[email protected]"}} - You'll use this raw body in Step 3 to build the signed content
- Example:
- Signatures from the
webhook-signatureheader- Example:
v1,U5HnozIIxoqswxyYsplgMpo1w5JaUjaPDlg5dm8n1SE= - You'll use the base64 portion (
U5HnozIIxoqswxyYsplgMpo1w5JaUjaPDlg5dm8n1SE=) in Step 5 to verify the computed HMAC
- Example:
Step 3: Construct the signed content
Concatenate the webhook ID, timestamp, and raw request body. Separate each item with a period (.)
signedContent = webhook_id + "." + webhook_timestamp + "." + body
Example:
signedContent = "msg_35bE2UOtsaBIqUl7VW4mLuR9q2B.1749816652.{"type":"user.created","timestamp":1749816600,"data":{"userId":"684c154ab06eb37545e3cbc0","createdAt":"2025-06-13T12:10:50.568Z","firstName":"Jane","lastName":"Grace","mail":"[email protected]"}}"
Use the raw request body JSON string exactly as received. Do not parse, reformat, minify, or re-serialize it; any change will produce a different hash and cause verification to fail.
If your environment automatically parses the body into an object, serialize it using a method that guarantees identical key order and formatting to the original JSON string—e.g.,
JSON.stringify(payload)in JS.
Step 4: Compute the signature
Use an HMAC-SHA256 implementation in your preferred language to compute the signature with the following elements:
- Key: Your webhook signing secret from Step 1
- Message: Your reconstructed signed content from Step 3
- Algorithm: HMAC with SHA256
- Output: Base64-encoded string
The resulting computed signature is what you will compare to the header value(s) in Step 5.
Step 5: Compare signatures
Compare your computed signature to the v1 value(s) in the webhook-signature header.
If multiple signatures are present (e.g., during secret rotation), check all of them; a match with any single v1 signature is sufficient.
Use constant-time comparison when comparing signatures to prevent timing attacks.
Example: Manual verification in JavaScript
For reference, here’s a sample function to manually verify webhook signatures in Node.js.
const crypto = require("crypto");
function isSignatureValid(req, signingSecret) {
// Extract values from the incoming request
const eventId = req.headers["webhook-id"];
const timestamp = req.headers["webhook-timestamp"];
const payload = req.body;
const headerSignatures = req.headers["webhook-signature"];
// Basic validation to ensure all required fields are present
if (!eventId || !timestamp || !payload || !headerSignatures) {
return false;
}
// Construct the signed content
const signedContent = `${eventId}.${timestamp}.${JSON.stringify(payload)}`;
// Compute HMAC-SHA256 signature
const computedSignature = crypto
.createHmac("sha256", Buffer.from(signingSecret, "base64"))
.update(signedContent)
.digest("base64");
// Extract all v1 signatures from the header
const providedSignatures = headerSignatures
.split(" ")
.filter(s => s.startsWith("v1,"))
.map(s => s.split(",")[1]);
// Compare computed signature with each provided signature using timing-safe comparison
return providedSignatures.some(signature => crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(computedSignature)
))
}Updated 7 days ago
