Skip to content

Data model and API

Purpose

This is the reference the other docs import from: TypeScript-style definitions for every core type, the event catalog (names, payloads, producers, consumers), the REST and GraphQL endpoints for the IC surface, the Owner Console and admin, the webhook contracts for inbound integrations and outbound customer subscriptions, and the error codes. Where a type is specified in depth elsewhere (Org Model nodes in Org Model Spec, run internals in Harness and Orchestration), the definition here is the canonical wire shape and the other doc is the semantics. When they disagree, this doc is wrong and should be fixed to match.

Scope / Non-scope

In scope

  • Conventions: identifiers, time, versioning, pagination, authentication, tenancy, idempotency.
  • Core types: tenancy and identity, Org Model, runs and events, decisions and receipts, memory, outreach and threads, escalations and handoffs, graduation, integrations, content, audit.
  • Event catalog with payloads, producers, consumers.
  • Endpoints: /v1/me (IC surface), /v1/owner (Owner Console), /v1/admin, /v1/graphql.
  • Inbound webhook contracts per provider family; outbound webhook contract.
  • Error codes with HTTP mapping and retry guidance.

Non-scope

1. Conventions

Topic Rule
Identifiers Prefixed ULIDs: tnt_ prs_ rol_ org_ gol_ prj_ cad_ pol_ dec_ edg_ chg_ snp_ run_ evt_ rcp_ mem_ esc_ hnd_ thr_ msg_ out_ pkt_ whk_ int_ cnt_ aud_. Prefix mismatches are E_VALIDATION.
Time ISO 8601 UTC with milliseconds on the wire (2026-09-03T14:05:00.000Z); local times only inside briefWindow/quietHours as HH:mm with an IANA timezone.
Versioning Path-versioned: /v1. Additive changes do not bump; removals and semantic changes do. Event and webhook payloads carry schemaVersion.
Pagination Cursor-based: ?limit=50&cursor=…; responses carry nextCursor (opaque, expires in 24 h). Max limit 200.
Authentication Humans: OIDC session (console, app, admin) → bearer JWT with sub=prs_…, tenant=tnt_…, roles. Surfaces: adapter-authenticated (Slack user id → Person via identity), never user tokens. Automation: tenant-scoped API keys with named scopes (org:read, org:write, runs:read, audit:export, webhooks:manage); API keys can never decide HUMAN_ONLY packets or approve PROPOSE_CONFIRM decisions. Internal services: mTLS.
Tenancy Every request resolves exactly one tenant from the token; X-Tenant-Id is optional and must match (E_TENANT_MISMATCH otherwise). Portfolio grants switch tenant per request via X-Tenant-Id.
Idempotency All POSTs accept Idempotency-Key (UUID). The same key with the same body returns the original response with Idempotent-Replayed: true; a different body is E_IDEMPOTENCY_CONFLICT. Keys are retained 7 days.
Concurrency Writes to Org Model nodes accept If-Match: <version>; mismatch is E_ORG_VERSION_CONFLICT.
Reasons Every mutating request that changes the Org Model, a policy, a graduation state, or memory requires reason (non-empty string).
Errors { "error": { "code": string, "message": string, "details"?: object, "requestId": string, "docs": string } }.
Rate limits Per tenant per key: 600 req/min read, 120 req/min write; Retry-After on 429.

2. Core types

2.1 Scalars and identity

type TenantId = `tnt_${string}`;   type PersonId = `prs_${string}`;   type RoleId = `rol_${string}`;
type OrgId = `org_${string}`;      type GoalId = `gol_${string}`;     type ProjectId = `prj_${string}`;
type CadenceId = `cad_${string}`;  type PolicyId = `pol_${string}`;   type DecisionId = `dec_${string}`;
type EdgeId = `edg_${string}`;     type ChangeId = `chg_${string}`;   type SnapshotId = `snp_${string}`;
type RunId = `run_${string}`;      type EventId = `evt_${string}`;    type ReceiptId = `rcp_${string}`;
type MemoryFactId = `mem_${string}`; type EscalationId = `esc_${string}`; type HandoffId = `hnd_${string}`;
type ThreadId = `thr_${string}`;   type MessageId = `msg_${string}`;  type OutreachId = `out_${string}`;
type PacketId = `pkt_${string}`;   type WebhookId = `whk_${string}`;  type IntegrationId = `int_${string}`;
type ContentId = `cnt_${string}`;  type AuditId = `aud_${string}`;
type ISODateTime = string; type ISODate = string;

type ChonkId = 'chonksworth' | 'fin' | 'ops' | 'human' | 'porpoise' | 'build' | 'marketing' | 'revenue' | 'security';
type AutonomyClass = 'AUTONOMOUS' | 'PROPOSE_CONFIRM' | 'HUMAN_ONLY';
type ExecutionMode = 'SHADOW' | 'PROPOSE_CONFIRM' | 'AUTONOMOUS' | 'HUMAN_ONLY' | 'BLOCKED';
type GraduationState = 'SHADOW' | 'PROPOSE' | 'AUTONOMOUS';
type Surface = 'slack' | 'teams' | 'sms' | 'voice' | 'app' | 'email' | 'console';
type ManagementFunction =
  | 'goal_setting' | 'planning' | 'checkins_unblocking' | 'feedback_growth' | 'staffing_scheduling'
  | 'hiring_onboarding' | 'budget_approvals' | 'reporting_escalation' | 'rituals_comms' | 'quiet_hr' | 'security_compliance';
type ConnectorProvider =
  | 'linear' | 'jira' | 'github' | 'asana' | 'notion' | 'gdrive'
  | 'slack' | 'msteams' | 'gmail' | 'gcal' | 'outlook' | 'twilio'
  | 'gusto' | 'rippling' | 'deel' | 'bamboohr' | 'workday'
  | 'quickbooks' | 'xero' | 'ramp' | 'brex' | 'stripe' | 'hubspot' | 'intercom'
  | 'square' | 'toast' | 'shopify_pos' | 'deputy' | 'wheniwork' | 'sevenshifts' | 'homebase' | 'timeclock' | 'iot'
  | 'mcp';

interface Actor { kind: 'human' | 'run' | 'system' | 'api_key'; id: string; onBehalfOf?: PersonId }
interface NodeRef { kind: NodeKind; id: string }
interface ExternalRef { provider: ConnectorProvider; externalId: string; url?: string; lastSyncedAt: ISODateTime }

2.2 Tenant

interface Tenant {
  id: TenantId;
  name: string;
  mode: 'FULL_SUITE' | 'ORG' | 'FUNCTION';
  atoms: boolean;
  phase: 'ONBOARDING' | 'LISTENING' | 'ACTIVE' | 'PAUSED' | 'OFFBOARDING';
  listening?: { startedAt: ISODateTime; endsAt: ISODateTime };
  scope: { orgIds?: OrgId[]; functions?: ManagementFunction[] };   // ORG / FUNCTION modes
  cellId: string;
  schemaVersion: string;
  plan: { perPersonDailyModelUsd: number; managedPeopleCap?: number };
  retention: Record<'work_signal' | 'feedback' | 'preference' | 'consideration' | 'decision_log' | 'recognition' | 'audit', string>;  // ISO 8601 durations or 'until_offboarded+P30D'
  modelProvider: { tiers: Record<'fast' | 'standard' | 'reasoning', string[]>; zeroRetentionOnly: true };
  createdAt: ISODateTime;
}

interface PortfolioGrant { granteeId: PersonId; tenantId: TenantId; scope: 'health' | 'approvals' | 'full'; grantedBy: PersonId; grantedAt: ISODateTime }

2.3 Org Model (wire shapes; semantics in doc 02)

type NodeKind = 'Person' | 'Role' | 'Org' | 'Goal' | 'Project' | 'Cadence' | 'Policy' | 'Decision';
interface NodeBase { id: string; tenantId: TenantId; kind: NodeKind; version: number; validFrom: ISODateTime; validTo?: ISODateTime; createdAt: ISODateTime; updatedAt: ISODateTime; labels?: string[]; externalRefs?: ExternalRef[] }

interface Identity { provider: ConnectorProvider; externalId: string; email?: string; handle?: string; displayName?: string; verified: boolean; linkedAt: ISODateTime; linkedBy: Actor; confidence: number }

interface Person extends NodeBase {
  kind: 'Person'; personKind: 'human' | 'agent';
  status: 'invited' | 'active' | 'on_leave' | 'offboarding' | 'offboarded' | 'merged';
  displayName: string; givenName?: string; familyName?: string; pronouns?: string; timezone: string; locale: string;
  identities: Identity[];
  contact: { preferredSurface: Surface; surfaceOrder: Surface[]; workEmail?: string; phoneE164?: string };
  briefWindow?: { start: string; end: string }; quietHours?: { start: string; end: string };
  employment?: { type: 'employee' | 'contractor' | 'intern' | 'agent_worker'; startDate: ISODate; endDate?: ISODate; laborClass?: 'exempt' | 'non_exempt' | 'hourly'; hrisRef?: ExternalRef; isMinor?: boolean };
  agent?: { agentKind: 'chonk' | 'worker'; chonkId?: ChonkId; endpoint?: string; charterRef?: string };
  isOwner: boolean; mergedInto?: PersonId;
}
interface Role extends NodeBase { kind: 'Role'; title: string; family: string; band?: string; responsibilities: string[]; skills?: string[]; certifications?: string[]; shiftEligible?: boolean; headcountPlan?: { period: string; planned: number } }
interface Org extends NodeBase { kind: 'Org'; name: string; orgKind: 'company' | 'department' | 'team' | 'location' | 'shift_group' | 'portfolio_slot'; timezone: string; managedFunctions?: ManagementFunction[]; location?: { address: string; operatingHours: Array<{ dow: number; open: string; close: string }>; laborJurisdiction: string; posRef?: ExternalRef; schedulingRef?: ExternalRef }; parentId?: OrgId; managerChonkId?: ChonkId; managerOfRecordId?: PersonId }
interface Goal extends NodeBase { kind: 'Goal'; title: string; description?: string; level: 'company' | 'org' | 'person'; period: { start: ISODate; end: ISODate }; metric?: { name: string; unit: string; target: number; baseline?: number; current?: number; direction: 'up' | 'down'; sourceRef?: ExternalRef }; status: 'draft' | 'proposed' | 'active' | 'achieved' | 'missed' | 'cancelled'; health: 'green' | 'yellow' | 'red' | 'unknown'; healthReason?: string; healthAsOf?: ISODateTime; sandbagFlag?: { flaggedAt: ISODateTime; reason: string; receiptId: ReceiptId }; ownerId?: PersonId; reviewCadenceId?: CadenceId }
interface Project extends NodeBase { kind: 'Project'; title: string; status: 'proposed' | 'active' | 'blocked' | 'paused' | 'done' | 'cancelled'; health: 'green' | 'yellow' | 'red' | 'unknown'; healthReason?: string; dates: { start?: ISODate; target?: ISODate; actualEnd?: ISODate }; estimate?: { unit: 'person_weeks' | 'points' | 'shifts'; value: number; confidence: number }; scope?: string; orgId?: OrgId; ownerId?: PersonId }
type CadenceKind = 'morning_brief' | 'async_checkin' | 'one_on_one' | 'daily_standup' | 'weekly_review' | 'monthly_retro' | 'quarterly_planning' | 'all_hands' | 'owner_update' | 'goal_review' | 'pipeline_review' | 'shift_huddle' | 'schedule_publish' | 'checklist' | 'custom';
interface Cadence extends NodeBase { kind: 'Cadence'; name: string; cadenceKind: CadenceKind; ownerChonkId: ChonkId; format: 'written' | 'video' | 'live' | 'sms' | 'voice' | 'comic'; schedule: { rrule: string; timezone: string; window?: { start: string; end: string }; graceMinutes: number }; scope: { orgId?: OrgId; personId?: PersonId }; inputs: Array<{ kind: 'org_model' | 'mirror' | 'memory' | 'reply'; spec: string }>; outputs: Array<{ kind: 'message' | 'document' | 'video' | 'decision' | 'org_change'; spec: string }>; status: 'active' | 'paused' | 'retired'; effectiveness?: { replyRate30d?: number; openRate30d?: number; lastTunedAt?: ISODateTime; tuneReason?: string }; priority: 'P0' | 'P1' | 'P2' | 'P3' | 'P4' | 'P5' }
type PolicyKind = 'hard_line' | 'threshold' | 'autonomy_override' | 'outreach_cap' | 'labor_rule' | 'approval_route' | 'retention' | 'tone';
interface Policy extends NodeBase { kind: 'Policy'; name: string; policyKind: PolicyKind; rule: PolicyRule; autonomyClass?: AutonomyClass; precedence: number; setBy: PersonId; effective: { from: ISODateTime; to?: ISODateTime }; appliesTo?: Array<{ kind: 'Org' | 'Role' | 'Person' | 'Function'; id: string }> }
type PolicyRule = Record<string, unknown>;   // AST defined in doc 05
type DecisionKind = 'spend_approval' | 'plan_change' | 'goal_set' | 'req_open' | 'cadence_change' | 'escalation_to_owner' | 'schedule_exception' | 'org_change' | 'identity_merge' | 'human_only_packet' | 'graduation_change' | 'custom';
interface Decision extends NodeBase { kind: 'Decision'; decisionKind: DecisionKind; autonomyClass: AutonomyClass; effectiveMode: ExecutionMode; proposedByChonkId: ChonkId; runId: RunId; summary: string; recommendation?: string; receiptId: ReceiptId; status: 'proposed' | 'confirmed' | 'rejected' | 'executed' | 'expired' | 'recorded' | 'stale' | 'shadow'; routedTo?: PersonId[]; decidedBy?: PersonId; decidedAt?: ISODateTime; humanNote?: string; expiresAt?: ISODateTime; outcome?: { observedAt: ISODateTime; summary: string; receiptId?: ReceiptId } }

type EdgeType = 'member_of' | 'holds_role' | 'child_of' | 'managed_by' | 'manager_of_record' | 'reports_to' | 'escalates_to' | 'owns' | 'contributes_to' | 'depends_on' | 'reviewed_by' | 'participates_in' | 'applies_to' | 'about' | 'backs_up' | 'scheduled_at' | 'assigned_to';
interface Edge { id: EdgeId; tenantId: TenantId; type: EdgeType; from: NodeRef; to: NodeRef; attrs: Record<string, unknown>; version: number; validFrom: ISODateTime; validTo?: ISODateTime; reason: string; actor: Actor }

type ChangeOp =
  | { op: 'create_node'; node: NodeBase } | { op: 'update_node'; id: string; patch: JsonPatch; before?: object }
  | { op: 'end_node'; id: string; validTo: ISODateTime; newStatus?: string }
  | { op: 'create_edge'; edge: Omit<Edge, 'id' | 'version' | 'validFrom' | 'actor' | 'reason'> }
  | { op: 'end_edge'; id: EdgeId; validTo: ISODateTime }
  | { op: 'merge_person'; from: PersonId; into: PersonId; edgeMap?: Array<{ edgeId: EdgeId; originalFrom: PersonId }> };
interface Change { id: ChangeId; tenantId: TenantId; version: number; at: ISODateTime; actor: Actor; reason: string; reasonCode?: string; runId?: RunId; decisionId?: DecisionId; ops: ChangeOp[]; invariantsChecked: string[]; schemaVersion: string }
interface MergeProposal { id: string; tenantId: TenantId; a: PersonId; b: PersonId; rule: 'M3' | 'M4' | 'M5'; conflict: boolean; evidence: Array<{ provider: ConnectorProvider; detail: string; score: number }>; status: 'open' | 'accepted' | 'rejected'; decidedBy?: PersonId; decidedAt?: ISODateTime }

2.4 Runs and events

type TriggerKind = 'CADENCE_TICK' | 'INTEGRATION_EVENT' | 'IC_MESSAGE' | 'ESCALATION' | 'HANDOFF' | 'HUMAN_DECISION' | 'FOLLOWUP' | 'REPLAY' | 'MANUAL';
type Trigger =
  | { kind: 'CADENCE_TICK'; cadenceId: CadenceId; fireAt: ISODateTime; participantId?: PersonId }
  | { kind: 'INTEGRATION_EVENT'; provider: ConnectorProvider; deliveryId: string; eventType: string; subjectRefs: NodeRef[]; signal?: Signal }
  | { kind: 'IC_MESSAGE'; surface: Surface; threadId: ThreadId; messageId: MessageId; personId: PersonId }
  | { kind: 'ESCALATION'; escalationId: EscalationId; stage: number }
  | { kind: 'HANDOFF'; handoffId: HandoffId; fromChonk: ChonkId; toChonk: ChonkId }
  | { kind: 'HUMAN_DECISION'; decisionId: DecisionId; status: 'confirmed' | 'rejected' | 'expired'; actorId?: PersonId; resumeRunId?: RunId }
  | { kind: 'FOLLOWUP'; parentRunId: RunId; stage: number; dueAt: ISODateTime }
  | { kind: 'REPLAY'; sourceRunId: RunId; mode: 'VERIFY' | 'SHADOW_REEXEC' | 'LIVE_REEXEC'; nonce: string }
  | { kind: 'MANUAL'; actorId: PersonId; objective: string; clientNonce: string };

interface Signal { id: string; kind: 'stall' | 'variance' | 'uncovered_shift' | 'callout' | 'goal_drift' | 'capacity' | 'anomaly' | 'compliance_due'; ruleId: string; subjectRefs: NodeRef[]; detectedAt: ISODateTime; evidence: SourceRef[]; severity: 'info' | 'watch' | 'risk' | 'critical' }

type RunKind = 'morning_brief' | 'ic_reply' | 'checkin' | 'one_on_one' | 'chase' | 'followup' | 'weekly_review' | 'owner_update' | 'quarterly_planning' | 'replan' | 'approval' | 'schedule_publish' | 'shift_swap' | 'checklist' | 'escalation' | 'handoff' | 'onboarding_draft' | 'listening_intro' | 'content_render' | 'integrity_check' | 'custom';
type RunState = 'REQUESTED' | 'DEDUPLICATED' | 'COALESCED' | 'QUEUED' | 'DEFERRED' | 'EXPIRED' | 'ADMITTED' | 'RUNNING' | 'WAITING_HUMAN' | 'WAITING_EXTERNAL' | 'SUCCEEDED' | 'BUDGET_EXCEEDED' | 'FAILED_RETRYABLE' | 'FAILED' | 'DEAD_LETTER' | 'CANCELLED';
type FailureClass = 'MODEL_TRANSIENT' | 'MODEL_OUTPUT_INVALID' | 'TOOL_TRANSIENT' | 'CONNECTOR_RATE_LIMITED' | 'CONNECTOR_UNAVAILABLE' | 'CONNECTOR_AUTH' | 'POLICY_BLOCKED' | 'BUDGET_EXCEEDED' | 'ORG_INVARIANT' | 'LEASE_LOST' | 'SURFACE_DELIVERY' | 'INTERNAL';

interface RunBudget { maxModelCalls: number; maxInputTokens: number; maxOutputTokens: number; maxWallMs: number; maxToolCalls: number; maxOutreach: number; maxCostUsd: number }
interface Usage { modelCalls: number; inputTokens: number; outputTokens: number; cachedTokens: number; wallMs: number; toolCalls: number; outreach: number; costUsd: number }

interface Run {
  id: RunId; tenantId: TenantId; chonkId: ChonkId; runKind: RunKind; trigger: Trigger;
  idempotencyKey: string; subjectKey: string; priority: 'P0' | 'P1' | 'P2' | 'P3' | 'P4' | 'P5';
  deadline?: ISODateTime; expirable: boolean; coalescable: boolean; budget: RunBudget;
  state: RunState; attempt: number; orgModelVersion: number; functionModes: Partial<Record<ManagementFunction, ExecutionMode>>;
  requestedAt: ISODateTime; startedAt?: ISODateTime; endedAt?: ISODateTime; usage: Usage;
  pendingDecisions: DecisionId[]; failure?: { class: FailureClass; message: string; retryable: boolean };
  parentRunId?: RunId; correlationId: string; replay?: { sourceRunId: RunId; mode: string }; degraded?: boolean;
}

interface Event<T extends string = string, P = unknown> { id: EventId; tenantId: TenantId; seq: number; ts: ISODateTime; type: T; payload: P; runId?: RunId; stepId?: string; correlationId: string; causationId?: EventId; actor: Actor; schemaVersion: string; hash: string }

type ActionKind = 'SEND_MESSAGE' | 'ORG_CHANGE' | 'CONNECTOR_WRITE' | 'SCHEDULE_FOLLOWUP' | 'HANDOFF' | 'ESCALATE' | 'RENDER_CONTENT' | 'OPEN_REQ' | 'PACKET' | 'APPROVE_SPEND' | 'SCHEDULE_SHIFT' | 'SWAP_SHIFT' | 'OFFER_DISCOUNT' | 'BOOK_MEETING';
interface ProposedAction { id: string; kind: ActionKind; subKind?: string; args: Record<string, unknown>; subjectRefs: NodeRef[]; receiptId: ReceiptId; declaredClass: AutonomyClass; fn: ManagementFunction; reason: string }
interface Verdict { mode: ExecutionMode; reasons: RuleEvaluation[]; gates: string[]; policyIds: PolicyId[]; narrowedBy?: PolicyId; decidedBy?: PersonId }

2.5 Receipts, packets, escalations, handoffs

interface SourceRef { n: number; kind: 'org' | 'mirror' | 'memory' | 'service' | 'policy' | 'message' | 'decision'; ref: string; label: string; asOf: ISODateTime; url?: string }
interface RuleEvaluation { ruleId: string; input: Record<string, unknown>; result: 'pass' | 'fail' | 'threshold_exceeded' | 'n/a'; detail: string }
interface Receipt { id: ReceiptId; runId: RunId; tenantId: TenantId; createdAt: ISODateTime; sources: SourceRef[]; snapshot: Array<{ source: ConnectorProvider | 'org_model' | 'memory'; asOf: ISODateTime; staleSince?: ISODateTime; version?: number }>; ruleEvaluations: RuleEvaluation[]; rationale: string; confidence: number; gaps: string[]; modelCalls: Array<{ purpose: string; tier: string; requestHash: string }>; visibility: 'self' | 'self_mor' | 'self_owner' | 'owner' }

interface Packet { id: PacketId; tenantId: TenantId; decisionId: DecisionId; kind: 'hiring' | 'termination' | 'compensation' | 'promotion' | 'discipline' | 'legal' | 'safety' | 'protected_characteristic' | 'other'; subjectRefs: NodeRef[]; preparedByChonkId: ChonkId; routedTo: PersonId[]; contents: { summary: string; facts: SourceRef[]; options?: string[]; noRecommendation: true }; accessLog: Array<{ personId: PersonId; at: ISODateTime }>; humanDecision?: { by: PersonId; at: ISODateTime; text: string }; status: 'sent' | 'viewed' | 'recorded' }

interface Escalation { id: EscalationId; tenantId: TenantId; kind: 'IC_TO_HUMAN' | 'CADENCE_MISSED' | 'DEPENDENCY_UNRESPONSIVE' | 'POLICY_VIOLATION' | 'SAFETY' | 'INTEGRITY' | 'CONNECTOR' | 'BUDGET' | 'OWNER_DECISION'; severity: 'low' | 'medium' | 'high' | 'critical'; openedBy: Actor; openedAt: ISODateTime; subjectRefs: NodeRef[]; scopeOrgId?: OrgId; route: Array<{ stage: number; to: PersonId; via: Surface[]; dueAt: ISODateTime; deliveredAt?: ISODateTime; acknowledgedAt?: ISODateTime }>; receiptId?: ReceiptId; message?: string; suppressible: false; status: 'open' | 'acknowledged' | 'resolved'; resolvedBy?: PersonId; resolvedAt?: ISODateTime; resolutionNote?: string }

type HandoffReason = 'people_problem' | 'budget_impact' | 'scope_change' | 'security_or_safety' | 'owner_escalation' | 'cross_org_dependency' | 'recognition' | 'quiet_hr' | 'deal_desk' | 'other';
interface Handoff { id: HandoffId; tenantId: TenantId; fromChonk: ChonkId; toChonk: ChonkId; reason: HandoffReason; subjectRefs: NodeRef[]; requestedAction: string; urgency: 'now' | 'today' | 'this_week'; contextBundleRef: string; receiptId: ReceiptId; correlationId: string; createdByRunId: RunId; status: 'requested' | 'accepted' | 'declined' | 'completed'; declineReason?: string }

2.6 Memory

type MemoryFactKind = 'preference' | 'working_agreement' | 'commitment' | 'blocker_history' | 'signal' | 'feedback_given' | 'feedback_received' | 'growth_plan' | 'recognition' | 'consideration' | 'decision_ref' | 'owner_report';
interface MemoryFact { id: MemoryFactId; tenantId: TenantId; subjectId: PersonId; kind: MemoryFactKind; content: string; source: SourceRef; sharedBy: 'self' | 'work_system' | 'chonk' | PersonId; visibility: 'self' | 'self_mor' | 'self_owner' | 'org_public'; retentionClass: 'work_signal' | 'feedback' | 'preference' | 'consideration' | 'decision_log' | 'recognition'; expiresAt: ISODateTime; writtenByRunId: RunId; writtenByChonkId: ChonkId; usedFor: string[]; subjectResponse?: { at: ISODateTime; text: string }; redactedAt?: ISODateTime }
interface KnowsPage { personId: PersonId; generatedAt: ISODateTime; facts: MemoryFact[]; edges: Edge[]; decisions: Decision[]; sources: Array<{ provider: ConnectorProvider; whatIsRead: string[]; whatIsNotRead: string[] }>; accessLog: Array<{ at: ISODateTime; chonkId: ChonkId; runKind: RunKind; factIds: MemoryFactId[] }>; escalationChain: Array<{ personId: PersonId; displayName: string; orgId: OrgId }> }

2.7 Outreach and threads

interface Thread { id: ThreadId; tenantId: TenantId; personId: PersonId; surface: Surface; providerRef: { provider: ConnectorProvider; channelId: string; threadTs?: string }; status: 'active' | 'read_only' | 'closed'; createdAt: ISODateTime }
interface Message { id: MessageId; tenantId: TenantId; threadId: ThreadId; direction: 'inbound' | 'outbound'; authorId: PersonId; body: string; attachments?: Array<{ kind: 'receipt' | 'video' | 'image' | 'document'; ref: string }>; receiptId?: ReceiptId; providerMessageId?: string; sentAt: ISODateTime; runId?: RunId }
type OutreachKind = 'brief' | 'checkin' | 'reply' | 'chase' | 'status' | 'feedback' | 'approval_request' | 'decision_notice' | 'escalation' | 'packet_notice' | 'schedule' | 'huddle' | 'checklist' | 'celebration' | 'intro' | 'owner_update' | 'fallback' | 'system';
interface OutreachRequest { id: OutreachId; tenantId: TenantId; runId: RunId; actionId: string; recipientId: PersonId; kind: OutreachKind; context: 'safety' | 'human_only_packet' | 'wellbeing' | 'feedback_concern' | 'rejection_notice' | 'escalation' | 'incident' | 'approval_request' | 'plan_change' | 'brief' | 'checkin' | 'reply' | 'review' | 'owner_update' | 'celebration' | 'all_hands' | 'launch'; body: string; receiptId: ReceiptId; toneUsed: number; surfaceHint?: Surface; aboutPersonIds?: PersonId[]; exempt?: 'escalation' | 'safety' | 'callout' | 'cadence_missed'; coalesceKey?: string }
interface OutreachResult { id: OutreachId; status: 'delivered' | 'queued' | 'suppressed' | 'failed' | 'coalesced'; surface?: Surface; messageId?: MessageId; reason?: 'cap_messages' | 'cap_chases' | 'quiet_hours' | 'tone_ceiling' | 'thread_read_only' | 'surface_unavailable'; retryAt?: ISODateTime }

2.8 Graduation, integrations, content, audit

interface Graduation { tenantId: TenantId; fn: ManagementFunction; state: GraduationState; since: ISODateTime; ownerCap?: GraduationState; stats: { runs: number; proposals: number; confirmedUnedited: number; edited: number; rejected: number; overrides: number; incidents: number; windowStart: ISODateTime }; history: Array<{ at: ISODateTime; from: GraduationState; to: GraduationState; reason: string; actor: Actor }> }
interface ShadowLogEntry { id: string; tenantId: TenantId; runId: RunId; chonkId: ChonkId; fn: ManagementFunction; action: ProposedAction; verdict: Verdict; renderedBody?: string; review?: { by: PersonId; at: ISODateTime; verdict: 'would_accept' | 'would_edit' | 'would_reject'; note?: string } }

interface Integration { id: IntegrationId; tenantId: TenantId; provider: ConnectorProvider; kind: 'mirror' | 'actuator' | 'both'; scopes: string[]; status: 'connected' | 'degraded' | 'needs_reauth' | 'disconnected'; health: { lastEventAt?: ISODateTime; lastSyncAt?: ISODateTime; staleSince?: ISODateTime; errorRate1h: number }; feeds: ManagementFunction[]; connectedBy: PersonId; connectedAt: ISODateTime }
interface IntegrationEvent { id: string; tenantId: TenantId; provider: ConnectorProvider; deliveryId: string; eventType: string; receivedAt: ISODateTime; occurredAt?: ISODateTime; subjectRefs: NodeRef[]; externalRefs: ExternalRef[]; payloadHash: string; normalized: Record<string, unknown> }

interface ContentJob { id: ContentId; tenantId: TenantId; runId: RunId; format: 'text' | 'video' | 'comic' | 'card' | 'audio'; chonkId: ChonkId; script: string; citations: SourceRef[]; voiceProfileRef: string; toneUsed: number; template: string; status: 'queued' | 'rendering' | 'ready' | 'failed'; assetUrl?: string; durationSec?: number; receiptId: ReceiptId }

interface AuditEntry { id: AuditId; tenantId: TenantId; seq: number; ts: ISODateTime; actor: Actor; action: 'policy.verdict' | 'outreach.sent' | 'data.access' | 'org.change' | 'memory.write' | 'memory.redact' | 'decision.human' | 'connector.write' | 'export' | 'purge' | 'graduation.change' | 'escalation.open' | 'system.degraded'; subject: NodeRef[]; why: string; receiptId?: ReceiptId; runId?: RunId; prevHash: string; hash: string }
interface AuditExport { id: string; tenantId: TenantId; requestedBy: PersonId; range: { from: ISODateTime; to: ISODateTime }; format: 'jsonl'; status: 'queued' | 'ready' | 'expired'; url?: string; chainHead: string; anchoredAt?: ISODateTime }

3. Event catalog

All events use the Event envelope. Producers write; consumers are idempotent on (tenantId, seq). "RM" = read models; "AUD" = audit-service; "SCH" = scheduler; "EXE" = executor; "OUT" = outreach-service; "ESC" = escalation-router; "MET" = metrics; "WHK" = outbound webhooks (only events marked ext).

Event type Payload (key fields) Producer Consumers
org.node.created / org.node.updated / org.node.ended { changeId, node: NodeBase, patch?, reason, actor } orgmodel-api RM, AUD, SCH (cadence index), MET
org.edge.created / org.edge.ended { changeId, edge: Edge } orgmodel-api RM, AUD, SCH
org.change.applied { change: Change } orgmodel-api AUD, RM
org.person.merged / org.person.unmerged { from, into, edgeMap, reason } orgmodel-api memory-service (rekey), surface adapters, AUD, WHK ext
org.person.offboarding_started / org.person.offboarded { personId, endDate, reason } orgmodel-api memory-service (retention), OUT (thread read-only), SCH, AUD, WHK ext
org.merge_proposal.opened / .decided { proposal: MergeProposal } matcher admin UI, AUD
org.import.rejected { provider, externalRef, invariant, detail } orgmodel-api admin UI, Chief Build Officer weekly (via RM)
org.snapshot.created / org.migration.applied { snapshotId | migrationId, version } orgmodel-api AUD, ops
run.requested / run.queued / run.deduplicated / run.coalesced / run.deferred / run.expired { run: Run } / { runId, original } / { runId, into } / { runId, resumeAt } SCH MET, admin UI
run.admitted / run.started / run.step.started / run.step.completed { run } / { runId, attempt, resume } / { stepId, kind } / { stepId, outputHash, usage, modelCalls } SCH / EXE MET, replay index
run.waiting / run.resumed { runId, reason: 'human' | 'external', pending } EXE SCH (lease release), admin UI
run.succeeded / run.failed / run.budget_exceeded / run.cancelled / run.dead_lettered { runId, usage, degraded? } / { runId, class, message } / { runId, dimension, usage } EXE / SCH MET, AUD, ESC (dead-letter, windowed failures), WHK ext (run.dead_lettered)
tool.called { runId, stepId, toolId, argsHash, resultHash, sources: SourceRef[] } tool gateway AUD (data.access), replay index
cadence.due / cadence.executed / cadence.skipped / cadence.missed / cadence.degraded / cadence.tuned { cadenceId, participantId?, fireAt, runId?, reason? } SCH / EXE ESC (missed), MET, Owner Console health, WHK ext (missed)
integration.event.received { event: IntegrationEvent } ingress RM, detectors, AUD
integration.sync.completed / .connector.degraded / .connector.restored / .connector.needs_reauth { provider, stats | staleSince | error } connectors admin UI, ESC (degraded > 6 h), Owner Console header, WHK ext
signal.detected / signal.cleared { signal: Signal } / { signalId, reason } detectors SCH (run request), RM, MET
policy.evaluated { runId, action: ProposedAction, verdict: Verdict } EXE / OUT AUD, MET, graduation stats
policy.violation_blocked { runId, action, verdict } EXE AUD, graduation (regress), ESC, WHK ext
policy.changed { policyId, before, after, reason, actor } orgmodel-api policy cache, AUD, WHK ext
action.executed / action.shadowed { runId, action, verdict, receiptId, result? } EXE AUD, shadow log, MET
decision.proposed / .confirmed / .rejected / .executed / .expired / .stale / .recorded / .overridden { decision: Decision, actor? } EXE / console-api OUT (notices), SCH (timers), AUD, graduation stats, WHK ext (all)
decision.outcome_observed { decisionId, outcome } analyst runs AUD, Owner Console
outreach.requested / .sent / .suppressed / .failed / .coalesced / .replied { request: OutreachRequest, result: OutreachResult } / { messageId, personId } OUT / surface adapters AUD (sent), MET, RM (thread), memory (owner_report on symmetry)
escalation.opened / .routed / .delivered / .acknowledged / .resolved / .unacknowledged { escalation: Escalation, stage? } ESC OUT, Owner Console, AUD, WHK ext (opened, resolved, unacknowledged)
handoff.requested / .accepted / .declined / .completed { handoff: Handoff } EXE SCH (run request), AUD, Chonksworth triage (declined ×2)
memory.fact.written / .redacted / .expired / .rekeyed { fact: MemoryFact } / { factId, by, reason } memory-service AUD, knows page RM
memory.read { subjectId, chonkId, runKind, factIds } memory-service AUD (data.access), knows page access log
graduation.advanced / .regressed / .capped { fn, from, to, reason, actor, receiptId? } harness / console-api policy cache, OUT (owner + MoR notice), AUD, WHK ext
listening.started / listening.ended { tenantId, startedAt, endsAt | endedAt, reason? } console-api / SCH policy cache, OUT (intro runs), AUD, WHK ext
tenant.paused / tenant.resumed / tenant.backpressure { reason } / { level } admin-api / SCH SCH, OUT, admin UI
content.rendered / content.failed { job: ContentJob } content-engine EXE (resume WAITING_EXTERNAL), OUT
audit.appended / audit.anchor.written / audit.chain.broken / audit.export.ready { entryId, seq } / { head, anchoredAt } / { atSeq, expected, found } / { export: AuditExport } AUD ESC (chain.broken → critical), admin UI, WHK ext (export.ready)
system.degraded / system.restored { domain: 'model' | 'connector' | 'surface' | 'db' | 'scheduler' | 'executor' | 'event_log' | 'audit_anchor', detail } health probes SCH (backpressure), Owner Console header, admin, WHK ext

4. REST endpoints

Notation: AUTH = required principal; → = response type. All list endpoints paginate. All POST/PUT/PATCH accept Idempotency-Key.

4.1 IC surface — /v1/me

AUTH: the person (session or surface-authenticated).

Method Path Body → Notes
GET /v1/me — { person: Person, manager: { chonkId, displayName }, managerOfRecord: Person, escalationChain: Array<{personId, displayName, orgId}>, tenantPhase } The thread header data
GET /v1/me/brief?date= — { brief: Message, receipt: Receipt, degraded: boolean } Today's by default
GET /v1/me/thread/messages?cursor= — { items: Message[], nextCursor } App surface only; Slack/Teams/SMS use their own history
POST /v1/me/thread/messages { body: string, clientNonce: string } { message: Message, runId: RunId } Creates an IC_MESSAGE run; reply arrives on the thread
POST /v1/me/checkin { occurrenceId?: string, answers?: Record<string,string>, freeText?: string } { accepted: true, runId } Optional structured reply
GET /v1/me/goals / /v1/me/projects — { items: Goal[] | Project[] } Owned or contributing
GET /v1/me/growth-plan — { plan: MemoryFact, history: MemoryFact[] }
PATCH /v1/me/growth-plan { content: string, reason: string } { plan } Subject-editable
GET /v1/me/feedback?cursor= — { items: MemoryFact[] } feedback_given, feedback_received
POST /v1/me/feedback/{factId}/response { text: string } { fact } Appends subjectResponse
GET /v1/me/knows — KnowsPage IC-4
GET /v1/me/knows/access-log?cursor= — { items }
POST /v1/me/knows/{factId}/redact { reason?: string } { status: 'redacted' | 'decision_opened', decisionId? } Non-deletable kinds open a decision to the MoR (EC-MGR-006)
POST /v1/me/escalate { message: string, urgency: 'now' | 'today', to?: 'mor' | 'owner' | 'safety' } { escalation: Escalation } Uncancellable; suppressible:false; delivered on every surface in order
GET /v1/me/escalations — { items: Escalation[] }
POST /v1/me/requests { kind: 'pto' | 'expense' | 'policy_question' | 'document' | 'other', payload: object } { request, runId } Quiet HR (IC-6); HRIS/finance remain systems of record
GET /v1/me/requests/{id} — { request, status, decisionId? }
GET /v1/me/decisions?cursor= — { items: Decision[] } Decisions about me; packets show existence and status only
GET /v1/me/receipts/{receiptId} — Receipt Only receipts whose visibility includes self
GET /v1/me/shifts?from=&to= — { items: Shift[] } Atoms
POST /v1/me/shifts/{shiftId}/swap-request { withPersonId?: PersonId, note?: string } { runId, decisionId? } Atoms; in-policy swaps AUTONOMOUS
POST /v1/me/shifts/{shiftId}/callout { reason?: string } { runId, escalationId? } Atoms; callOutException lifts caps
GET /v1/me/checklists/{occurrenceId} — { items } Atoms
POST /v1/me/checklists/{occurrenceId}/items/{itemId} { done: boolean, note?: string, photoRef?: string } { item } Atoms

4.2 Owner Console — /v1/owner

AUTH: isOwner in the tenant, or a PortfolioGrant with sufficient scope. Human sessions only; API keys are rejected on decide.

Method Path Body → Notes
GET /v1/owner/direction — { goals: Goal[], values: string[], envelopes: Policy[], hardLines: Policy[], priorities: string[], version } OW-1
PUT /v1/owner/direction { …same, reason: string } with If-Match { direction, changeId } Produces policy.changed / org.change.applied
GET /v1/owner/health?level=company|org|goal|person&id= — { node, health, reasons: SourceRef[], narrative: string, children: HealthNode[], receiptId } Drill in three clicks
GET /v1/owner/updates?cursor= — { items: OwnerUpdate[] } { id, weekOf, text, videoUrl?, decisions: DecisionId[], receiptId }
GET /v1/owner/updates/{id} — OwnerUpdate OW-2
GET /v1/owner/approvals?status=proposed|all&cursor= — { items: Array<Decision & { receipt: Receipt, packet?: Packet }> } OW-3; packets included with noRecommendation
GET /v1/owner/approvals/{decisionId} — Decision & { receipt, packet? }
POST /v1/owner/approvals/{decisionId}/decide { status: 'confirmed' | 'rejected', note?: string, edits?: object } or for packets { humanDecision: string } { decision, runId } Creates HUMAN_DECISION trigger; edits count as edited for graduation stats
GET /v1/owner/escalations?status= — { items: Escalation[] }
POST /v1/owner/escalations/{id}/acknowledge / /resolve { note?: string } { escalation }
POST /v1/owner/ask { question: string } { answer: string, receipt: Receipt, runId } OW-4; runs Chonksworth custom with read-only tools
GET /v1/owner/decisions?since=&cursor= — { items: Decision[] } Decision log
GET /v1/owner/portfolio — { tenants: Array<{ tenantId, name, health, openApprovals, escalations, promise: object }> } OW-5; fans out under grants
GET /v1/owner/graduation — { items: Graduation[] } Read; changes via admin

4.3 GraphQL — /v1/graphql

Read-only, for console drill-down and ask-anything citations. Mutations are REST.

type Query {
  orgTree(rootId: ID): OrgNode!
  health(level: HealthLevel!, id: ID!): HealthNode!
  goal(id: ID!): Goal
  project(id: ID!): Project
  person(id: ID!): PersonSummary          # owner-safe projection: name, role, org, goals, health; never memory
  decisions(filter: DecisionFilter, first: Int, after: String): DecisionConnection!
  escalations(status: EscalationStatus, first: Int, after: String): EscalationConnection!
  receipt(id: ID!): Receipt
  narrative(orgId: ID!, weeks: Int = 4): [Narrative!]!
  cadences(orgId: ID): [Cadence!]!
  graduation: [Graduation!]!
}
type OrgNode { org: Org!, managerChonk: ChonkRef!, managerOfRecord: PersonSummary!, members: [PersonSummary!]!, children: [OrgNode!]!, goals: [Goal!]!, health: HealthNode! }
type HealthNode { id: ID!, kind: String!, health: Health!, reason: String!, asOf: DateTime!, sources: [SourceRef!]!, children: [HealthNode!]!, narrative: String }
enum HealthLevel { COMPANY ORG GOAL PROJECT PERSON }
enum Health { GREEN YELLOW RED UNKNOWN }
type Receipt { id: ID!, sources: [SourceRef!]!, ruleEvaluations: [RuleEvaluation!]!, rationale: String!, confidence: Float!, gaps: [String!]! }

PersonSummary is the only person type exposed to the owner through GraphQL; memory facts are not reachable from it (symmetry is served through owner_report to the person, not by exposing memory to the owner).

4.4 Admin — /v1/admin

AUTH: tenant admin role, or API key with matching scope. Every mutation requires reason.

Area Method Path Body / → Notes
Tenant GET / PATCH /v1/admin/tenant Tenant / { patch, reason } Mode, atoms, retention, plan (plan is Sea Suite-ops only)
POST /v1/admin/tenant/pause / /resume { reason } Emits tenant.paused/resumed; escalations still deliver while paused
POST /v1/admin/tenant/listening/end { reason } Not before day 5; owner session only
Onboarding POST /v1/admin/onboarding/draft { providers: ConnectorProvider[] } → { runId } Starts onboarding_draft run
GET /v1/admin/onboarding/draft → { status, nodes, edges, proposals, gaps }
POST /v1/admin/onboarding/draft/approve { corrections: ChangeOp[], reason } → { changeId } Emits listening.started
Org Model GET /v1/admin/org/nodes?kind=&status=&asOf=&cursor= → { items: NodeBase[] } asOf = version or timestamp
GET /v1/admin/org/nodes/{id}?asOf= → node
GET /v1/admin/org/edges?type=&from=&to=&asOf= → { items: Edge[] }
POST /v1/admin/org/changes { ops: ChangeOp[], reason, reasonCode? } with If-Match → Change Atomic; E_ORG_INVARIANT_* on violation
GET /v1/admin/org/changes?since=&cursor= → { items: Change[] }
GET / POST /v1/admin/org/snapshots → { items } / { reason } → { snapshotId }
GET /v1/admin/org/export?asOf= → JSONL stream Portability
GET /v1/admin/org/merge-proposals?status= → { items: MergeProposal[] }
POST /v1/admin/org/merge-proposals/{id}/decide { accept: boolean, canonical?: PersonId, reason }
POST /v1/admin/org/people/{id}/offboard { endDate, reason, hrisRef? } → { checklist } Rejected with E_ORG_INVARIANT_I-ORG-01 if MoR without replacement
POST /v1/admin/org/people/{id}/unmerge { reason }
Roster GET / PUT /v1/admin/roster { chonks: Array<{ chonkId, enabled, orgIds, displayName?, species? }> } managed_by edges are derived from this
PUT /v1/admin/roster/{chonkId}/voice Partial<VoiceProfile> + reason Charter and allowlist are not editable here
Policies GET / POST /v1/admin/policies → { items: Policy[] } / Policy + appliesTo + reason Hard lines require owner session
PATCH / DELETE /v1/admin/policies/{id} { patch, reason } / { reason } Delete = end with validTo
POST /v1/admin/policies/evaluate { action: ProposedAction, asOf? } → Verdict Dry run
Graduation GET /v1/admin/graduation → { items: Graduation[] }
POST /v1/admin/graduation/{fn} { state, ownerCap?, reason } Widening requires owner session; narrowing any admin
GET /v1/admin/shadow-log?fn=&cursor= → { items: ShadowLogEntry[] }
POST /v1/admin/shadow-log/{id}/review { verdict, note? } Feeds graduation criteria
Integrations GET /v1/admin/integrations → { items: Integration[] }
POST /v1/admin/integrations/{provider}/connect { scopes?: string[] } → { authorizeUrl } OAuth start; callback at /v1/admin/integrations/{provider}/callback
DELETE /v1/admin/integrations/{provider} { reason } Tokens destroyed; mirrors retained per retention
POST /v1/admin/integrations/{provider}/resync — → { runId }
GET /v1/admin/integrations/{provider}/health → Integration['health']
Cadences GET / POST /v1/admin/cadences → { items } / Cadence + reason
PATCH /v1/admin/cadences/{id} { patch, reason }
GET /v1/admin/cadences/{id}/occurrences?from=&to= → { items: Array<{ fireAt, participantId?, runId?, status: 'satisfied' | 'missed' | 'degraded' | 'pending' }> }
Runs GET /v1/admin/runs?state=&chonk=&kind=&subject=&since=&cursor= → { items: Run[] }
GET /v1/admin/runs/{id} / /events / /receipts → Run / { items: Event[] } / { items: Receipt[] }
POST /v1/admin/runs/{id}/cancel { reason } Decisions stay open
POST /v1/admin/runs/{id}/replay { mode: 'VERIFY' | 'SHADOW_REEXEC' | 'LIVE_REEXEC', reason } → { runId } LIVE_REEXEC requires owner or Sea Suite ops session
POST /v1/admin/runs { chonkId, objective, subjectRefs, clientNonce } → { runId } MANUAL trigger
Decisions GET /v1/admin/decisions?status=&kind=&cursor= / /{id} → Decision[] / Decision Packets: metadata only unless routed to the caller
Escalations GET /v1/admin/escalations?status= → Escalation[]
PUT /v1/admin/escalations/contacts { safetyContactId: PersonId, opsContact?: string, reason }
Memory GET / PUT /v1/admin/memory/retention Tenant['retention'] + reason
POST /v1/admin/memory/purge { subjectId, kinds?: MemoryFactKind[], reason, legalBasis?: string } → { purged: number, auditId } Owner or admin session; audited with hashes
Audit GET /v1/admin/audit?since=&action=&cursor= → { items: AuditEntry[] }
POST /v1/admin/audit/export { from, to } → AuditExport Async; audit.export.ready
GET /v1/admin/audit/exports/{id} → AuditExport
GET /v1/admin/audit/verify?from=&to= → { ok: boolean, brokenAtSeq?: number, head: string }
Webhooks GET / POST /v1/admin/webhooks → { items } / { url, events: string[], secret?: string } → { webhook, secret } Secret shown once
DELETE / POST /v1/admin/webhooks/{id} / /test { reason } / —
Budgets GET / PUT /v1/admin/budgets { perRunKind: Record<RunKind, Partial<RunBudget>>, outreach: Policy } + reason Within tier caps
Simulation POST /v1/admin/sim/seed { org: 'lanternfish' | 'harbortable', clock?: ISODateTime } Non-production cells only

5. Webhook contracts

5.1 Inbound (integrations → ingress)

Endpoint pattern: POST https://ingress.{cell}.seasuite.app/v1/hooks/{provider}/{integrationId}. Ingress verifies the provider signature, records integration.event.received with payloadHash, returns 2xx within 2 s (work is asynchronous), and returns 2xx for duplicate deliveries (idempotent on deliveryId). Ingress never returns provider errors to the provider except 401 for a bad signature and 503 when the event log is unavailable (providers retry).

Provider family Verification deliveryId Event types normalized to Subject mapping Default Chonk
GitHub X-Hub-Signature-256 HMAC-SHA256 X-GitHub-Delivery work.pr.opened/updated/review_requested/reviewed/merged/closed, work.issue.*, work.deploy.* Repo → Org via externalRefs; author/reviewer → Person via identity github build
Linear / Jira / Asana Linear Linear-Signature; Jira JWT/HMAC; Asana X-Hook-Secret Linear deliveryId; Jira webhookEvent + timestamp + issue id; Asana event id work.issue.created/updated/status_changed/assigned/blocked/unblocked/commented Project/issue → Project via externalRefs; assignee → Person build (eng), porpoise (product), ops (other)
Notion / Google Drive Notion signature; Drive channel token Notion event id; Drive X-Goog-Message-Number + channel doc.updated/shared Doc → Project by link porpoise
Slack / Teams Slack signing secret (v0); Teams JWT Slack event_id; Teams message id comms.message.received (only in manager threads and channels the Chonk is in), comms.reaction, comms.member_joined Slack user → Person; DM to bot → IC_MESSAGE Managing Chonk of the person's org
Twilio (SMS / voice) X-Twilio-Signature MessageSid / CallSid comms.sms.received, comms.call.received, comms.call.transcript Phone → Person (phoneE164) human (thread), ops (call-outs by intent)
Gmail / Calendar / Outlook Google push channel token; Graph validation token history id / message number calendar.event.created/updated/cancelled, mail.received (only for connected manager mailbox) Attendee → Person ops
Gusto / Rippling / Deel / BambooHR / Workday Provider HMAC Provider event id people.hired, people.terminated (recorded, never decided), people.leave.started/ended, people.role_changed, people.pto.requested/approved, people.timesheet.* HRIS employee id → Person (anchor identity) human; org placement proposals to admin
QuickBooks / Xero / Ramp / Brex / Stripe Provider HMAC Provider event id finance.transaction.created, finance.request.created (Ramp/Brex approvals), finance.invoice.*, finance.subscription.* Card holder → Person; category → envelope policy fin
HubSpot / Intercom HMAC Event id crm.deal.stage_changed, crm.deal.updated, support.conversation.* Owner → Person; deal → Project (optional) revenue, ops (support)
Square / Toast / Shopify POS HMAC Event id pos.sales.summary, pos.forecast.updated, pos.86.updated, pos.reservation.* Location → Org(location) ops
Deputy / When I Work / 7shifts / Homebase / time clocks HMAC or polling Event id or hash(objectId, updatedAt) shift.published/changed/uncovered, shift.callout, shift.swap.requested, timeclock.punch.in/out, timeclock.overtime_risk Employee → Person; location → Org ops; security for compliance
IoT / sensors Shared secret per feed Sequence number iot.reading, iot.alert (temperature, door, equipment) Device → Org(location) security
MCP agent workers mTLS Request id agent.task.completed/failed/needs_input Agent → Person(agent worker) Managing Chonk

Normalized shape:

interface InboundWebhookEnvelope {
  provider: ConnectorProvider;
  integrationId: IntegrationId;
  deliveryId: string;
  receivedAt: ISODateTime;
  signatureVerified: true;
  eventType: string;                         // normalized name above
  occurredAt?: ISODateTime;
  subjectRefs: NodeRef[];                    // resolved; unresolved identities produce a MergeProposal or an 'invited' Person
  externalRefs: ExternalRef[];
  normalized: Record<string, unknown>;       // provider-specific fields mapped per doc 11
  payloadHash: string;                       // raw payload stored encrypted for 30 days for replay/debug
}

5.2 Outbound (Sea Suite → customer)

Customers subscribe via /v1/admin/webhooks to any event marked ext in §3.

interface OutboundWebhook {
  id: string;                                // evt_… (the source event id; idempotency key for the receiver)
  type: string;                              // event type
  tenantId: TenantId;
  seq: number;
  ts: ISODateTime;
  schemaVersion: string;
  data: unknown;                             // the event payload, minus memory content and packet contents
}

Headers: X-Sea-Event: <type>, X-Sea-Delivery: <uuid>, X-Sea-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, t + '.' + rawBody)>. Receivers must verify v1 and reject t older than 5 minutes. Delivery: 2xx within 10 s counts as success; otherwise retries with exponential backoff (1 min, 5, 30, 2 h, 6 h, 24 h) then dead; a webhook with 50 consecutive failures is disabled with an admin notification. Ordering across retries is not guaranteed; receivers order by seq. Payloads never include MemoryFact.content, Packet.contents, or receipts with visibility='self'.

6. Error codes

Code HTTP Meaning Retry
E_AUTH_REQUIRED 401 No credentials No
E_AUTH_INVALID 401 Expired or bad token; bad webhook signature (E_WEBHOOK_SIGNATURE alias) No
E_FORBIDDEN 403 Principal lacks role or scope No
E_TENANT_MISMATCH 403 X-Tenant-Id does not match the token and no grant covers it No
E_HUMAN_SESSION_REQUIRED 403 API key attempted to decide a PROPOSE_CONFIRM decision, record a HUMAN_ONLY packet, widen graduation, end listening, or run LIVE_REEXEC No
E_NOT_FOUND 404 Unknown id or not visible to the principal No
E_VALIDATION 400 Schema or prefix error; details.fields[] No (fix input)
E_REASON_REQUIRED 400 Mutation without reason No
E_IDEMPOTENCY_CONFLICT 409 Same Idempotency-Key, different body No
E_ORG_VERSION_CONFLICT 409 If-Match version stale Yes, after re-read
E_ORG_INVARIANT_<ID> 422 Change would violate invariant I-ORG-<ID>; details.invariant, details.nodes[] No
E_ORG_MERGE_KIND_MISMATCH 422 Merge between human and agent No
E_ORG_CYCLE 422 Alias of E_ORG_INVARIANT_I-ORG-03 with the cycle path in details.path No
E_POLICY_BLOCKED 422 Hard line or policy forbids the request; details.policyIds, details.evaluations No
E_AUTONOMY_HUMAN_ONLY 422 Request would execute a human-only action; a packet route is returned in details.packetId No
E_DECISION_TERMINAL 409 Decision already decided/expired No
E_DECISION_STALE 409 Confirmed, but re-classification changed the verdict; details.newVerdict No; a new proposal follows
E_ESCALATION_UNSUPPRESSIBLE 422 Attempt to cancel or delay an IC → human, safety, or cadence-missed escalation No
E_GRADUATION_CAP 422 Requested state above ownerCap or criteria unmet; details.criteria No
E_LISTENING_MIN_DAYS 422 Listening cannot end before day 5 No
E_TENANT_PAUSED 423 Tenant paused; reads allowed, runs and writes refused Yes, after resume
E_RUN_STATE 409 Operation invalid for run state (cancel a SUCCEEDED run) No
E_RUN_BUDGET 422 Requested budget above tier cap No
E_MEMORY_FORBIDDEN_CONTENT 422 Write matched a forbidden pattern (§5.4 of doc 04); details.pattern No
E_MEMORY_NOT_REDACTABLE 409 Kind is annotate-only; response includes decisionId opened for the MoR No
E_INTEGRATION_NEEDS_REAUTH 422 Connector token revoked; details.authorizeUrl After re-auth
E_INTEGRATION_UNAVAILABLE 502 Provider down for a synchronous call (OAuth start, resync) Yes
E_RATE_LIMITED 429 Per-key limit; Retry-After Yes
E_BACKPRESSURE 503 Cell or tenant at L3; Retry-After Yes
E_EXPORT_IN_PROGRESS 409 An audit or Org Model export is already running for the range Yes, later
E_SIM_ONLY 403 Endpoint available only in non-production cells No
E_INTERNAL 500 Unexpected; requestId for support Yes, with backoff

7. Edge cases

ID Scenario Signal / detection Rule Action (autonomy class) Escalation Test
EC-API-001 Automation API key calls POST /v1/owner/approvals/{id}/decide Principal kind api_key on a human-only route Approvals and packets require a human session E_HUMAN_SESSION_REQUIRED; audit entry decision.human attempt (AUTONOMOUS) Admin notified on 3 attempts/day Call with a key in sim; assert 403 and audit entry
EC-API-002 GitHub delivers the same X-GitHub-Delivery twice, 3 days apart Idempotent on deliveryId Duplicate inbound deliveries are acknowledged, not processed 200 with Idempotent-Replayed: true; no run (AUTONOMOUS) None Re-post a recorded delivery
EC-API-003 Owner PUT /v1/owner/direction with stale If-Match after Chonksworth's goal-draft change Version mismatch Optimistic concurrency E_ORG_VERSION_CONFLICT with details.currentVersion and a diff (AUTONOMOUS) None Concurrent writes in sim
EC-API-004 Customer webhook endpoint returns 200 but takes 25 s Delivery timeout 10 s Success requires 2xx within 10 s Retry per schedule; receiver dedups on id (AUTONOMOUS) Admin after 50 consecutive failures Slow endpoint in the webhook test harness
EC-API-005 POST /v1/me/escalate while the tenant is paused tenant.phase = PAUSED Escalation right survives pause Delivered; escalation.opened (AUTONOMOUS, uncapped) MoR; owner if unacknowledged in 2 h Pause tenant in sim; escalate; assert delivery
EC-API-006 GET /v1/me/knows for a person merged last week Person status=merged Merged ids resolve to canonical 307 to canonical person's page; access logged (AUTONOMOUS) None Merge then fetch
EC-API-007 Admin POST /v1/admin/memory/purge for a person with open HUMAN_ONLY packet Packet references the subject Packets are decision records, not memory; purge does not touch them Memory purged; packet untouched; response lists what was not purged and why (AUTONOMOUS) Owner informed Purge in sim; assert packet intact
EC-API-008 Inbound Slack event from a channel the Chonk is not a member of (Slack sends member_joined for a private channel) Channel not in the Chonk's membership list Never reads private channels Event dropped at ingress with integration.event.dropped{reason:'not_in_scope'}; nothing stored beyond the hash (AUTONOMOUS) None Send a synthetic private-channel event
EC-API-009 POST /v1/admin/graduation/planning to AUTONOMOUS by an admin who is not the owner Widening requires owner session Widening is direction E_HUMAN_SESSION_REQUIRED variant details.requires='owner' None Call as admin in sim
EC-API-010 Outbound webhook subscribed to decision.* receives a human_only_packet decision Payload filter Packet contents never leave data.decision with summary replaced by "[human-only packet]" and no receipt (AUTONOMOUS) None Subscribe and create a packet in sim; assert redaction

8. Risk control

RC-API-01 — Data leaving the tenant boundary through APIs or webhooks. Owner: Chief Security Officer. Metric: responses containing memory content or packet contents to a principal without visibility (target 0); webhook payload filter test failures (target 0).

  • Detect: response filters are typed (serializers per principal kind); a canary memory fact per tenant with a unique token is asserted never to appear in owner, admin, or webhook responses by a continuous synthetic probe.
  • Contain: on a probe hit, the affected endpoint is disabled by feature flag within the cell; webhooks for the tenant pause.
  • Correct: serializer fixed; affected deliveries listed from the audit log and the tenant notified per Security, Privacy, Compliance.
  • Learn: the response shape joins the contract test suite that diffs every endpoint's serializer against the visibility matrix on every deploy.

Open questions

  1. GraphQL surface size. Whether ask-anything should expose a person query at all, or only org-level nodes with people as unlinked names; current answer is PersonSummary with no memory reachability.
  2. Webhook payload minimization. Some customers will want decision.* payloads with receipts for their own audit; that means receipts crossing the boundary. Consider a receipts: 'none' | 'owner_visible' option per webhook.
  3. API keys for MoR actions. A manager of record may want to acknowledge escalations from their own tooling; today acknowledgment requires a human session. Decide whether acknowledgment (not decision) can be key-scoped.
  4. Surface-authenticated writes. Slack interactive buttons (Approve/Reject) arrive as Slack-signed payloads, not human sessions; they must count as human decisions. Define the binding (Slack user → verified identity → session-equivalent) precisely in doc 13.
  5. Version negotiation for events. schemaVersion on events is per event type; a consumer registry that declares supported versions would let us evolve payloads without breaking read models.