diff --git a/docs/openapi.yaml b/docs/openapi.yaml new file mode 100644 index 0000000..fad3c4b --- /dev/null +++ b/docs/openapi.yaml @@ -0,0 +1,864 @@ +openapi: 3.0.3 +info: + title: IIOS — Insignia Interaction OS API + version: "0.9.0" + description: > + REST API for the IIOS service. Everything is an Interaction inside a tenant scope, + behind auth/consent/policy gates. Auth is a per-app Bearer JWT (mint dev tokens via + POST /v1/dev/token when IIOS_DEV_TOKENS=1). Cross-tenant access returns 403. + Companion docs: docs/IIOS_API_AND_SDK_GUIDE.md. +servers: + - url: http://localhost:3200 + description: Local dev + +security: + - bearerAuth: [] + +tags: + - name: Health + - name: Dev + - name: Interactions + - name: Threads + - name: Inbox + - name: Support + - name: Adapters + - name: Routing + - name: AI + - name: Calendar + +paths: + /health: + get: + tags: [Health] + summary: Liveness + egress provider bindings + security: [] + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + status: { type: string, example: ok } + db: { type: boolean } + egressProviders: + type: object + additionalProperties: { type: string } + example: { WEBHOOK: sandbox, EMAIL: http } + + /v1/dev/token: + post: + tags: [Dev] + summary: Mint a dev JWT (requires IIOS_DEV_TOKENS=1) + security: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [appId, userId] + properties: + appId: { type: string, example: portal-demo } + userId: { type: string, example: alice } + name: { type: string } + orgId: { type: string, description: "Tenant org; default org_", example: org_A } + responses: + "201": + description: Token + content: + application/json: + schema: + type: object + properties: { token: { type: string } } + "400": { $ref: "#/components/responses/BadRequest" } + + /v1/dev/webhook/{channelType}: + post: + tags: [Dev] + summary: Inject a server-signed simulated inbound webhook (dev only) + security: [] + parameters: + - $ref: "#/components/parameters/channelType" + requestBody: + content: + application/json: + schema: + type: object + properties: + from: { type: string } + text: { type: string } + responses: + "201": { description: Raw inbound event accepted } + + /v1/interactions/ingest: + post: + tags: [Interactions] + summary: Ingest a normalized interaction into the kernel (idempotent by providerEventId) + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/IngestRequest" } + responses: + "201": + description: Created + content: + application/json: + schema: + type: object + properties: { interactionId: { type: string } } + "400": { $ref: "#/components/responses/BadRequest" } + "401": { $ref: "#/components/responses/Unauthorized" } + + /v1/threads/{id}/messages: + get: + tags: [Threads] + summary: List a thread's messages + parameters: [{ $ref: "#/components/parameters/idPath" }] + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + threadId: { type: string } + messages: { type: array, items: { type: object } } + "401": { $ref: "#/components/responses/Unauthorized" } + post: + tags: [Threads] + summary: Send a message (REST fallback for the socket path) + parameters: + - $ref: "#/components/parameters/idPath" + - name: idempotency-key + in: header + required: false + schema: { type: string } + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [content] + properties: + content: { type: string } + contentRef: { type: string } + responses: + "201": + description: Message + content: + application/json: + schema: { $ref: "#/components/schemas/Message" } + "400": { $ref: "#/components/responses/BadRequest" } + + /v1/inbox/items: + get: + tags: [Inbox] + summary: List inbox items for the caller + parameters: + - name: state + in: query + required: false + schema: { $ref: "#/components/schemas/InboxState" } + responses: + "200": + description: OK + content: + application/json: + schema: { type: array, items: { $ref: "#/components/schemas/InboxItem" } } + + /v1/inbox/items/{id}: + patch: + tags: [Inbox] + summary: Transition an inbox item + parameters: [{ $ref: "#/components/parameters/idPath" }] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [state] + properties: + state: { $ref: "#/components/schemas/InboxState" } + reason: { type: string } + responses: + "200": + description: OK + content: + application/json: + schema: { $ref: "#/components/schemas/InboxItem" } + "404": { $ref: "#/components/responses/NotFound" } + + /v1/support/tickets: + post: + tags: [Support] + summary: Create a ticket + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [subject] + properties: + subject: { type: string } + priority: { $ref: "#/components/schemas/TicketPriority" } + threadId: { type: string } + responses: + "201": { description: Ticket, content: { application/json: { schema: { $ref: "#/components/schemas/Ticket" } } } } + get: + tags: [Support] + summary: List tickets + parameters: + - name: scope + in: query + required: false + schema: { type: string, enum: [mine, assigned], default: mine } + responses: + "200": { description: OK, content: { application/json: { schema: { type: array, items: { $ref: "#/components/schemas/Ticket" } } } } } + + /v1/support/escalate: + post: + tags: [Support] + summary: Escalate a thread to support (opens a customer↔agent thread) + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [threadId] + properties: + threadId: { type: string } + subject: { type: string } + responses: + "201": { description: Ticket, content: { application/json: { schema: { $ref: "#/components/schemas/Ticket" } } } } + + /v1/support/tickets/{id}: + patch: + tags: [Support] + summary: Transition a ticket + parameters: [{ $ref: "#/components/parameters/idPath" }] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [state] + properties: + state: { $ref: "#/components/schemas/TicketState" } + reason: { type: string } + responses: + "200": { description: Ticket, content: { application/json: { schema: { $ref: "#/components/schemas/Ticket" } } } } + "404": { $ref: "#/components/responses/NotFound" } + + /v1/support/callbacks: + post: + tags: [Support] + summary: Request a callback (bridges to a meeting) + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [preferChannel] + properties: + preferChannel: { type: string, example: PHONE } + preferredTime: { type: string } + notes: { type: string } + ticketId: { type: string } + responses: + "201": { description: CallbackRequest, content: { application/json: { schema: { type: object } } } } + + /v1/support/queues: + post: + tags: [Support] + summary: Create a support queue + requestBody: + required: true + content: + application/json: + schema: { type: object, required: [name], properties: { name: { type: string } } } + responses: + "201": { description: Queue, content: { application/json: { schema: { type: object, properties: { id: { type: string } } } } } } + + /v1/support/queues/{id}/members: + post: + tags: [Support] + summary: Add the caller to a queue + parameters: [{ $ref: "#/components/parameters/idPath" }] + responses: + "201": { description: Membership } + + /v1/support/agents/me/join: + post: + tags: [Support] + summary: Join the default queue (go online) + responses: + "201": { description: Membership } + + /v1/support/members/me/availability: + patch: + tags: [Support] + summary: Set the caller's availability + requestBody: + required: true + content: + application/json: + schema: { type: object, required: [state], properties: { state: { type: string, example: AVAILABLE } } } + responses: + "200": { description: Membership } + + /v1/adapters/{channelType}/webhook: + post: + tags: [Adapters] + summary: Provider inbound webhook (HMAC-signed; async normalize → 202) + security: [] + parameters: + - $ref: "#/components/parameters/channelType" + - name: x-iios-signature + in: header + required: true + schema: { type: string, example: "sha256=..." } + requestBody: + required: true + content: + application/json: + schema: { type: object, description: raw provider payload } + responses: + "202": { description: Accepted (normalized asynchronously) } + "401": { description: Bad signature } + + /v1/adapters/{channelType}/send: + post: + tags: [Adapters] + summary: Outbound send (through the Capability Broker; sandbox by default) + parameters: [{ $ref: "#/components/parameters/channelType" }] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [target] + properties: + target: { type: string } + payload: { type: object } + idempotencyKey: { type: string } + responses: + "201": { description: OutboundCommand, content: { application/json: { schema: { $ref: "#/components/schemas/OutboundCommand" } } } } + + /v1/adapters/inbound: + get: + tags: [Adapters] + summary: List raw inbound events (+ interactionId once normalized) + responses: + "200": { description: OK, content: { application/json: { schema: { type: array, items: { type: object } } } } } + + /v1/adapters/outbound: + get: + tags: [Adapters] + summary: List outbound commands (+ delivery attempts) + responses: + "200": { description: OK, content: { application/json: { schema: { type: array, items: { $ref: "#/components/schemas/OutboundCommand" } } } } } + + /v1/routes/bindings: + post: + tags: [Routing] + summary: Create a route binding + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/CreateBinding" } + responses: + "201": { description: RouteBinding, content: { application/json: { schema: { $ref: "#/components/schemas/RouteBinding" } } } } + get: + tags: [Routing] + summary: List bindings (your scope) + responses: + "200": { description: OK, content: { application/json: { schema: { type: array, items: { $ref: "#/components/schemas/RouteBinding" } } } } } + + /v1/routes/simulate: + post: + tags: [Routing] + summary: Preview-first simulation (produces decisions, sends nothing) + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [interactionId, originChannelType] + properties: + interactionId: { type: string } + originChannelType: { type: string } + originRef: { type: string } + responses: + "201": + description: Simulation + content: + application/json: + schema: + type: object + properties: + simulationId: { type: string } + decisions: { type: array, items: { $ref: "#/components/schemas/RouteDecision" } } + + /v1/routes/decisions: + get: + tags: [Routing] + summary: List route decisions (your scope) + parameters: + - name: state + in: query + required: false + schema: { $ref: "#/components/schemas/RouteDecisionState" } + responses: + "200": { description: OK, content: { application/json: { schema: { type: array, items: { $ref: "#/components/schemas/RouteDecision" } } } } } + + /v1/routes/decisions/{id}/approve: + post: + tags: [Routing] + summary: Approve a REVIEW decision (executes to sandbox) + parameters: [{ $ref: "#/components/parameters/idPath" }] + responses: + "201": { description: RouteDecision, content: { application/json: { schema: { $ref: "#/components/schemas/RouteDecision" } } } } + "403": { $ref: "#/components/responses/Forbidden" } + "404": { $ref: "#/components/responses/NotFound" } + + /v1/routes/decisions/{id}/deny: + post: + tags: [Routing] + summary: Deny a decision + parameters: [{ $ref: "#/components/parameters/idPath" }] + responses: + "201": { description: RouteDecision, content: { application/json: { schema: { $ref: "#/components/schemas/RouteDecision" } } } } + "403": { $ref: "#/components/responses/Forbidden" } + + /v1/routes/bindings/{id}/flush-digest: + post: + tags: [Routing] + summary: Flush a binding's pending DIGEST decisions into one forward + parameters: [{ $ref: "#/components/parameters/idPath" }] + responses: + "201": { description: OK, content: { application/json: { schema: { type: object, properties: { forwarded: { type: integer } } } } } } + + /v1/ai/jobs: + post: + tags: [AI] + summary: Run an AI job (proposes; never decides) + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [interactionId, jobType] + properties: + interactionId: { type: string } + jobType: { $ref: "#/components/schemas/AiJobType" } + responses: + "201": + description: Job + proposed artifact + content: + application/json: + schema: + type: object + properties: + job: { type: object } + artifact: { $ref: "#/components/schemas/AiArtifact" } + + /v1/ai/artifacts: + get: + tags: [AI] + summary: List AI artifacts (your scope) + parameters: + - name: interactionId + in: query + schema: { type: string } + - name: status + in: query + schema: { $ref: "#/components/schemas/AiArtifactStatus" } + responses: + "200": { description: OK, content: { application/json: { schema: { type: array, items: { $ref: "#/components/schemas/AiArtifact" } } } } } + + /v1/ai/artifacts/{id}: + get: + tags: [AI] + summary: Get an artifact (claims + evidence + modelRun) + parameters: [{ $ref: "#/components/parameters/idPath" }] + responses: + "200": { description: AiArtifact, content: { application/json: { schema: { $ref: "#/components/schemas/AiArtifact" } } } } + "403": { $ref: "#/components/responses/Forbidden" } + "404": { $ref: "#/components/responses/NotFound" } + + /v1/ai/artifacts/{id}/accept: + post: + tags: [AI] + summary: Accept an artifact (human feedback) + parameters: [{ $ref: "#/components/parameters/idPath" }] + responses: + "201": { description: AiArtifact, content: { application/json: { schema: { $ref: "#/components/schemas/AiArtifact" } } } } + "403": { $ref: "#/components/responses/Forbidden" } + + /v1/ai/artifacts/{id}/reject: + post: + tags: [AI] + summary: Reject an artifact + parameters: [{ $ref: "#/components/parameters/idPath" }] + responses: + "201": { description: AiArtifact, content: { application/json: { schema: { $ref: "#/components/schemas/AiArtifact" } } } } + "403": { $ref: "#/components/responses/Forbidden" } + + /v1/calendar/meetings: + post: + tags: [Calendar] + summary: Schedule a meeting directly + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/ScheduleMeeting" } + responses: + "201": { description: Meeting, content: { application/json: { schema: { $ref: "#/components/schemas/Meeting" } } } } + get: + tags: [Calendar] + summary: List meetings (your scope) + responses: + "200": { description: OK, content: { application/json: { schema: { type: array, items: { $ref: "#/components/schemas/Meeting" } } } } } + + /v1/calendar/requests: + post: + tags: [Calendar] + summary: Create a meeting request (from a callback / AI claim / direct) + parameters: + - name: fromCallback + in: query + schema: { type: string } + - name: fromClaim + in: query + schema: { type: string } + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [requestedWindow] + properties: + requestedWindow: { type: object } + meetingType: { $ref: "#/components/schemas/MeetingType" } + interactionId: { type: string } + targetActorId: { type: string } + responses: + "201": { description: MeetingRequest, content: { application/json: { schema: { type: object } } } } + + /v1/calendar/meetings/{id}: + get: + tags: [Calendar] + summary: Get a meeting (participants, transcripts, summaries, action items) + parameters: [{ $ref: "#/components/parameters/idPath" }] + responses: + "200": { description: Meeting, content: { application/json: { schema: { $ref: "#/components/schemas/Meeting" } } } } + "403": { $ref: "#/components/responses/Forbidden" } + "404": { $ref: "#/components/responses/NotFound" } + + /v1/calendar/meetings/{id}/consent: + post: + tags: [Calendar] + summary: Set a participant's recording consent + parameters: [{ $ref: "#/components/parameters/idPath" }] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [actorRefId, status] + properties: + actorRefId: { type: string } + status: { $ref: "#/components/schemas/ConsentStatus" } + responses: + "201": { description: Participant } + "403": { $ref: "#/components/responses/Forbidden" } + + /v1/calendar/meetings/{id}/transcript: + post: + tags: [Calendar] + summary: Generate a transcript (BLOCKED unless every attendee consented) + parameters: [{ $ref: "#/components/parameters/idPath" }] + responses: + "201": + description: Transcript (status READY or BLOCKED) + content: + application/json: + schema: + type: object + properties: + status: { type: string, enum: [READY, BLOCKED, PENDING] } + segments: { type: array, items: { type: object } } + "403": { $ref: "#/components/responses/Forbidden" } + + /v1/calendar/meetings/{id}/summarize: + post: + tags: [Calendar] + summary: Summarize a READY transcript (AI) + derive action items + parameters: [{ $ref: "#/components/parameters/idPath" }] + responses: + "201": { description: Meeting, content: { application/json: { schema: { $ref: "#/components/schemas/Meeting" } } } } + "403": { $ref: "#/components/responses/Forbidden" } + + /v1/calendar/meetings/{id}/action-items: + get: + tags: [Calendar] + summary: List meeting action items + parameters: [{ $ref: "#/components/parameters/idPath" }] + responses: + "200": { description: OK, content: { application/json: { schema: { type: array, items: { type: object } } } } } + "403": { $ref: "#/components/responses/Forbidden" } + + /v1/calendar/providers: + post: + tags: [Calendar] + summary: Connect a simulated calendar provider account + responses: + "201": { description: ProviderAccount, content: { application/json: { schema: { type: object, properties: { id: { type: string } } } } } } + + /v1/calendar/providers/{id}/sync: + post: + tags: [Calendar] + summary: Sync the provider once (deterministic, no network) + parameters: [{ $ref: "#/components/parameters/idPath" }] + responses: + "201": { description: OK, content: { application/json: { schema: { type: object, properties: { pulled: { type: integer }, cursor: { type: string } } } } } } + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + + parameters: + idPath: + name: id + in: path + required: true + schema: { type: string } + channelType: + name: channelType + in: path + required: true + schema: { type: string, enum: [WEBHOOK, EMAIL, WHATSAPP, PORTAL] } + + responses: + BadRequest: + description: Validation error / missing auth / unknown field + content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } + Unauthorized: + description: Invalid or missing token / bad signature + content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } + Forbidden: + description: Cross-tenant access — resource not in your scope + content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } + NotFound: + description: Not found (within your scope) + content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } + + schemas: + Error: + type: object + properties: + statusCode: { type: integer } + message: { type: string } + error: { type: string } + + InboxState: { type: string, enum: [OPEN, SNOOZED, DONE, ARCHIVED, CANCELLED, STALE] } + TicketState: { type: string, enum: [NEW, OPEN, PENDING, RESOLVED, CLOSED] } + TicketPriority: { type: string, enum: [LOW, NORMAL, HIGH, URGENT] } + RouteMode: { type: string, enum: [MANUAL, AUTOMATIC, HYBRID, SIMULATION_ONLY] } + OutputFormat: { type: string, enum: [FORWARD, THREADED, DIGEST, SUMMARY, TRANSCRIPT] } + RouteDecisionState: { type: string, enum: [ALLOW, DENY, REVIEW, SUPPRESS, SIMULATED] } + AiJobType: { type: string, enum: [CLASSIFY, SUMMARIZE, EXTRACT] } + AiArtifactStatus: { type: string, enum: [PROPOSED, ACCEPTED, REJECTED, SUPERSEDED] } + MeetingType: { type: string, enum: [ZOOM, PHONE, IN_PERSON, CALLBACK, INTERNAL] } + ConsentStatus: { type: string, enum: [UNKNOWN, GRANTED, DENIED, REVOKED] } + + IngestRequest: + type: object + required: [scope, channel, source, parts, occurredAt] + properties: + scope: + type: object + required: [orgId, appId] + properties: + orgId: { type: string } + appId: { type: string } + buId: { type: string } + tenantId: { type: string } + workspaceId: { type: string } + projectId: { type: string } + userScopeId: { type: string } + channel: + type: object + required: [type] + properties: + type: { type: string } + externalChannelId: { type: string } + capabilityContract: { type: object } + source: + type: object + required: [handleKind, externalId] + properties: + handleKind: { type: string } + externalId: { type: string } + displayName: { type: string } + kind: { type: string } + thread: + type: object + properties: + externalThreadId: { type: string } + subject: { type: string } + parts: + type: array + items: + type: object + required: [kind] + properties: + kind: { type: string, example: TEXT } + bodyText: { type: string } + contentRef: { type: string } + mimeType: { type: string } + occurredAt: { type: string, format: date-time } + providerEventId: { type: string } + metadata: { type: object } + + Message: + type: object + properties: + id: { type: string } + threadId: { type: string } + senderActorId: { type: string } + content: { type: string } + createdAt: { type: string, format: date-time } + + InboxItem: + type: object + properties: + id: { type: string } + ownerActorId: { type: string } + kind: { type: string } + state: { $ref: "#/components/schemas/InboxState" } + title: { type: string } + threadId: { type: string } + + Ticket: + type: object + properties: + id: { type: string } + subject: { type: string } + state: { $ref: "#/components/schemas/TicketState" } + priority: { $ref: "#/components/schemas/TicketPriority" } + threadId: { type: string } + assignedActorId: { type: string } + + OutboundCommand: + type: object + properties: + id: { type: string } + channelType: { type: string } + target: { type: string } + status: { type: string, enum: [PENDING, SENT, FAILED, RATE_LIMITED, BLOCKED] } + providerRef: { type: string } + consentReceiptRef: { type: string } + idempotencyKey: { type: string } + + CreateBinding: + type: object + required: [originChannelType, destinationChannelType] + properties: + originChannelType: { type: string } + originRef: { type: string } + destinationChannelType: { type: string } + destinationRef: { type: string } + restrictionProfile: { type: string, example: CHILD } + mode: { $ref: "#/components/schemas/RouteMode" } + outputFormat: { $ref: "#/components/schemas/OutputFormat" } + frequency: { type: string } + includeAttachments: { type: boolean } + requiresReview: { type: boolean } + enabled: { type: boolean } + + RouteBinding: + allOf: + - $ref: "#/components/schemas/CreateBinding" + - type: object + properties: + id: { type: string } + + RouteDecision: + type: object + properties: + id: { type: string } + interactionId: { type: string } + routeBindingId: { type: string } + decisionState: { $ref: "#/components/schemas/RouteDecisionState" } + reasonCodes: { type: array, items: { type: string } } + previewPayload: { type: object } + outputFormat: { $ref: "#/components/schemas/OutputFormat" } + executed: { type: boolean } + + AiArtifact: + type: object + properties: + id: { type: string } + jobId: { type: string } + artifactType: { type: string, enum: [CLASSIFICATION, SUMMARY, TRANSCRIPT, DIGEST, EXTRACTION] } + status: { $ref: "#/components/schemas/AiArtifactStatus" } + confidence: { type: number, nullable: true } + contentRef: { type: object } + abstentionReason: { type: string, nullable: true } + claims: { type: array, items: { type: object } } + evidence: { type: array, items: { type: object } } + modelRun: { type: object } + + ScheduleMeeting: + type: object + required: [meetingType, title, startAt] + properties: + meetingType: { $ref: "#/components/schemas/MeetingType" } + title: { type: string } + startAt: { type: string, format: date-time } + endAt: { type: string, format: date-time } + timezone: { type: string } + requestId: { type: string } + attendees: + type: array + items: + type: object + required: [userId] + properties: + userId: { type: string } + displayName: { type: string } + role: { type: string, enum: [REQUIRED, OPTIONAL] } + visibility: { type: string, enum: [FULL, LIMITED, NONE] } + + Meeting: + type: object + properties: + id: { type: string } + meetingType: { $ref: "#/components/schemas/MeetingType" } + status: { type: string, enum: [REQUESTED, SCHEDULED, CONFIRMED, CANCELLED, COMPLETED, NO_SHOW] } + title: { type: string } + organizerActorId: { type: string } + startAt: { type: string, format: date-time, nullable: true } + timezone: { type: string } + participants: { type: array, items: { type: object } }