Webhook Signing
Recuro can sign every outbound HTTP request with HMAC-SHA256 so your endpoints can verify the request genuinely came from Recuro and has not been tampered with.
How it works
When you generate a signing secret in Settings → Webhook Signing, Recuro adds two headers to every outbound request (crons, jobs, and 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 built by concatenating the timestamp, a period, and the raw request body:
{timestamp}.{body}For example, if the timestamp is 1742659200 and the body is {"status":"ok"}, the signed string is:
1742659200.{"status":"ok"}Verifying signatures
To verify a request, recompute the HMAC-SHA256 digest using the same signed-string format and compare it to the X-Recuro-Signature header using a constant-time comparison.
PHP
$secret = env('RECURO_SIGNING_SECRET');$signature = $request->header('X-Recuro-Signature');$timestamp = $request->header('X-Recuro-Timestamp');$body = $request->getContent();
$expected = hash_hmac('sha256', "{$timestamp}.{$body}", $secret);
if (! hash_equals($expected, $signature)) { abort(403, 'Invalid signature');}Node.js
import crypto from 'node:crypto';
function verifySignature(body, signature, timestamp, secret) { 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 hmac
def verify_signature(body: str, signature: str, timestamp: str, secret: str) -> bool: 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 old:
$timestamp = (int) $request->header('X-Recuro-Timestamp');
if (abs(time() - $timestamp) > 300) { abort(403, 'Request timestamp too old');}This protects against replay attacks where a captured request is resent later.
Managing your secret
- Go to Settings → Webhook Signing in the Recuro dashboard.
- Click Generate secret (or Regenerate secret if one already exists).
- Copy the secret and store it in your application’s environment variables.