Webhooks
Verify signatures
HMAC signature verification, timestamp tolerance, and replay-attack prevention
Every webhook request from RevKeen carries an X-RevKeen-Signature header. Reject any request that fails signature verification — no exceptions.
The header
X-RevKeen-Signature: t=1705689600,v1=5d2c67a1b4e8f0d3c6a2b9e0f1d8c7b4a9e3f2c5d8b1a4e7f0c3b6a9d2e5f8c1| Component | Meaning |
|---|---|
t=<unix> | Unix timestamp (seconds) when RevKeen signed the request. |
v1=<hex> | HMAC-SHA256 of "{t}.{payload}" using the endpoint's signing secret. |
Verification procedure
- Split the header on
,and parse thet=andv1=components. - Compute the expected signature:
HMAC_SHA256(secret, f"{t}.{raw_body}")— hex-encoded. - Use a constant-time compare to match the expected and received signatures.
- Reject if the timestamp is more than 5 minutes old — prevents replay attacks.
- Only then parse the body and act on the event.
SDK helpers
Every official SDK ships a webhooks.verify() helper that does all of the above in one call. Prefer it over writing your own.
# Extract components from X-RevKeen-Signature header
TIMESTAMP=$(echo "$SIGHDR" | grep -oE 't=[0-9]+' | cut -d= -f2)
SIGNATURE=$(echo "$SIGHDR" | grep -oE 'v1=[a-f0-9]+' | cut -d= -f2)
# Compute expected
EXPECTED=$(printf '%s.%s' "$TIMESTAMP" "$RAW_BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
# Constant-time compare (bash has no native one — use a proper lib in prod)
[ "$EXPECTED" = "$SIGNATURE" ] || exit 1import { RevKeen } from "@revkeen/sdk";
const client = new RevKeen({ apiKey: process.env.REVKEEN_API_KEY! });
// In your webhook handler (Next.js App Router, Hono, Express, etc.)
const rawBody = await request.text(); // Must be the raw string, not JSON.parse'd
const signature = request.headers.get("x-revkeen-signature");
const secret = process.env.REVKEEN_WEBHOOK_SECRET!;
try {
const event = client.webhooks.verify(rawBody, signature!, secret);
// event.type, event.data.object are now trusted
} catch (err) {
return new Response("invalid signature", { status: 400 });
}import (
"io"
"net/http"
revkeen "github.com/RevKeen/sdk-go"
)
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
sig := r.Header.Get("X-RevKeen-Signature")
event, err := revkeen.Webhooks.Verify(body, sig, os.Getenv("REVKEEN_WEBHOOK_SECRET"))
if err != nil {
http.Error(w, "invalid signature", http.StatusBadRequest)
return
}
// event.Type, event.Data.Object are now trusted
}use RevKeen\Webhooks\WebhookVerifier;
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_REVKEEN_SIGNATURE'] ?? '';
$secret = getenv('REVKEEN_WEBHOOK_SECRET');
try {
$event = WebhookVerifier::verify($rawBody, $signature, $secret);
// $event->type, $event->data->object are now trusted
} catch (\RevKeen\Exception\SignatureException $e) {
http_response_code(400);
exit('invalid signature');
}Replay protection
The timestamp t in the signature header is part of the signed payload. Reject deliveries where now - t > 300s (5 minutes). An attacker who captures a valid request cannot replay it after the window expires.
Rotating the signing secret
- Create a new endpoint with the dashboard or API — each endpoint has its own secret.
- Ship the new endpoint's secret to your consumer.
- Delete the old endpoint.
- Alternatively, rotate the existing endpoint's secret — the response returns a new secret; update your consumer before the old secret is invalidated.
Never commit signing secrets to source control. Load them from your secrets manager (Vercel env, Infisical, AWS Secrets Manager, etc.).
Common failure modes
| Symptom | Cause |
|---|---|
| Signature always fails | Reading the request body as JSON before computing HMAC mutates whitespace. Always hash the raw body bytes. |
| Signature fails intermittently | Clock skew between RevKeen and your server. Check NTP. |
| Signature fails in production only | Reverse proxy or WAF is re-encoding the body. Pass raw bytes through. |
| "signature too old" | Retry delay pushed the request past the 5-minute window. Retries carry a fresh timestamp and signature; only delivered-now requests should verify. |