# Iris — hosted customer messaging (irissays.com) > Iris is a hosted, HIPAA-grade real-time messaging platform. Teams create a > workspace, embed a chat widget in their apps, and answer conversations from > an operator portal — with optional server-to-server access via API keys. > This file is the agent-readable contract: everything a coding agent needs to > sign a team up, configure the workspace, and install Iris in an app. Conventions used below: - API_BASE is the platform API origin (production: https://api.irissays.chat). - All endpoints return an envelope: success `{"data": ...}`, error `{"error": {"code": "...", "message": "..."}}`. - Authenticated calls send `Authorization: Bearer `. - Two token kinds exist: a SESSION token (from signup/verify, login, or invite accept — used by humans and the admin console) and an API KEY (`iris_sk_*` / `iris_pub_*` — used by your backend against the product API). - Public write endpoints are rate-limited per IP (HTTP 429 + Retry-After). ## 1. Create a workspace (self-serve signup) 1. Check the handle you want: GET API_BASE/api/v1/signup/slug-available?slug= -> {"data": {"slug": "", "available": true|false}} Handles: 3-40 chars, lowercase letters/digits/hyphens. Reserved words (www, api, admin, docs, ...) are unavailable. 2. Create the pending signup: POST API_BASE/api/v1/signup {"full_name": "...", "email": "...", "password": "", "company_name": "...", "slug": "", "plan": "trial"} -> 202 {"data": {"signup_id", "tenant_id", "email", "slug", "verification_required": true, "verification_email_sent": true, "expires_at"}} A verification email is sent to the address. The link lands on /signup.html?tenant_id=&token=&slug= and is valid for 24 hours. If `verification_email_sent` is false and no `verification_token` is present, this environment cannot verify signups — stop and tell the user. 3. Verify (the page does this automatically; agents can call it directly with the token from the email link): POST API_BASE/api/v1/signup/verify {"tenant_id": "", "token": ""} -> 201 {"data": {"token": "", ...}} // owner session, role=admin This provisions the tenant, the owner account, default retention policy, and writes an audit trail. ## 2. Sign in later (owner / teammates) POST API_BASE/api/v1/owner/login {"email": "...", "password": "..."} -> 200 {"data": {"token": "", ...}} Sessions are role-mapped: owners/admins can administer; operators/viewers cannot. The human console for everything below is /admin.html. ## 3. Invite the team POST API_BASE/api/v1/admin/team/invites (SESSION, admin/owner) {"email": "teammate@company.com", "org_role": "admin"|"operator"|"viewer"} -> 201 {"data": {"invitation_id", "email", "org_role", "expires_at", "email_sent": true, "token": ""}} The teammate gets an email linking to /accept.html?tenant_id=&token=&email= where they set a password. Programmatic accept: POST API_BASE/api/v1/team/accept (public) {"tenant_id": "...", "token": "...", "full_name": "...", "password": "..."} -> 201 {"data": {"token": "", ...}} Manage the roster (all SESSION, admin/owner): GET API_BASE/api/v1/admin/team/members GET API_BASE/api/v1/admin/team/invites DELETE API_BASE/api/v1/admin/team/invites/{invitation_id} PATCH API_BASE/api/v1/admin/team/members/{user_id} {"org_role": "..."} DELETE API_BASE/api/v1/admin/team/members/{user_id} ## 4. Issue API keys (for your backend) POST API_BASE/api/v1/admin/keys (SESSION, admin/owner) {"name": "Production backend", "key_type": "secret"|"publishable"} -> 201 {"data": {"key_id", "key_prefix", "secret": "iris_sk_...", "scopes": [...]}} THE SECRET IS SHOWN EXACTLY ONCE. Store it in a secret manager immediately. GET API_BASE/api/v1/admin/keys // metadata only POST API_BASE/api/v1/admin/keys/{key_id}/rotate // new secret, shown once DELETE API_BASE/api/v1/admin/keys/{key_id} // revoke Key scopes (granted by default per key type): chat:channels.read, chat:channels.create, chat:channels.manage, chat:messages.read, chat:messages.write, chat:presence.write, chat:read-state.write, chat:identity.link ## 5. Embed the widget (client side) Add to the host page, with your tenant_id from signup: (Production CDN_BASE: https://cdn.irissays.chat) Then add every origin the widget loads on to the workspace's allowed origins (Admin console -> Widget -> Allowed origins, or PUT /api/v1/admin/widget-config). Per-tenant branding (colors, title, logo, welcome message, launcher position) is configured the same way and fetched publicly at: GET API_BASE/api/v1/widget-config?tenant_id= ## 6. Server-to-server (product API, API-key auth) Base path: API_BASE/api/product/v1 Auth: `Authorization: Bearer iris_sk_...` or `X-API-Key: iris_sk_...` The key's scopes gate each route; visibility is membership-scoped (the key sees channels it created or was added to). All access is tenant-isolated at the database tier. GET /api/product/v1/me // key introspection POST /api/product/v1/sessions // chat:identity.link {"external_user_id": "", "display_name"?, "email"?, "channel_name"?} -> 201 {"data": {"token": "", "session": {...}, "chat": {"channel_id", ...}}} Mints a chat session for one of YOUR app users. The same external_user_id always resolves to the same Iris user and the same durable support conversation. Hand the token to your frontend; it can read/post messages on /api/v1 and connect to the realtime socket, but can never create or manage channels. Tenant comes from your key — a tenant_id in the body is rejected. POST /api/product/v1/channels // chat:channels.create {"name": "...", "channel_type": "support", "topic": "..."} GET /api/product/v1/channels // chat:channels.read GET /api/product/v1/channels/{id} // chat:channels.read PATCH /api/product/v1/channels/{id} // chat:channels.manage POST /api/product/v1/channels/{id}/archive // chat:channels.manage GET /api/product/v1/channels/{id}/members // chat:channels.read POST /api/product/v1/channels/{id}/members // chat:channels.manage PATCH /api/product/v1/channels/{id}/members/{user_id} // chat:channels.manage DELETE /api/product/v1/channels/{id}/members/{user_id} // chat:channels.manage POST /api/product/v1/channels/{id}/messages // chat:messages.write {"content": "...", "content_type": "text"} GET /api/product/v1/channels/{id}/messages?limit=&cursor= // chat:messages.read GET /api/product/v1/messages/{id} // chat:messages.read PATCH /api/product/v1/messages/{id} // chat:messages.write DELETE /api/product/v1/messages/{id} // chat:messages.write Quick start: verify your key, create a channel, post a message: curl -s API_BASE/api/product/v1/me -H "Authorization: Bearer iris_sk_..." ## 7. Workspace introspection (for dashboards/agents) All SESSION, admin/owner: GET API_BASE/api/v1/admin/workspace // {tenant_id, name, slug, plan, status, created_at} GET API_BASE/api/v1/admin/usage // [{day, metric, value}] daily rollups GET API_BASE/api/v1/admin/retention-policy // retention windows (days) ## 8. End-user chat sessions (widget runtime) Two ways to give an end user a chat session: 1. Anonymous guests — the widget bootstraps itself via: POST API_BASE/api/v1/session/guest (public) {"tenant_id": "", "display_name": "...", "email": "..."} -> {"data": {"token": "", "chat": {...}, "resume_token": "..."}} 2. Known app users — your backend mints a session with your secret key via POST /api/product/v1/sessions (section 6) and passes the returned token to the widget script as `data-token` (the widget then skips guest bootstrap), with `data-initial-channel-id` set to the returned chat.channel_id. Realtime delivery is WebSocket at API_BASE/ws (token-authenticated). ## Compliance notes - Tenant isolation: PostgreSQL row-level security on every table, enforced inside every transaction. API keys and sessions can never cross tenants. - Encryption: TLS 1.3 in transit, AES-256 at rest. - Audit: provisioning, team, key, and admin actions write append-only audit log rows. Retention windows are per-tenant policy. - Do not put PHI in channel names, topics, or metadata. ## Pages - /signup.html create a workspace (also the email verify landing) - /accept.html teammate invite landing - /admin.html admin console (overview, team, keys, widget, install, compliance) - /docs.html human developer docs