Skip to content

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

  1. Go to Settings > Webhook Signing in the dashboard
  2. Click Generate Secret
  3. 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):

HeaderDescription
X-Recuro-SignatureHMAC-SHA256 hex digest of the signed string
X-Recuro-TimestampUnix 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 hashlib
import hmac
import 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