Custom webhook recipe
Your own HTTPS endpoint, your own runtime, your own choice of HTTP framework. The production-grade option when you want full control over execution. Examples for Node/Express, Node/Hono, and Python/FastAPI — same contract, different syntax.
Node / Express
typescriptserver.ts — Express + node:crypto
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';
const app = express();
app.use(express.json({
verify: (req, _res, buf) => { (req as any).rawBody = buf.toString('utf8'); },
}));
const FIVE_MIN = 5 * 60 * 1000;
app.post('/cortex', async (req, res) => {
const sig = String(req.headers['x-cortex-signature'] ?? '').replace(/^sha256=/, '');
const ts = String(req.headers['x-cortex-timestamp'] ?? '');
const tsMs = Date.parse(ts);
if (!sig || Number.isNaN(tsMs) || Math.abs(Date.now() - tsMs) > FIVE_MIN) {
return res.status(401).json({ error: 'signature_or_timestamp_invalid' });
}
const expected = createHmac('sha256', process.env.CORTEX_WEBHOOK_SECRET!)
.update(ts + '.' + (req as any).rawBody, 'utf8')
.digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(sig, 'hex');
if (a.length !== b.length || !timingSafeEqual(a, b)) {
return res.status(401).json({ error: 'signature_invalid' });
}
res.status(200).end(); // ack quickly
const trigger = req.body as any;
const externalRef = await runYourChannel(trigger);
await callback(trigger.trigger_id, 'executed', externalRef);
});
async function runYourChannel(_t: unknown): Promise<string> {
return 'msg_' + Math.random().toString(36).slice(2);
}
async function callback(
triggerId: string,
outcome: 'executed' | 'approved' | 'rejected' | 'deferred',
externalRef: string,
) {
await fetch(`https://mcp.cortex.jakeselby.com/v1/triggers/${triggerId}/complete`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CORTEX_PRINCIPAL_TOKEN}`,
'Content-Type': 'application/json',
'Idempotency-Key': `cb_${triggerId}`,
},
body: JSON.stringify({ outcome, external_ref: externalRef, executed_at: new Date().toISOString() }),
});
}
app.listen(3000);Node / Hono
typescriptserver.ts — Hono
import { Hono } from 'hono';
import { createHmac, timingSafeEqual } from 'node:crypto';
const app = new Hono();
const FIVE_MIN = 5 * 60 * 1000;
app.post('/cortex', async (c) => {
const raw = await c.req.text();
const sig = (c.req.header('x-cortex-signature') ?? '').replace(/^sha256=/, '');
const ts = c.req.header('x-cortex-timestamp') ?? '';
const tsMs = Date.parse(ts);
if (!sig || Number.isNaN(tsMs) || Math.abs(Date.now() - tsMs) > FIVE_MIN) {
return c.json({ error: 'signature_or_timestamp_invalid' }, 401);
}
const expected = createHmac('sha256', process.env.CORTEX_WEBHOOK_SECRET!)
.update(ts + '.' + raw, 'utf8')
.digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(sig, 'hex');
if (a.length !== b.length || !timingSafeEqual(a, b)) {
return c.json({ error: 'signature_invalid' }, 401);
}
const trigger = JSON.parse(raw);
// Fire-and-forget; Hono returns 200 once we return below.
void executeAndCallback(trigger);
return c.body(null, 200);
});
async function executeAndCallback(trigger: any) {
// ... same shape as Express example ...
}
export default app;Python / FastAPI
pythonmain.py — FastAPI + hmac stdlib
import hmac, hashlib, os, json, time
from datetime import datetime, timezone
from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
import httpx
app = FastAPI()
WEBHOOK_SECRET = os.environ["CORTEX_WEBHOOK_SECRET"]
PRINCIPAL_TOKEN = os.environ["CORTEX_PRINCIPAL_TOKEN"]
CORTEX_HOST = "https://mcp.cortex.jakeselby.com"
FIVE_MIN = 5 * 60
@app.post("/cortex")
async def cortex_handler(req: Request, background: BackgroundTasks):
raw = (await req.body()).decode("utf-8")
sig = (req.headers.get("x-cortex-signature") or "").removeprefix("sha256=")
ts = req.headers.get("x-cortex-timestamp") or ""
try:
ts_epoch = datetime.fromisoformat(ts.replace("Z", "+00:00")).timestamp()
except ValueError:
raise HTTPException(status_code=401, detail="bad_timestamp")
if abs(time.time() - ts_epoch) > FIVE_MIN:
raise HTTPException(status_code=401, detail="stale")
expected = hmac.new(
WEBHOOK_SECRET.encode("utf-8"),
(ts + "." + raw).encode("utf-8"),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, sig):
raise HTTPException(status_code=401, detail="signature_invalid")
trigger = json.loads(raw)
background.add_task(_execute_and_callback, trigger)
return {}
async def _execute_and_callback(trigger: dict):
external_ref = "msg_" + os.urandom(6).hex() # your channel send
async with httpx.AsyncClient() as client:
await client.post(
f"{CORTEX_HOST}/v1/triggers/{trigger['trigger_id']}/complete",
headers={
"Authorization": f"Bearer {PRINCIPAL_TOKEN}",
"Content-Type": "application/json",
"Idempotency-Key": f"cb_{trigger['trigger_id']}",
},
json={
"outcome": "executed" if trigger["mode"] != "ask" else "approved",
"external_ref": external_ref,
"executed_at": datetime.now(timezone.utc).isoformat(),
},
)Best practices
- Acknowledge within 30s. Return
200as soon as you've verified the signature; execute and callback asynchronously. - Dedupe on
X-Cortex-Idempotency-Key. Store the key with a short TTL (e.g., 24h) and short-circuit on repeat. - Rotate secrets quarterly via
handler.rotate_secret. The 60-second overlap window covers in-flight triggers. - Log the trigger_id in every related log line for cross-reference with
trigger.inspect.