Verify webhooks
Verify that WriftAI webhook requests are authentic and untampered
Short on time?
If you just want to verify webhook requests, skip ahead to Verification Steps or Using official clients.
Why you should verify webhooks
Webhook endpoints are public by design, which means anyone on the internet can send requests to them. If your application treats every incoming webhook as trusted, an attacker could:
- Fake a prediction update (e.g. mark a prediction as
succeeded) - Trigger downstream jobs in your system
- Insert malicious or incorrect data into your database
- Spam your webhook endpoint to cause operational issues
To prevent this, WriftAI supports signed webhooks which lets you verify the request:
- Was sent by WriftAI (authenticity)
- Was not modified in transit (integrity)
- Is recent enough to reduce replay attacks (freshness)
The signature header
When a webhook secret is configured, WriftAI includes the wriftai-webhook-signature header on every webhook request.
Header format
The signature header is composed of a timestamp followed by a list of signatures and their corresponding version identifiers.
The header is formatted as a comma-separated list of key-value pairs: t=timestamp,<version>=signature[,<version>=signature...]
Where:
timestampis a Unix timestamp (in seconds) indicating when the request was signed<version>is the identifier for the signing schemesignatureis a hex-encoded HMAC digest
The signature list is most commonly of length one, though there may be multiple signatures present. Multiple signatures can be included to support key rotation or signing scheme evolution.
Example:
t=1729168452,v1=4f9c2a6b8e3d1a7c0f5b9d6e2c8a4e1f7d3b5a9c6e8f2d4a1b0c9e7f6a3d8b2,v1=9a3e6f5c2b7d4a8f1c0e9b6d5a2f3e8c7d4b1a0f9e6c5b2a8d7f4e3c1b9,v2=8c7a2e9d5f4b6c1a0e3d9f8b2c5a7e6d4f1b9a8c3e5d7a6b2f0c4Your verifier should treat the webhook request as valid if any signature in the header matches the expected value for a supported signing scheme.
Signing Scheme Version
Currently, WriftAI uses the v1 signing scheme. Additional versions may be added in the future.
What is signed
To generate the signature, WriftAI constructs a string to sign in the format: timestamp.payload
Where:
timestampis the value from the signature headerpayloadis the raw request body exactly as sent
This string is then signed using your webhook secret.
Signature algorithm
WriftAI computes webhook signatures using the following parameters:
| Parameter | Value |
|---|---|
| Algorithm | HMAC |
| Hash function | SHA-256 |
| Signing key | Your webhook secret |
| Signed message | timestamp.payload |
The resulting HMAC digest is hex-encoded and included in the signature header.
Preventing replay attacks
A replay attack occurs when an attacker captures a legitimate webhook request and re-sends it at a later time. If your server accepts the request again, this can lead to duplicate processing, incorrect state transitions, or unintended side effects.
Recommended timestamp tolerance
When verifying a webhook request, you should check that the timestamp in the signature header is recent.
A common and recommended approach is to:
- Reject requests with timestamps older than a short window (for example, 5 minutes)
- Allow a small amount of clock skew between your server and WriftAI (for example, accept timestamps within ±5 minutes of your server’s current time)
Verification Steps
To verify an incoming webhook request:
Read the raw request body.
Do not parse or modify the body before verification.
Extract the signature header
Read the wriftai-webhook-signature header from the request.
Parse the header values
- Extract the timestamp (
t) - Extract all versioned signatures (
<version>=signature)
Validate the timestamp
- Compute the age of the request using the timestamp
- Reject the request if it falls outside your allowed time window (for example, ±5 minutes)
Reconstruct the string to sign
timestamp.payload Where payload is the raw request body.
Compute the expected signature
- Use the signing algorithm for the supported scheme
- Sign the string using your webhook secret
Compare signatures securely
- Compare the computed signature against each signature provided in the header
- Use a constant-time comparison function
- Treat the request as valid if any signature matches
Reject unsupported schemes
- Ignore signatures with versions you do not support
Use the raw request body
Verification must be done using the raw request body, exactly as received. Parsing JSON and re-serializing it may change whitespace or key ordering and cause signature verification to fail.
If no provided signature matches, the webhook request must be rejected.
Verifying webhooks using official clients
While you are free to implement webhook verification yourself, WriftAI provides verification helpers in its official clients.
from wriftai import verify_webhook, WebhookSignatureVerificationError
try:
# use default tolerance and scheme
verify_webhook(
payload=raw_body, # bytes exactly as received
signature=signature_header,
secret=webhook_secret,
)
# Signature is valid — continue processing the webhook
except WebhookSignatureVerificationError:
# Signature verification failed
# Reject the webhook and do not process itimport { verifyWebhook, SignatureVerificationError } from 'wriftai';
try {
// use default tolerance and scheme
await verifyWebhook(
rawBody, // Uint8Array, exactly as received
signatureHeader,
webhookSecret,
);
// Signature is valid — continue processing the webhook
} catch (err) {
if (err instanceof WebhookSignatureVerificationError) {
// Signature verification failed
// Reject the webhook and do not process it
} else {
// Unexpected error
throw err;
}
}import (
"errors"
"github.com/wriftai/wriftai-go"
)
err := wriftai.VerifyWebhook(
rawBody, // []byte exactly as received
signatureHeader,
webhookSecret,
nil, // Use default tolerance
nil, // Use default scheme
)
if err == nil {
// Signature is valid — continue processing the webhook
} else if errors.Is(err, wriftai.ErrWebhookTimestampOutsideTolerance) ||
errors.Is(err, wriftai.ErrWebhookNoTimestamp) ||
errors.Is(err, wriftai.ErrWebhookNoSignatures) ||
errors.Is(err, wriftai.ErrWebhookSignatureMismatch) {
// Signature verification failed
// Reject the webhook and do not process it
} else {
// Unexpected error
panic(err)
}Advanced verification options
You can customize verification behavior by providing optional arguments such as timestamp tolerance or signing scheme. In most cases, the defaults are sufficient. Refer to the API reference of each client for more details.