Webhook Signature Verification
Verify delivery payloads and require signed environment attribution.
Anlyon signs message deliveries when a workspace delivery secret is configured. Event subscriptions can instead use their own generated signing secret. Configure your receiving endpoint to verify the signature before processing the body. A workspace with no signing secret configured sends unsigned deliveries. A receiver that requires verification rejects them.
This page covers the Anlyon-Signature header. It applies to event subscription deliveries and to message deliveries. Action invocations carry no Anlyon delivery signature: Anlyon sends the request your action defines.
Require the expected environment
Workspace signing secrets are currently shared across environments. The earlier
v1 verification authenticates the payload but does not prove which environment
sent it. For a production endpoint, configure the receiver with your production
environment’s UUID. Do not take the expected environment from request headers or
from the request body.
The signature header includes backward-compatible v1, a signed environment
field, and a domain-separated v2 signature:
Anlyon-Signature: t=<unix-seconds>,v1=<hex>,env=<environment-uuid>,v2=<hex>
The input to each HMAC-SHA256 is the exact UTF-8 string below, using the signing secret as the HMAC key:
v1: ${timestamp}.${rawBody}
v2: anlyon-delivery-v2.${timestamp}.${environmentId}.${rawBody}
The literal anlyon-delivery-v2. prefix is part of the signed bytes. Do not omit
it, parse/re-serialize JSON before checking, or substitute the informational
X-Anlyon-Environment-Id header for signed attribution. The SDK validates the
env field inside Anlyon-Signature and requires it to match receiver configuration.
A receiver configured with an expected environment rejects missing attribution, a different environment, invalid signatures, duplicate timestamp/environment fields and timestamps outside its tolerance (300 seconds by default). Empty raw bodies are valid when their signatures match. A timestamp bounds replay age. It does not prevent duplicate processing within that window. Make your handler’s business effects idempotent.
TypeScript
Use an SDK version that has expectedEnvironmentId. Check that your installed
version exposes this option before enabling enforcement.
import { Receiver } from '@anlyonhq/sdk';
const receiver = new Receiver({
signingSecret: process.env.ANLYON_SIGNING_SECRET!,
expectedEnvironmentId: process.env.ANLYON_ENVIRONMENT_ID ?? '',
});
export async function POST(request: Request): Promise<Response> {
const rawBody = await request.text();
if (!receiver.verify({
signature: request.headers.get('Anlyon-Signature'),
body: rawBody,
})) {
return new Response('Invalid delivery signature', { status: 401 });
}
const payload = rawBody ? JSON.parse(rawBody) : null;
// Apply idempotent business logic to the verified payload.
return Response.json({ received: true });
}
For a Next.js App Router endpoint, the adapter takes a handler followed by options
and returns the POST export:
import { serve } from '@anlyonhq/sdk/nextjs';
export const { POST } = serve(async (request) => {
const payload = await request.json();
// Apply idempotent business logic to the verified payload.
return Response.json({ received: true });
}, {
signingSecret: process.env.ANLYON_SIGNING_SECRET!,
expectedEnvironmentId: process.env.ANLYON_ENVIRONMENT_ID ?? '',
});
An explicitly empty expected environment fails receiver initialization, so these examples fail closed if ANLYON_ENVIRONMENT_ID is missing.
Receiver.verifyOrThrow({ signature, body }) raises SignatureVerificationError
instead of returning false. The Next.js, SvelteKit and Cloudflare Workers
serve/verifySignature adapters also accept expectedEnvironmentId.
Cloudflare verification uses Web Crypto. The standalone Node receiver uses Node crypto.
Python
Use the SDK version containing expected_environment_id. Pass the signing secret
directly to Receiver. There is no ReceiverConfig wrapper.
import os
from fastapi import FastAPI, HTTPException, Request
from anlyon import Receiver, SignatureVerificationError
app = FastAPI()
receiver = Receiver(
signing_secret=os.environ["ANLYON_SIGNING_SECRET"],
expected_environment_id=os.environ["ANLYON_ENVIRONMENT_ID"],
)
@app.post("/anlyon/webhook")
async def handle_delivery(request: Request):
try:
raw_body = (await request.body()).decode("utf-8")
receiver.verify_or_throw(
signature=request.headers.get("Anlyon-Signature"),
body=raw_body,
)
except (UnicodeDecodeError, SignatureVerificationError):
raise HTTPException(status_code=401, detail="Invalid delivery signature")
# Parse and process only after verification; make business effects idempotent.
return {"received": True}
receiver.verify(signature=signature, body=raw_body) returns a boolean. Pass an
unchanged UTF-8 string, not a parsed JSON object or bytes.
Migration and rotation
- Configure a workspace delivery secret (or a subscription signing secret), and store it in your receiver’s secret manager. Keep it separate from API keys.
- Upgrade your receiver SDKs. Confirm genuine deliveries include the correct
signed
envandv2fields before enforcing the expected environment. Callbacks queued before attribution was recorded can still arrive withv1-only signatures. Reconcile or drain them before enforcing v2. Do not infer their environment from a payload or a header. - Configure each production receiver with its expected environment UUID. Confirm
a staging delivery is rejected and a production delivery is accepted. A
receiver with no expected environment retains the
v1behavior. It has no environment boundary. - For workspace-secret rotation, Anlyon includes signatures from both the new and previous secrets during the existing five-minute grace window. There is still one timestamp and one environment field. Receivers accept any matching signature for their configured secret. Roll receiver secrets during that overlap. Subscription-secret rotation replaces the secret at once, with no overlap. Coordinate subscription receiver updates separately.
There is no automatic cutoff for v1 receivers. Keeping v1
compatibility does not provide per-environment key isolation: a holder of the
workspace signing secret can sign for any environment. Use separate workspaces
when separate signing-key custody is required today.
Delivery coverage
Environment-bound signatures cover event subscription deliveries. Notification webhooks use a separate HMAC format, described in Notifications. That format names no environment.
Environment-bound signatures also cover ordinary messages (including scheduled deliveries), message success and failure callbacks with recorded attribution, workflow handler requests and workflow failure URL notifications. A workflow failure notification is signed when the workspace has a delivery secret, and it is one best-effort attempt. Verify it like any other signed request, and do not rely on its arrival.
X-Anlyon-Workspace-Id, X-Anlyon-Environment-Id, message IDs and attempt headers
are informational. These MACs cover the timestamp, the environment (in v2) and
the exact body, and nothing else. Caller-supplied environment/signature header overrides
are rejected on message creation and filtered again during delivery.

