v1 (DEPRECATED)
Our webhooks now include an encrypted signature
Webhook Signing
Webhook payloads are signed with an HMAC-SHA256 signature. A Snappt-Signature header is included on every webhook request, allowing you to cryptographically verify that the request came from Snappt.
It is recommended that you verify the signature on every webhook request, and always make a follow-up request to the API to retrieve the full application data.
Important Details
- Signing key: Your API Key ID (the UUID found in the
apiKeyIdfield of the webhook payload), not the API key secret. - Payload format: Snappt sends the webhook body as compact JSON with no whitespace (no newlines, no indentation, no extra spaces). The signature is computed against this exact string.
- Raw body required: You must use the raw HTTP request body as received — do not parse and re-serialize the JSON before verifying. Many web frameworks (e.g., ASP.NET, Express with JSON middleware) parse the body automatically, which can introduce whitespace, reorder keys, or change formatting. If your framework does this, ensure you capture the raw body string before parsing occurs.
- Hash encoding: The signature uses
base64urlencoding (RFC 4648 §5), which differs from standard base64:+becomes-,/becomes_, and trailing=padding is omitted.
Signature Format
The Snappt-Signature header has the format: t=<timestamp>,v1=<signature>
t— Unix timestamp in milliseconds when the signature was generatedv1— HMAC-SHA256 signature, base64url-encoded
Verification Steps (TypeScript)
import crypto from "crypto";
const body = req.body; // Must be the raw request body string, not parsed JSON
const signature = req.headers["snappt-signature"];
// Step 1: Extract the timestamp and signature from the header
const parts = signature.split(",");
let timestamp: string | undefined;
let v1Sig: string | undefined;
for (const part of parts) {
const [key, value] = part.split("=", 2);
if (key === "t") {
timestamp = value;
} else if (key === "v1") {
v1Sig = value;
}
}
if (!v1Sig || !timestamp) {
throw new Error(`Missing timestamp or signature in ${signature}`);
}
// Step 2: Build the signed payload string: "{timestamp}.{raw body}"
const toSign = `${timestamp}.${body}`;
// Step 3: Compute the expected signature using your API Key ID as the HMAC key
const expected = crypto
.createHmac("sha256", "YOUR_API_KEY_ID")
.update(toSign)
.digest("base64url");
// Step 4: Compare the signatures
if (expected !== v1Sig) {
throw new Error("Webhook signature verification failed");
}Verification Steps (C#)
// Ensure you read the raw body BEFORE any middleware parses it
string body;
using (var reader = new StreamReader(request.Body, Encoding.UTF8, false, 4096, true))
{
body = await reader.ReadToEndAsync();
}
request.Body.Position = 0;
// Extract timestamp and signature from Snappt-Signature header
var header = request.Headers["Snappt-Signature"].ToString();
string? timestamp = null;
string? v1Sig = null;
foreach (var part in header.Split(','))
{
var kv = part.Split('=', 2);
if (kv[0] == "t") timestamp = kv[1];
else if (kv[0] == "v1") v1Sig = kv[1];
}
// Build the signed payload and compute HMAC-SHA256
var toSign = $"{timestamp}.{body}";
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes("YOUR_API_KEY_ID"));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(toSign));
var expected = Convert.ToBase64String(hash)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
if (expected != v1Sig)
throw new Exception("Webhook signature verification failed");Troubleshooting
If signatures do not match, verify the following:
- You are using the raw request body — not a parsed-and-re-serialized version. Frameworks like ASP.NET and Express can silently modify the body (adding whitespace, reordering keys, or inserting
\r\nline endings). - You are using
base64url— not standardbase64. Check for+,/, or trailing=in your output. - You are using the API Key ID — the UUID from the
apiKeyIdfield in the payload, not the API key secret. - You are extracting the timestamp from the header — use the
t=value fromSnappt-Signature, not your own system clock.
Updated 3 days ago