Skip to content
Anlyon
Esc
↑↓navigate↵open⌘Jpreview
On this page

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

  1. 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.
  2. Upgrade your receiver SDKs. Confirm genuine deliveries include the correct signed env and v2 fields before enforcing the expected environment. Callbacks queued before attribution was recorded can still arrive with v1-only signatures. Reconcile or drain them before enforcing v2. Do not infer their environment from a payload or a header.
  3. 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 v1 behavior. It has no environment boundary.
  4. 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.

Was this page helpful?