Signature Verification
Every webhook delivery from amser includes a cryptographic signature in the X-Amser-Signature header. You must verify this signature before processing any event to confirm the request originated from amser and was not tampered with in transit.
Signature Format
The header value follows this pattern:
X-Amser-Signature: sha256=<hex-encoded HMAC>
This matches the convention used by Stripe and GitHub webhooks. The value after sha256= is a hex-encoded HMAC-SHA256 digest.
How Verification Works
- Retrieve the raw request body as a string (do not parse it first)
- Compute the HMAC-SHA256 of the raw body using your webhook signing secret
- Prefix the hex digest with
sha256= - Compare the computed value to the
X-Amser-Signatureheader using constant-time comparison
You must use the raw request body for verification. If you parse the JSON and re-serialize it, whitespace or key ordering differences will produce a different digest and verification will fail.
Webhook Signing Secret
Your webhook signing secret is generated when you configure a webhook endpoint in the amser dashboard. It is shown once at creation — store it immediately in an environment variable. If you lose the secret, you must rotate the endpoint to generate a new one.
Replay Protection
Use the deterministic id field in every event as an idempotency key. Store processed event IDs and skip any event you have already handled. This is the most reliable defense against replay attacks and also protects you from duplicate deliveries during retries.
Timestamp tolerance (optional)
The created_at field is a Unix timestamp (seconds) set when the event is first created. Because amser retries failed deliveries for up to 8 hours using the original payload, the created_at value is not refreshed on retries. If you enforce a timestamp tolerance, set it to at least 32 400 seconds (9 hours) to avoid rejecting legitimate retries.
The created_at field is included in the signed payload body, so an attacker cannot modify it without invalidating the signature.
Code Examples
Node.js
import crypto from 'crypto';
export function verifyWebhookSignature(
payload: string, // raw request body as string
signature: string, // X-Amser-Signature header value
secret: string // webhook signing secret from dashboard
): boolean {
const expected = `sha256=${crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex')}`;
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
Usage in an Express handler:
import express from 'express';
const app = express();
// Use raw body parser — required for signature verification
app.post(
'/webhooks/amser',
express.raw({ type: 'application/json' }),
async (req, res) => {
const signature = req.headers['x-amser-signature'] as string;
const rawBody = req.body.toString('utf-8');
if (!verifyWebhookSignature(rawBody, signature, process.env.AMSER_WEBHOOK_SECRET!)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(rawBody);
// Idempotency: skip events you have already processed
if (await alreadyProcessed(event.id)) {
return res.status(200).send('OK');
}
// Acknowledge receipt before async processing
res.status(200).send('OK');
// Process the event asynchronously
processEvent(event).catch(console.error);
}
);
Python
import hmac
import hashlib
def verify_webhook_signature(
payload: bytes, # raw request body as bytes
signature: str, # X-Amser-Signature header value
secret: str # webhook signing secret from dashboard
) -> bool:
expected = "sha256=" + hmac.new(
secret.encode("utf-8"),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
Usage in a Flask handler:
from flask import Flask, request, abort
app = Flask(__name__)
@app.route("/webhooks/amser", methods=["POST"])
def handle_webhook():
signature = request.headers.get("X-Amser-Signature", "")
raw_body = request.get_data()
if not verify_webhook_signature(raw_body, signature, os.environ["AMSER_WEBHOOK_SECRET"]):
abort(401, "Invalid signature")
event = request.get_json(force=True)
# Idempotency: skip events you have already processed
if already_processed(event["id"]):
return "OK", 200
# Acknowledge receipt before async processing
# (in production, enqueue the event for background processing)
return "OK", 200
Security Checklist
- Use constant-time comparison. Both
crypto.timingSafeEqual(Node.js) andhmac.compare_digest(Python) prevent timing attacks. Never use===or==for signature comparison. - Store the secret in an environment variable. Do not hardcode webhook secrets in source code or commit them to version control.
- Always verify before processing. Reject unsigned or incorrectly signed requests immediately. Do not process the payload first and verify later.
- Deduplicate with the event
id. Store processed event IDs and skip duplicates. This is the primary defense against replays and duplicate deliveries. - Optionally enforce timestamp tolerance. If you check
created_at, allow at least 32 400 seconds (9 hours) to accommodate the retry schedule. - Return HTTP 200 before async work. Respond with a 2xx status code before performing any long-running processing. amser retries on non-2xx responses, so a slow handler may cause duplicate deliveries.
Next Steps
- Review the Events Reference for the full list of event types and payloads
- Understand the Retry Behaviour to know when amser will redeliver events
- Generate an API Key for authenticating your server-side requests to the amser API