Webhook Signatures
Every webhook request includes a signature in the X-Sturdy-Signature header. This signature allows you to verify that the webhook was sent by Sturdy Technologies and hasn’t been tampered with.
How Signatures Work
We generate signatures using HMAC-SHA256 with your webhook secret key. The signature is calculated directly from the raw request body.
Verification Steps
Get the signature from the request header:
const signature = request . headers [ 'x-sturdy-signature' ];
2. Calculate Expected Signature
const crypto = require ( 'crypto' );
const expectedSignature = crypto
. createHmac ( 'sha256' , webhookSecret )
. update ( requestBody )
. digest ( 'hex' );
3. Compare Signatures
const isValid = crypto . timingSafeEqual (
Buffer . from ( signature ),
Buffer . from ( expectedSignature )
);
if ( ! isValid ) {
throw new Error ( 'Invalid signature' );
}
Complete Example (Node.js)
const express = require ( 'express' );
const crypto = require ( 'crypto' );
const app = express ();
// Raw body needed for signature verification
app . post ( '/webhooks' , express . raw ({ type: 'application/json' }), ( req , res ) => {
const signature = req . headers [ 'x-sturdy-signature' ];
const webhookSecret = process . env . WEBHOOK_SECRET ;
// Verify signature
const expectedSignature = crypto
. createHmac ( 'sha256' , webhookSecret )
. update ( req . body )
. digest ( 'hex' );
const isValid = crypto . timingSafeEqual (
Buffer . from ( signature ),
Buffer . from ( expectedSignature )
);
if ( ! isValid ) {
return res . status ( 401 ). send ( 'Invalid signature' );
}
// Process webhook
const event = JSON . parse ( req . body );
console . log ( 'Received event:' , event . event_type );
res . status ( 200 ). send ( 'OK' );
});
Preventing Replay Attacks
Use the event_id fields in the webhook payload to prevent replay attacks:
const event = JSON . parse ( req . body );
// Check if event was already processed
if ( await isEventProcessed ( event . event_id )) {
return res . status ( 200 ). send ( 'Already processed' );
}
// Mark event as processed
await markEventAsProcessed ( event . event_id );
IP Allowlisting
For additional security, you can allowlist our webhook IP addresses:
203.0.113.10
203.0.113.11
203.0.113.12
Contact support for the current list of webhook IP addresses for production.
Additional Examples
Python
PHP
Go
Java
Spring Boot
C#
ASP.NET Core
Rust
TypeScript
Ruby
Kotlin
Swift
Elixir
import hmac
import hashlib
def verify_webhook ( request , webhook_secret ):
signature = request.headers.get( 'X-Sturdy-Signature' )
# Calculate expected signature
expected_signature = hmac.new(
webhook_secret.encode( 'utf-8' ),
request.body,
hashlib.sha256
).hexdigest()
# Compare signatures
if not hmac.compare_digest(signature, expected_signature):
raise ValueError ( 'Invalid signature' )
return True
Security Checklist
Verify Signatures
Always verify the X-Sturdy-Signature header on every request
Use HTTPS
Only accept webhooks on HTTPS endpoints
Keep Secrets Safe
Store your webhook secret securely (environment variables, secret manager)
Implement Idempotency
Track processed event_id values to prevent duplicate processing