Events and webhooks
Subscribe an endpoint to approval, invocation and environment events, verify the signature on each delivery, and handle the events.
Anlyon can POST to your application when something happens: an approval is requested or decided, an invocation completes or ends unknown, an environment is halted. A subscription with a signing secret signs each delivery, and the SDKs verify the signature.
Approval and invocation events
Anlyon publishes these events to the reserved anlyon topic in your workspace. Subscribe an endpoint to it with the event types you want, and Anlyon POSTs each matching event there:
const { data } = await client.events.createSubscription('anlyon', {
url: 'https://your-app.com/anlyon/events',
eventTypes: ['approval.*', 'action.*', 'environment.*'],
generateSecret: true,
});
console.log(data!.secret); // the signing secret is returned once, on create
| Event type | When |
|---|---|
approval.requested |
An approval was created, by a gated action or by approvals.request |
approval.approved, approval.denied, approval.expired |
A decision was recorded, or the approval ran out of time |
action.succeeded, action.failed |
Anlyon made the call and recorded the outcome |
action.unknown |
The outcome could not be confirmed. Reconcile before anyone retries |
action.invocation.blocked |
A halt, or a refused check on a governed effect, stopped the call before dispatch. reason says which |
environment.halted, environment.resumed |
The kill switch was thrown or released |
Event types match as prefix globs (approval.*) or exact names.
Each delivery is one JSON envelope:
{
"id": "evt_...",
"topic": "anlyon",
"type": "approval.requested",
"timestamp": "2026-10-01T15:00:00.000Z",
"data": { "environmentId": "...", "approvalId": "apr_..." }
}
data carries environmentId and the id to follow up with (approvalId or invocationId). Read the approval or invocation for the decision, the decider and the outcome.
These events are published to the reserved topic of the workspace’s default environment. Create the subscription with a key bound to that environment.
Events are best-effort. They are a prompt to go and read the record, and the record is the source of truth.
Signature verification
A signed delivery carries an Anlyon-Signature header:
Anlyon-Signature: t=1234567890,v1=abc123def456...
tis the Unix timestamp when the signature was generated.v1is an HMAC-SHA256 of the timestamp and the raw body.
A delivery is signed with the subscription’s secret when the subscription has one, and with the workspace delivery secret otherwise. With neither configured, the delivery is unsigned. The header also carries a signed environment field. See Webhook signature verification for that format and for how to require the expected environment.
Notification webhooks use a different header and format. See Notifications.
Next.js
The SDK has Next.js helpers that verify the signature before your handler runs.
// app/api/anlyon/route.ts
import { serve } from '@anlyonhq/sdk/nextjs';
export const { POST } = serve(async (request) => {
const event = await request.json();
switch (event.type) {
case 'approval.requested':
await notifyReviewers(event.data.approvalId);
break;
case 'action.unknown':
await openReconciliationTicket(event.data.invocationId);
break;
}
return { success: true };
});
verifySignature wraps a handler that returns its own Response:
// app/api/webhook/route.ts
import { verifySignature } from '@anlyonhq/sdk/nextjs';
export const POST = verifySignature(async (request) => {
const event = await request.json();
await handleEvent(event);
return Response.json({ ok: true });
});
Both read the secret from ANLYON_SIGNING_SECRET:
ANLYON_SIGNING_SECRET=your_signing_secret_here
The Receiver class
Outside Next.js, use the Receiver class:
import express from 'express';
import { Receiver } from '@anlyonhq/sdk';
const app = express();
const receiver = new Receiver({
signingSecret: process.env.ANLYON_SIGNING_SECRET!,
});
app.post('/webhooks/anlyon', express.raw({ type: 'application/json' }), async (req, res) => {
const signature = req.headers['anlyon-signature'] as string;
const body = req.body.toString();
// verify() returns a boolean. verifyOrThrow() throws instead.
if (!receiver.verify({ signature, body })) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(body);
if (event.type === 'action.unknown') {
await openReconciliationTicket(event.data.invocationId);
}
res.status(200).send('OK');
});import os
from flask import Flask, request
from anlyon import Receiver
app = Flask(__name__)
receiver = Receiver(os.environ["ANLYON_SIGNING_SECRET"])
@app.route('/webhooks/anlyon', methods=['POST'])
def handle_webhook():
raw_body = request.get_data(as_text=True)
if not receiver.verify(signature=request.headers.get('Anlyon-Signature'), body=raw_body):
return 'Invalid signature', 401
event = request.get_json()
# Handle event["type"] ...
return 'OK', 200Verify against the raw body. A body that was parsed and serialized again does not match.
Good practice
- Verify the signature before you read the body.
- Make the handler safe to call more than once. A delivery can arrive twice. Use the envelope
idto deduplicate. - Answer
200quickly and do slow work afterwards. - Treat an event as a prompt to read the approval or invocation. Do not act on the event body alone.
Testing locally
Using ngrok
# Install ngrok
npm install -g ngrok
# Start your local server
npm start
# In another terminal, expose your local server
ngrok http 3000
# Use the ngrok URL as the subscription URL
# https://abc123.ngrok.io/webhooks/anlyonUsing webhook.site
- Visit webhook.site
- Copy your unique URL
- Use it as the subscription URL while testing
- Read the incoming requests on the site

