Envelope & headers
The full wire format for an inbound trigger dispatch, every header Cortex sets, the canonical signing string, and the per-action-class payload shapes. This is what your handler sees on the wire.
HTTP request
httpInbound — Cortex → your handler
POST https://handlers.your-app.com/cortex
X-Cortex-Signature: sha256=<hex>
X-Cortex-Trigger-Id: trg_01HXP9...
X-Cortex-Idempotency-Key: idk_01HXP9...
X-Cortex-Schema-Version: 2026-07-20
X-Cortex-Timestamp: 2026-05-27T14:31:00Z
Content-Type: application/json
User-Agent: Cortex-Dispatch/1.0Headers
| Header | Purpose |
|---|---|
X-Cortex-Signature | HMAC-SHA256 over X-Cortex-Timestamp + "." + raw_body using the handler's webhook_secret. Hex-encoded. |
X-Cortex-Trigger-Id | ULID identifying the trigger. Stable across retries; correlation key for trigger.inspect. |
X-Cortex-Idempotency-Key | Dedupe key. Equal to the trigger id on first dispatch; preserved on retries. Your handler MUST treat repeat keys as no-ops. |
X-Cortex-Schema-Version | Envelope schema version. Mirrors schema_version in the body. |
X-Cortex-Timestamp | ISO-8601 dispatch timestamp. Reject any signature for a timestamp more than 5 minutes old (replay protection). |
Body schema
typescriptTriggerEnvelope (TypeScript)
interface TriggerEnvelope {
trigger_id: string;
decision_id: string;
action_class: ActionClass;
mode: "act" | "notify_after" | "ask" | "test";
counterparty?: Counterparty;
payload: ActionPayload; // see per-class shapes below
reasoning: string;
reversal: ReversalContract;
expires_at: string; // ISO-8601, default dispatch+24h
schema_version: string; // "2026-07-20" at launch
links: {
decision_url: string; // dashboard deep-link
ledger_url: string;
};
}
interface Counterparty {
id: string; // e.g. "entity_01HXSARAH"
display_name: string;
kind?: string; // "person" by default; pluggable per ADR-0018
}
interface ReversalContract {
path: string; // human-readable description
window_sec?: number; // optional reversal window (default 86400)
callback_url: string; // POST /v1/triggers/{trigger_id}/reverse
}
type ActionClass = "communicate" | "schedule" | "remind" | string;
type ActionPayload =
| CommunicatePayload
| SchedulePayload
| RemindPayload
| Record<string, unknown>;Per-action-class payloads
communicate
typescript
interface CommunicatePayload {
body: string; // proposed message body
tone?: string; // optional tone hint
channel_hint?: "email" | "sms" | "slack" | "imessage" | string;
preview: {
mime: "text/plain" | "text/html";
body: string;
};
}schedule
typescript
interface SchedulePayload {
title: string;
when: string; // ISO-8601 or natural language
duration_min?: number;
attendees?: Array<{ entity_id?: string; email?: string }>;
preview: {
mime: "text/plain";
body: string; // human-readable calendar block
};
}remind
typescript
interface RemindPayload {
message: string;
when: string;
delivery_hint?: "push" | "email" | "inapp" | string;
preview: {
mime: "text/plain";
body: string;
};
}Full example
jsonInbound dispatch — communicate / ask mode
{
"trigger_id": "trg_01HXP9COMMUNICATE",
"decision_id": "dec_01HXP9COMM",
"action_class": "communicate",
"mode": "ask",
"counterparty": {
"id": "entity_01HXSARAH",
"display_name": "Sarah Chen",
"kind": "person"
},
"payload": {
"body": "Quick check — does Tuesday 2pm still work?",
"tone": "warm",
"channel_hint": "imessage",
"preview": {
"mime": "text/plain",
"body": "To: Sarah Chen via iMessage\n\nQuick check — does Tuesday 2pm still work?"
}
},
"reasoning": "Rule R-12 fired (24h follow-up on rescheduled 1:1); trust L2 for communicate × Sarah.",
"reversal": {
"path": "User can undo in your app's UI within 60 minutes; reversal fires trust demotion.",
"window_sec": 3600,
"callback_url": "https://mcp.cortex.jakeselby.com/v1/triggers/trg_01HXP9COMMUNICATE/reverse"
},
"expires_at": "2026-05-28T14:31:00Z",
"schema_version": "2026-07-20",
"links": {
"decision_url": "https://app.cortex.jakeselby.com/decisions/dec_01HXP9COMM",
"ledger_url": "https://app.cortex.jakeselby.com/ledger/led_01HXP9COMM"
}
}Verification (canonical example)
typescriptverify.ts — runtime-agnostic verifier
import { createHmac, timingSafeEqual } from "node:crypto";
const FIVE_MIN = 5 * 60 * 1000;
export function verifyCortexDispatch(opts: {
headers: Record<string, string>;
rawBody: string;
webhookSecret: string;
now?: () => Date;
}): { ok: true } | { ok: false; reason: string } {
const now = (opts.now ?? (() => new Date()))();
const sig = opts.headers["x-cortex-signature"]?.replace(/^sha256=/, "");
const ts = opts.headers["x-cortex-timestamp"];
if (!sig || !ts) return { ok: false, reason: "missing_headers" };
const tsMs = Date.parse(ts);
if (Number.isNaN(tsMs)) return { ok: false, reason: "bad_timestamp" };
if (Math.abs(now.getTime() - tsMs) > FIVE_MIN) return { ok: false, reason: "stale" };
const expected = createHmac("sha256", opts.webhookSecret)
.update(ts + "." + opts.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 { ok: false, reason: "signature_invalid" };
}
return { ok: true };
}