Full as-built reference: run/auth (dev tokens), conventions (idempotency, tenancy, error codes), every REST endpoint across health/dev/interactions/threads/inbox/ support/adapters/routes/ai/calendar with request/response shapes, the kernel-client + 6 React hook SDKs, a curl QA quickstart + the 8 smoke scripts, env var reference, and enum vocabularies. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
18 KiB
IIOS — API & SDK Guide (for QA and Developers)
Everything you need to run, test, and build against the IIOS service and SDKs. One NestJS API (
iios-service) + a set of browser SDKs. This doc is generated from the current code — endpoints, request/response shapes, SDK methods, auth, and env are all as-built.
1. Quick facts
| Thing | Value |
|---|---|
| Base URL (dev) | http://localhost:3200 (env PORT, default 3200) |
| Auth | Authorization: Bearer <JWT> — HS256, per-app secret |
| Content type | application/json |
| Validation | Global — unknown body fields are rejected (400); types are coerced |
| CORS | Enabled (origin: true) |
| Realtime | Socket.IO namespace /message |
| DB | PostgreSQL (dev: localhost:5434, db iios) |
Everything is scoped to a tenant. A caller can only see/act on data in its own scope (orgId + appId + optional tenantId, derived from the token). Cross-tenant access returns 403.
2. Getting started (run it locally)
# 1. Postgres (docker-compose in the repo root brings up 'iios-db' on :5434)
docker compose up -d
# 2. Install + generate + migrate
pnpm install
pnpm -F @insignia/iios-service prisma:generate
pnpm -F @insignia/iios-service prisma:migrate
# 3. Run the API (dev tokens ON so you can mint test tokens)
cd packages/iios-service && IIOS_DEV_TOKENS=1 pnpm start
# → iios-service listening on :3200
Health check: GET http://localhost:3200/health → { "status": "ok", "db": true, "egressProviders": { ... } }
3. Auth model (how to get a token)
Tokens are per-app JWTs signed with a secret from the APP_SECRETS env map ({"portal-demo":"dev-secret"} by default). The token carries sub (userId), appId, orgId, optional tenantId.
Get a dev token (only when IIOS_DEV_TOKENS=1):
curl -s -X POST http://localhost:3200/v1/dev/token \
-H 'content-type: application/json' \
-d '{"appId":"portal-demo","userId":"alice","orgId":"org_A"}'
# → { "token": "eyJhbGc..." }
Use it on every call: -H "authorization: Bearer <token>".
- Different
orgId= different tenant. Mint two tokens withorg_A/org_Bto test isolation. - Token TTL: 2h.
Error responses (what QA will see)
| Status | When |
|---|---|
400 |
Missing Authorization header, unknown appId, or invalid/unknown body fields |
401 |
Invalid/expired token, or bad webhook signature |
403 |
Cross-tenant access (resource not in your scope) |
404 |
Resource id not found (within your scope) |
201 |
Created (POST) · 200 others |
4. Conventions
- Idempotency — native message send accepts an
idempotency-keyheader; adapter send takesidempotencyKeyin the body; AI/route/meeting derive their own keys. Re-sending the same key returns the same record (no duplicate). - Scopes/tenancy — inferred from the token; you never pass
scopeIdin requests (except the raw/v1/interactions/ingestand adapter ingest which carry a full scope object). - Fail-closed — reads/writes/sends pass through policy (OPA) + consent (CMP) gates; a denial blocks the action rather than silently allowing it.
- Preview / propose first — routing simulates before sending; AI proposes (never auto-decides); meetings gate recording on consent.
5. REST API reference
Grouped by domain. All paths are relative to the base URL. Auth = Bearer token required unless noted.
5.1 Health & Dev
| Method | Path | Auth | Body / Query | Returns |
|---|---|---|---|---|
| GET | /health |
none | — | {status, db, egressProviders} |
| POST | /v1/dev/token |
none* | {appId, userId, name?, orgId?} |
{token} |
| POST | /v1/dev/webhook/:channelType |
none* | {from?, text?} |
raw event (simulated signed inbound) |
*Dev endpoints require IIOS_DEV_TOKENS=1; never enabled in prod.
5.2 Interactions (kernel ingest)
| Method | Path | Body | Returns |
|---|---|---|---|
| POST | /v1/interactions/ingest |
IngestRequestDto |
{interactionId, ...} |
IngestRequestDto: { scope:{orgId,appId,buId?,tenantId?,workspaceId?,projectId?,userScopeId?}, channel:{type,externalChannelId?,capabilityContract?}, source:{handleKind,externalId,displayName?}, kind?, thread?:{externalThreadId?,subject?}, parts:[{kind,bodyText?,contentRef?,mimeType?}], occurredAt (ISO8601), providerEventId?, metadata? }. Send providerEventId twice → deduped.
5.3 Threads & Messaging (native chat)
| Method | Path | Body / Headers | Returns |
|---|---|---|---|
| GET | /v1/threads/:id/messages |
— | {threadId, messages[]} |
| POST | /v1/threads/:id/messages |
{content, contentRef?} + idempotency-key? header |
Message (201) |
Realtime (Socket.IO, namespace /message): connect, then emit client→server events:
open_thread{threadId?|otherUserId},send_message{threadId, content, idempotencyKey?},read{threadId},delivered{threadId}.- Server emits to the thread room:
message(a Message) andreceipt{interactionId, actorId, kind: READ|DELIVERED}.
5.4 Inbox (work queue)
| Method | Path | Query / Body | Returns |
|---|---|---|---|
| GET | /v1/inbox/items |
`?state=OPEN | SNOOZED |
| PATCH | /v1/inbox/items/:id |
{state, reason?} |
InboxItem |
5.5 Support (tickets, escalation, callbacks, queues, agents)
| Method | Path | Body / Query | Returns |
|---|---|---|---|
| POST | /v1/support/tickets |
{subject, priority?, threadId?} |
Ticket |
| POST | /v1/support/escalate |
{threadId, subject?} |
Ticket (opens customer↔agent thread) |
| GET | /v1/support/tickets |
`?scope=mine | assigned` |
| PATCH | /v1/support/tickets/:id |
{state, reason?} |
Ticket |
| POST | /v1/support/callbacks |
{preferChannel, preferredTime?, notes?, ticketId?} |
CallbackRequest |
| POST | /v1/support/queues |
{name} |
{id} |
| POST | /v1/support/queues/:id/members |
— | membership |
| POST | /v1/support/agents/me/join |
— | joins default queue (go online) |
| PATCH | /v1/support/members/me/availability |
{state} |
membership |
priority ∈ {LOW, NORMAL, HIGH, URGENT}; ticket state ∈ {NEW, OPEN, PENDING, RESOLVED, CLOSED} (transitions validated).
5.6 Adapters (channels — inbound/outbound)
| Method | Path | Auth | Body / Headers | Returns |
|---|---|---|---|---|
| POST | /v1/adapters/:channelType/webhook |
signature | raw provider body + x-iios-signature (HMAC) |
202 (async normalize) |
| POST | /v1/adapters/:channelType/send |
Bearer | {target, payload?, idempotencyKey?} |
OutboundCommand |
| GET | /v1/adapters/inbound |
Bearer | — | raw inbound events (+ interactionId once normalized) |
| GET | /v1/adapters/outbound |
Bearer | — | outbound commands (+ delivery attempts) |
channelType ∈ {WEBHOOK, EMAIL, WHATSAPP, PORTAL}. Outbound goes through the Capability Broker (policy + consent + provider); a blocked send → command status BLOCKED, a tenant over quota → RATE_LIMITED.
5.7 Routing / rules (community)
| Method | Path | Body / Query | Returns |
|---|---|---|---|
| POST | /v1/routes/bindings |
CreateBindingDto |
RouteBinding |
| GET | /v1/routes/bindings |
— | RouteBinding[] (your scope) |
| POST | /v1/routes/simulate |
{interactionId, originChannelType, originRef?} |
{simulationId, decisions[]} (no send) |
| GET | /v1/routes/decisions |
`?state=ALLOW | DENY |
| POST | /v1/routes/decisions/:id/approve |
— | RouteDecision (executes to sandbox) |
| POST | /v1/routes/decisions/:id/deny |
— | RouteDecision |
| POST | /v1/routes/bindings/:id/flush-digest |
— | {forwarded} |
CreateBindingDto: {originChannelType, originRef?, destinationChannelType, destinationRef?, restrictionProfile?, mode?, outputFormat?, frequency?, includeAttachments?, requiresReview?, enabled?}. mode ∈ {MANUAL, AUTOMATIC, HYBRID, SIMULATION_ONLY}; outputFormat ∈ {FORWARD, THREADED, DIGEST, SUMMARY, TRANSCRIPT}. Restricted destinations are deny-by-default for flagged content.
5.8 AI (proposal layer)
| Method | Path | Body / Query | Returns |
|---|---|---|---|
| POST | /v1/ai/jobs |
{interactionId, jobType} |
{job, artifact} |
| GET | /v1/ai/artifacts |
?interactionId=&status= |
AiArtifact[] (your scope) |
| GET | /v1/ai/artifacts/:id |
— | AiArtifact (claims + evidence + modelRun) |
| POST | /v1/ai/artifacts/:id/accept |
— | AiArtifact (ACCEPTED) |
| POST | /v1/ai/artifacts/:id/reject |
— | AiArtifact (REJECTED) |
jobType ∈ {CLASSIFY, SUMMARIZE, EXTRACT}. Artifacts carry confidence, evidence (citations), and abstentionReason when the model declines. AI proposes; a human accepts.
5.9 Calendar & Meetings
| Method | Path | Body / Query | Returns |
|---|---|---|---|
| POST | /v1/calendar/meetings |
ScheduleMeetingDto |
Meeting (SCHEDULED) |
| POST | /v1/calendar/requests |
RequestMeetingDto + ?fromCallback=&fromClaim= |
meeting request |
| GET | /v1/calendar/meetings |
— | Meeting[] (your scope) |
| GET | /v1/calendar/meetings/:id |
— | Meeting (participants, transcripts, summaries, action items) |
| POST | /v1/calendar/meetings/:id/consent |
{actorRefId, status} |
participant |
| POST | /v1/calendar/meetings/:id/transcript |
— | transcript (READY or BLOCKED) |
| POST | /v1/calendar/meetings/:id/summarize |
— | Meeting (summary + action items) |
| GET | /v1/calendar/meetings/:id/action-items |
— | MeetingActionItem[] |
| POST | /v1/calendar/providers |
— | provider account (simulated) |
| POST | /v1/calendar/providers/:id/sync |
— | {pulled, cursor} |
ScheduleMeetingDto: {meetingType, title, startAt, endAt?, timezone?, attendees?:[{userId, displayName?, role?, visibility?}], requestId?}. meetingType ∈ {ZOOM, PHONE, IN_PERSON, CALLBACK, INTERNAL}; consent status ∈ {UNKNOWN, GRANTED, DENIED, REVOKED}. Transcript/summary is BLOCKED until every attendee has GRANTED consent.
6. SDK reference
Two layers: a low-level RestClient (+ socket) and per-domain React hook packages.
6.1 Low-level client — @insignia/iios-kernel-client
import { RestClient } from '@insignia/iios-kernel-client';
const client = new RestClient({ serviceUrl: 'http://localhost:3200', token });
Methods (all return typed promises):
- Messaging:
getThreadMessages(threadId),sendMessage(threadId, content, {contentRef?, idempotencyKey?}) - Inbox:
listInboxItems(state?),patchInboxItem(id, {state, reason?}) - Support:
createTicket({subject, priority?, threadId?}),escalate(threadId, subject?),listTickets('mine'|'assigned'),patchTicket(id, state),requestCallback({...}),createQueue(name),joinQueue(id),joinDefaultQueue(),setAvailability(state) - Routing:
createBinding(input),listBindings(),simulateRoute({interactionId, originChannelType, originRef?}),listRouteDecisions(state?),approveDecision(id),denyDecision(id) - AI:
runAiJob({interactionId, jobType}),listArtifacts({interactionId?, status?}),getArtifact(id),acceptArtifact(id),rejectArtifact(id) - Calendar:
scheduleMeeting(input),requestMeeting(input, {fromCallback?, fromClaim?}),listMeetings(),getMeeting(id),setConsent(meetingId, actorRefId, status),generateTranscript(meetingId),summarizeMeeting(meetingId),listActionItems(meetingId),connectCalendarProvider(),syncCalendarProvider(id) - Ingest:
ingest(body, idempotencyKey) - Realtime:
MessageSocket(Socket.IO facade for/message).
6.2 React hook packages (front-end teams)
Each package exports a Provider (wrap your app with serviceUrl + token) plus hooks:
| Package | Provider | Hooks |
|---|---|---|
@insignia/iios-message-web |
MessageProvider |
useThread, useMessages |
@insignia/iios-inbox-web |
InboxProvider |
useInbox |
@insignia/iios-support-web |
SupportProvider |
useEscalate, useTickets, useAssignedTickets, useCallbackRequest, useAvailability, useGoOnline (+ re-exports useThread/useMessages) |
@insignia/iios-community-web |
CommunityProvider |
useBindings, useRoutePreview, useRouteDecisions |
@insignia/iios-ai-web |
AiProvider |
useRunJob, useAiProposals, useArtifact |
@insignia/iios-meeting-web |
MeetingProvider |
useMeetings, useMeeting, useSchedule, useConsent, useSummarize, useActionItems |
// Example: an AI-assist panel
import { AiProvider, useRunJob, useAiProposals } from '@insignia/iios-ai-web';
<AiProvider serviceUrl={SERVICE} token={token}>
<Panel interactionId={id} />
</AiProvider>;
function Panel({ interactionId }: { interactionId: string }) {
const run = useRunJob();
const { artifacts, accept, reject } = useAiProposals({ interactionId, pollMs: 2500 });
return <>
<button onClick={() => run(interactionId, 'SUMMARIZE')}>Summarize</button>
{artifacts.map(a => <ProposalCard key={a.id} a={a} onAccept={() => accept(a.id)} onReject={() => reject(a.id)} />)}
</>;
}
7. QA quickstart (curl walkthrough)
BASE=http://localhost:3200
TOKEN=$(curl -s -X POST $BASE/v1/dev/token -H 'content-type: application/json' \
-d '{"appId":"portal-demo","userId":"alice","orgId":"org_A"}' | jq -r .token)
AUTH="authorization: Bearer $TOKEN"
# schedule a meeting
MID=$(curl -s -X POST $BASE/v1/calendar/meetings -H "$AUTH" -H 'content-type: application/json' \
-d '{"meetingType":"INTERNAL","title":"QA test","startAt":"2026-07-02T10:00:00.000Z"}' | jq -r .id)
# transcript is BLOCKED (no consent yet)
curl -s -X POST $BASE/v1/calendar/meetings/$MID/transcript -H "$AUTH" | jq .status # "BLOCKED"
# tenant isolation: a second tenant cannot read it → 403
TOKEN_B=$(curl -s -X POST $BASE/v1/dev/token -H 'content-type: application/json' \
-d '{"appId":"portal-demo","userId":"bob","orgId":"org_B"}' | jq -r .token)
curl -s -o /dev/null -w "%{http_code}\n" $BASE/v1/calendar/meetings/$MID \
-H "authorization: Bearer $TOKEN_B" # 403
Ready-made end-to-end tests (smoke scripts)
Run against a live service (from packages/iios-service, node scripts/<name>):
| Script | Proves |
|---|---|
smoke-realtime.mjs |
native chat + realtime + unread (P2) |
smoke-inbox.mjs |
needs-reply queue + auto-resolve (P3) |
smoke-support.mjs (+ seed-support.mjs) |
ticket assign + escalate + live reply (P4) |
smoke-adapter.mjs |
signed webhook ingest + sandbox send (P5) |
smoke-route.mjs |
preview-first routing, deny-by-default, approve→send (P6) |
smoke-ai.mjs |
AI classify/summarize/extract, abstain, KG-05 (P7) |
smoke-calendar.mjs |
consent-gated transcript, summary, action items (P8) |
smoke-capability.mjs |
governed egress + real HTTP provider (needs IIOS_PROVIDER_URL_EMAIL) |
smoke-tenant.mjs |
cross-tenant 403 + list isolation |
Automated unit/integration suite: pnpm test (114 tests). Import-boundary check: pnpm boundary.
8. Tenancy & security notes for QA
- Cross-tenant — any by-id read/mutate of another tenant's resource → 403; lists only ever return your own scope's data. Test with two
orgIds. - Consent — a meeting transcript/summary is
BLOCKEDunless every attendee consented; an outbound send to an opted-out recipient isBLOCKED(consent gate). - Per-tenant quota — one tenant flooding outbound gets
RATE_LIMITEDwithout affecting others. - AI safety — AI outputs are proposals (
PROPOSED) with confidence/evidence; they never auto-send or auto-approve; low-confidence → abstains. - Idempotency — replaying a webhook/message/job with the same key yields one record.
9. Environment variables
| Var | Default | Purpose |
|---|---|---|
PORT |
3200 |
HTTP port |
DATABASE_URL |
postgresql://iios:iios@localhost:5434/iios?schema=public |
Postgres |
APP_SECRETS |
{"portal-demo":"dev-secret"} |
per-app JWT secrets ({appId: secret}) |
IIOS_DEV_TOKENS |
0 |
set 1 to enable /v1/dev/* |
ADAPTER_SECRETS |
{} |
per-channel HMAC secrets (default dev-adapter-secret) |
IIOS_OUTBOUND_LIMIT / _WINDOW_MS |
5 / 60000 |
per-(channel,target) rate limit |
IIOS_TENANT_OUTBOUND_LIMIT / _WINDOW_MS |
10000 / 60000 |
per-tenant egress quota |
IIOS_AI_BUDGET_UNITS |
1000000 |
per-tenant AI cost budget |
IIOS_CELL_ID |
cell-default |
cell assignment for new scopes |
IIOS_PROVIDER_URL_<CHANNEL> |
— | set to route that channel's egress to a real HTTP provider |
IIOS_RELAY_INTERVAL_MS |
500 |
outbox → event relay poll interval |
10. Vocabularies (enums)
- Interaction kind: MESSAGE, EMAIL, SYSTEM_NOTICE, INBOX_WORK, SUPPORT_CASE, MEETING_REQUEST, DIGEST, SUMMARY, NOTIFICATION
- Channel types: WEBHOOK, EMAIL, WHATSAPP, PORTAL
- Inbox state: OPEN, SNOOZED, DONE, ARCHIVED, CANCELLED, STALE · Inbox kind: NEEDS_REPLY, NEEDS_REVIEW, NEEDS_APPROVAL, SUPPORT_UPDATE, MEETING_FOLLOWUP, DIGEST, SYSTEM_ALERT, CRM_OWNER_INTEREST
- Ticket state: NEW, OPEN, PENDING, RESOLVED, CLOSED · priority: LOW, NORMAL, HIGH, URGENT
- Route mode: MANUAL, AUTOMATIC, HYBRID, SIMULATION_ONLY · output format: FORWARD, THREADED, DIGEST, SUMMARY, TRANSCRIPT · decision: ALLOW, DENY, REVIEW, SUPPRESS, SIMULATED
- AI job: CLASSIFY, SUMMARIZE, EXTRACT · artifact: CLASSIFICATION, SUMMARY, TRANSCRIPT, DIGEST, EXTRACTION · artifact status: PROPOSED, ACCEPTED, REJECTED, SUPERSEDED
- Meeting type: ZOOM, PHONE, IN_PERSON, CALLBACK, INTERNAL · status: REQUESTED, SCHEDULED, CONFIRMED, CANCELLED, COMPLETED, NO_SHOW · consent: UNKNOWN, GRANTED, DENIED, REVOKED · participant visibility: FULL, LIMITED, NONE
Base URL, ports, and demos: the service is http://localhost:3200; runnable demo UIs live under apps/ (message-demo 5173, agent-demo 5174, adapter-inspector 5175, route-admin 5176, ai-studio 5177, meeting-studio 5178). For architecture/what-each-SDK-is-for, see docs/IIOS_OVERVIEW_FOR_CEO.md.