Webhook Authentication
This guide walks you through setting up webhook signing so your endpoints can verify that HTTP requests genuinely come from Recuro.
Step 1: Generate a signing secret
- Go to Settings > Webhook Signing in the dashboard
- Click Generate Secret
- Copy the secret and store it in your application’s environment variables
Step 2: Recuro signs all outbound requests
Once you have a signing secret, Recuro adds two headers to every outbound HTTP request (cron executions, job executions, and completion callbacks):
| Header | Description |
|---|---|
X-Recuro-Signature | HMAC-SHA256 hex digest of the signed string |
X-Recuro-Timestamp | Unix timestamp (seconds) when the request was signed |
The signed string is: {timestamp}.{body}
For example, if the timestamp is 1742659200 and the body is {"status":"ok"}, the signed string is 1742659200.{"status":"ok"}.
Step 3: Verify the signature on your server
Recompute the HMAC-SHA256 digest and compare it to the X-Recuro-Signature header using constant-time comparison.
PHP
$secret = env('RECURO_SIGNING_SECRET');$signature = $request->header('X-Recuro-Signature');$timestamp = $request->header('X-Recuro-Timestamp');$body = $request->getContent();
// Reject stale requests (replay protection)if (abs(time() - (int) $timestamp) > 300) { abort(403, 'Request timestamp too old');}
$expected = hash_hmac('sha256', "{$timestamp}.{$body}", $secret);
if (! hash_equals($expected, $signature)) { abort(403, 'Invalid signature');}Node.js
import crypto from 'node:crypto';
function verifyRecuroWebhook(req, secret) { const signature = req.headers['x-recuro-signature']; const timestamp = req.headers['x-recuro-timestamp']; const body = req.body; // raw string body
// Reject stale requests (replay protection) if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) { throw new Error('Request timestamp too old'); }
const expected = crypto .createHmac('sha256', secret) .update(`${timestamp}.${body}`) .digest('hex');
return crypto.timingSafeEqual( Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'), );}Python
import hashlibimport hmacimport time
def verify_recuro_webhook(body: str, headers: dict, secret: str) -> bool: signature = headers.get('X-Recuro-Signature', '') timestamp = headers.get('X-Recuro-Timestamp', '0')
# Reject stale requests (replay protection) if abs(time.time() - int(timestamp)) > 300: raise ValueError('Request timestamp too old')
expected = hmac.new( secret.encode(), f"{timestamp}.{body}".encode(), hashlib.sha256, ).hexdigest()
return hmac.compare_digest(expected, signature)Replay protection
The X-Recuro-Timestamp header lets you reject stale requests. A common approach is to reject any request where the timestamp is more than 5 minutes (300 seconds) old. This protects against replay attacks where a captured request is resent later.
The verification examples above include replay protection.
Next steps
- Webhook Signing — Technical reference
- Webhook Security Best Practices — HTTPS, signature verification, token rotation