Recipes

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 200 as 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.