Dispatch

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.0

Headers

HeaderPurpose
X-Cortex-SignatureHMAC-SHA256 over X-Cortex-Timestamp + "." + raw_body using the handler's webhook_secret. Hex-encoded.
X-Cortex-Trigger-IdULID identifying the trigger. Stable across retries; correlation key for trigger.inspect.
X-Cortex-Idempotency-KeyDedupe key. Equal to the trigger id on first dispatch; preserved on retries. Your handler MUST treat repeat keys as no-ops.
X-Cortex-Schema-VersionEnvelope schema version. Mirrors schema_version in the body.
X-Cortex-TimestampISO-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 };
}