1"use strict";(globalThis.webpackChunkorki_docs=globalThis.webpackChunkorki_docs||[]).push([[3524],{2474(e,n,s){s.d(n,{A:()=>t});const t=s.p+"assets/files/Orki-Production.postman_environment-815445c9dea124e9cb8cce8958abd9ee.json"},6099(e,n,s){s.r(n),s.d(n,{assets:()=>c,contentTitle:()=>o,default:()=>h,frontMatter:()=>d,metadata:()=>t,toc:()=>a});const t=JSON.parse('{"id":"integrations/web-chat/customer-chat-api","title":"Customer Chat API (Build Your Own Chat UI)","description":"Build your own chat frontend on Orki\'s backend \u2014 your design, your components, while","source":"@site/docs/integrations/web-chat/customer-chat-api.md","sourceDirName":"integrations/web-chat","slug":"/integrations/web-chat/customer-chat-api","permalink":"/docs/integrations/web-chat/customer-chat-api","draft":false,"unlisted":false,"tags":[],"version":"current","sidebarPosition":4,"frontMatter":{"sidebar_position":4},"sidebar":"tutorialSidebar","previous":{"title":"Mobile SDKs (React Native & Flutter)","permalink":"/docs/integrations/web-chat/mobile-sdks"},"next":{"title":"Email","permalink":"/docs/category/email"}}');var i=s(5656),r=s(6145);const d={sidebar_position:4},o="Customer Chat API (Build Your Own Chat UI)",c={},a=[{value:"1. Architecture in one paragraph",id:"1-architecture-in-one-paragraph",level:2},{value:"2. Authentication",id:"2-authentication",level:2},{value:"2a. The session token (all modes)",id:"2a-the-session-token-all-modes",level:3},{value:"2b. Identified users \u2014 tenant-signed JWT (recommended)",id:"2b-identified-users--tenant-signed-jwt-recommended",level:3},{value:"2c. Anonymous visitors \u2014 Turnstile (web frontends only)",id:"2c-anonymous-visitors--turnstile-web-frontends-only",level:3},{value:"The session response",id:"the-session-response",level:3},{value:"3. Endpoint reference",id:"3-endpoint-reference",level:2},{value:"Bootstrap & profile",id:"bootstrap--profile",level:3},{value:"Conversation",id:"conversation",level:3},{value:"Media",id:"media",level:3},{value:"4. The SignalR hub (send + receive)",id:"4-the-signalr-hub-send--receive",level:2},{value:"5. The message model (what you render)",id:"5-the-message-model-what-you-render",level:2},{value:"6. Integration checklist",id:"6-integration-checklist",level:2}];function l(e){const n={a:"a",admonition:"admonition",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",input:"input",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components},{Details:t}=n;return t||function(e,n){throw new Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.header,{children:(0,i.jsx)(n.h1,{id:"customer-chat-api-build-your-own-chat-ui",children:"Customer Chat API (Build Your Own Chat UI)"})}),"\n",(0,i.jsxs)(n.p,{children:["Build your ",(0,i.jsx)(n.strong,{children:"own"})," chat frontend on Orki's backend \u2014 your design, your components, while\nOrki's AI agents, human handover, knowledge base, and the operator dashboard all keep\nworking unchanged. This is the exact same API that powers the official widget; nothing here\nis a second-class surface."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"Shortcuts:"})," ",(0,i.jsx)(n.a,{href:"https://www.npmjs.com/package/@orki/chat-core",children:(0,i.jsx)(n.code,{children:"@orki/chat-core"})})," (JS/TS \u2014\nbrowser, React Native, Node) and the ",(0,i.jsx)(n.code,{children:"OrkiChatClient"})," class inside\n",(0,i.jsx)(n.a,{href:"https://pub.dev/packages/orki_webchat",children:(0,i.jsx)(n.code,{children:"orki_webchat"})})," (Dart/Flutter) wrap everything on\nthis page. Read on if you want to call the API directly or build for another stack."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"Postman playground:"})," ",(0,i.jsx)(n.a,{target:"_blank","data-noBrokenLinkCheck":!0,href:s(8785).A+"",children:"collection"})," \xb7\n",(0,i.jsx)(n.a,{target:"_blank","data-noBrokenLinkCheck":!0,href:s(2474).A+"",children:"production environment"})," \u2014 import both,\nfill in ",(0,i.jsx)(n.code,{children:"tenant_id"})," / ",(0,i.jsx)(n.code,{children:"integration_id"}),", run ",(0,i.jsx)(n.em,{children:"Mint / resume session"})," first (its test script\ncaptures ",(0,i.jsx)(n.code,{children:"session_token"}),", ",(0,i.jsx)(n.code,{children:"customer_id"}),", ",(0,i.jsx)(n.code,{children:"chat_id"})," into variables for every other request).\nThe collection demonstrates the anonymous flow; on a ",(0,i.jsx)(n.code,{children:"jwt"}),"-mode integration, add\n",(0,i.jsx)(n.code,{children:'"identityToken": "\u2026"'})," to that request's body as shown in \xa72b below."]}),"\n",(0,i.jsx)(n.h2,{id:"1-architecture-in-one-paragraph",children:"1. Architecture in one paragraph"}),"\n",(0,i.jsxs)(n.p,{children:["Your frontend talks to ",(0,i.jsx)(n.strong,{children:"one service"})," over ",(0,i.jsx)(n.strong,{children:"one base path"})," \u2014 the public chat API:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"https://app.orki.ai/services/public/ext/v1/tenants/{tenantId}/integrations/{integrationId}/...\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Everything except sending a message is plain REST (session, profile, history, media upload,\nread receipts). ",(0,i.jsx)(n.strong,{children:"Sending messages and receiving replies happens over a SignalR WebSocket\nhub"})," \u2014 there is deliberately no REST send endpoint. Replies can come from an AI agent or a\nhuman supp
1ort agent; your client renders both the same way."]}),"\n",(0,i.jsx)(n.admonition,{title:"The two production base URIs",type:"note",children:(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"https://app.orki.ai/services/public"})," \u2014 the ",(0,i.jsx)(n.strong,{children:"public chat API"})," (this page). Customer-side,\nsession-token auth, everything a custom chat UI needs."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"https://app.orki.ai/services/gateway"})," \u2014 the ",(0,i.jsx)(n.strong,{children:"dashboard/management API"})," (Keycloak-authed\noperator surface: agents, integrations, campaigns\u2026). Never call it from a customer-facing\nfrontend; it is not part of this integration."]}),"\n"]})}),"\n",(0,i.jsxs)(n.p,{children:["You need a ",(0,i.jsx)(n.code,{children:"tenantId"})," and an ",(0,i.jsx)(n.code,{children:"integrationId"})," \u2014 create a web integration in the dashboard\n(",(0,i.jsx)(n.strong,{children:"Integrations \u2192 Website"}),") and copy both from the install panel."]}),"\n",(0,i.jsx)(n.h2,{id:"2-authentication",children:"2. Authentication"}),"\n",(0,i.jsxs)(n.p,{children:["Two layers, one simple rule: ",(0,i.jsxs)(n.strong,{children:["your app holds a short-lived ",(0,i.jsx)(n.code,{children:"session_token"})," and sends it\neverywhere"]}),"; how you ",(0,i.jsx)(n.em,{children:"obtain"})," that token depends on the integration's auth mode."]}),"\n",(0,i.jsx)(n.h3,{id:"2a-the-session-token-all-modes",children:"2a. The session token (all modes)"}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"POST /session"})," \u2192 the response carries ",(0,i.jsx)(n.strong,{children:(0,i.jsx)(n.code,{children:"session_token"})})," (a 30-day JWT) plus\n",(0,i.jsx)(n.code,{children:"customer_id"})," and ",(0,i.jsx)(n.code,{children:"chat_id"}),"."]}),"\n",(0,i.jsxs)(n.li,{children:["Persist it (localStorage / secure storage) and send it as:","\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"Authorization: Bearer <session_token>"})," on every REST call"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"?access_token=<session_token>"})," on the WebSocket URL"]}),"\n"]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["To ",(0,i.jsx)(n.strong,{children:"resume"})," on a later launch, call ",(0,i.jsx)(n.code,{children:"POST /session"})," again ",(0,i.jsx)(n.em,{children:"with"})," the bearer \u2014 the same\ncustomer and chat come back. On ",(0,i.jsx)(n.code,{children:"401"}),", drop the stored token and start fresh."]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Two mandatory request rules:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:(0,i.jsx)(n.code,{children:"X-Public-Chat-Client: 1"})})," header on every ",(0,i.jsx)(n.code,{children:"POST"}),"/",(0,i.jsx)(n.code,{children:"PUT"}),"/",(0,i.jsx)(n.code,{children:"DELETE"}),"\n(missing = ",(0,i.jsx)(n.code,{children:'403 {"error":"Missing X-Public-Chat-Client header"}'})," \u2014 CSRF guard)."]}),"\n",(0,i.jsxs)(n.li,{children:["If your HTTP stack sends an ",(0,i.jsx)(n.code,{children:"Origin"})," header it must match the domain registered on the\nintegration (",(0,i.jsx)(n.code,{children:"localhost"})," always allowed). Clients that send no ",(0,i.jsx)(n.code,{children:"Origin"})," (native apps,\nserver-side) are unaffected."]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"2b-identified-users--tenant-signed-jwt-recommended",children:"2b. Identified users \u2014 tenant-signed JWT (recommended)"}),"\n",(0,i.jsxs)(n.p,{children:["For integrations in ",(0,i.jsx)(n.strong,{children:(0,i.jsx)(n.code,{children:"jwt"})})," or ",(0,i.jsx)(n.strong,{children:(0,i.jsx)(n.code,{children:"both"})})," auth mode\n(see ",(0,i.jsx)(n.a,{href:"/docs/integrations/web-chat/authenticated-identity",children:"Authenticated Visitors"}),"), your backend signs a short\nidentity JWT and the frontend exchanges it at bootstrap:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-http",children:'POST /session\nContent-Type: application/json\nX-Public-Chat-Client: 1\n\n{ "identityToken": "<HS256 JWT: {sub, iat, exp\u226424h, tenantId, integrationId, name?, email?}>" }\n'})}),"\n",(0,i.jsxs)(n.p,{children:["A valid signature ",(0,i.jsx)(n.strong,{children:"replaces the bot check entirely"})," \u2014 no Turnstile, and the visitor is\nbound to your ",(0,i.jsx)(n.code,{children:"sub"})," (same conversation on every device, ",(0,i.jsx)(n.code,{children:"customer.identityVerified: true"}),").\nRe-POST ",(0,i.jsx)(n.code,{children:"/session"})," with a fresh ",(0,i.jsx)(n.code,{children:"identityToken"})," any time before it expires; the session\ncontinues in place. This is the mode to use for apps behind your own login, and the ",(0,i.jsx)(n.strong,{children:"only"}),"\nmode native mobile custom UIs can use."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.strong,{children:"401 error codes:"})," ",(0,i.jsx)(n.code,{children:"jwt_required"}),", ",(0,i.jsx)(n.code,{children:"jwt_invalid_signature"}),", ",(0,i.jsx)(n.code,{children:"jwt_expired"}),",\n",(0,i.jsx)(n.code,{children:"jwt_not_yet_valid"}),", ",(0,i.jsx)(n.code,{children:"jwt_exp_too_far"}),", ",(0,i.jsx)(n.code,{children:"jwt_tenant_mismatch"}),", ",(0,i.jsx)(n.code,{children:"jwt_integration_mismatch"}),",\n",(0,i.jsx)(n.code,{children:"jwt_malformed"}),". In ",(0,i.jsx)(n.code,{children:"both"})," mode with ",(0,i.jsx)(n.em,{children:"Enforce identity"}
1)," off, an invalid JWT still returns\n",(0,i.jsx)(n.strong,{children:"200"})," with an anonymous session plus the response header ",(0,i.jsx)(n.code,{children:"X-Orki-Identity-Error: <code>"})," \u2014\nwatch for it and refresh your token."]}),"\n",(0,i.jsx)(n.h3,{id:"2c-anonymous-visitors--turnstile-web-frontends-only",children:"2c. Anonymous visitors \u2014 Turnstile (web frontends only)"}),"\n",(0,i.jsxs)(n.p,{children:["In ",(0,i.jsx)(n.code,{children:"anonymous"}),"/",(0,i.jsx)(n.code,{children:"both"})," mode, a ",(0,i.jsx)(n.strong,{children:"new"})," session must pass Cloudflare Turnstile: render the\n",(0,i.jsx)(n.a,{href:"https://developers.cloudflare.com/turnstile/",children:"Turnstile widget"})," on your page with Orki's\nsitekey (ask us for it) and send the result:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{ "turnstileToken": "<token from the Turnstile callback>" }\n'})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"403"})," = challenge failed/stale \u2192 re-render and retry. Resuming with a stored bearer never\nneeds Turnstile. Because Turnstile is browser-only, anonymous mode is ",(0,i.jsx)(n.strong,{children:"not available to\nnative custom UIs"})," \u2014 use the identity JWT there."]}),"\n",(0,i.jsxs)(n.p,{children:["Rate limits: anonymous mints are capped at 30/IP/hour; JWT-verified bootstraps at\n600/integration/hour (both ",(0,i.jsx)(n.code,{children:"429"}),")."]}),"\n",(0,i.jsx)(n.h3,{id:"the-session-response",children:"The session response"}),"\n",(0,i.jsxs)(n.p,{children:["Top level is ",(0,i.jsx)(n.code,{children:"snake_case"})," (historic); everything else in the API is ",(0,i.jsx)(n.code,{children:"camelCase"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n "customer_id": "665f0c\u2026",\n "chat_id": "665f0d\u2026",\n "unread_count": 0,\n "session_token": "eyJhbGciOiJIUzI1NiIs\u2026",\n "customer": { "id": "\u2026", "name": "Layla", "email": null, "phone": null,\n "language": null, "identityVerified": true },\n "chat": { "id": "\u2026", "status": "unopened", "handler": "\u2026",\n "handlerName": "Maya", "unreadCount": 0 }\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["A fresh anonymous session creates a ",(0,i.jsx)(n.strong,{children:"ghost customer"})," and an ",(0,i.jsx)(n.strong,{children:"unopened chat"}),"; the chat\nflips to ",(0,i.jsx)(n.code,{children:"open"})," and the customer becomes real automatically when the first message is\nprocessed. If an anonymous visitor later logs in (you send an ",(0,i.jsx)(n.code,{children:"identityToken"})," on the same\nsession), their conversation is upgraded in place \u2014 history preserved."]}),"\n",(0,i.jsx)(n.h2,{id:"3-endpoint-reference",children:"3. Endpoint reference"}),"\n",(0,i.jsxs)(n.p,{children:["All paths relative to ",(0,i.jsx)(n.code,{children:"\u2026/tenants/{tenantId}/integrations/{integrationId}"})," unless noted.\n\ud83d\udd13 = anonymous \xb7 \ud83d\udd11 = bearer required \xb7 \u2709\ufe0f = also needs ",(0,i.jsx)(n.code,{children:"X-Public-Chat-Client: 1"}),"."]}),"\n",(0,i.jsx)(n.h3,{id:"bootstrap--profile",children:"Bootstrap & profile"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{}),(0,i.jsx)(n.th,{children:"Endpoint"}),(0,i.jsx)(n.th,{children:"Notes"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"\ud83d\udd13"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:'GET ""'})}),(0,i.jsxs)(n.td,{children:["Widget/branding config: ",(0,i.jsx)(n.code,{children:"primaryColor"}),", ",(0,i.jsx)(n.code,{children:"starterMessages"}),", ",(0,i.jsx)(n.code,{children:"initialForm"})," (which profile fields to collect), ",(0,i.jsx)(n.code,{children:"aiProfileName"}),", ",(0,i.jsx)(n.code,{children:"authMode"}),", ",(0,i.jsx)(n.code,{children:"showPoweredBy"})," \u2014 useful even for a custom UI (greeting texts, which pre-chat fields the tenant enabled)"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"\ud83d\udd13\u2709\ufe0f"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"POST /session"})}),(0,i.jsxs)(n.td,{children:["Mint or resume \u2014 see \xa72. Body fields all optional: ",(0,i.jsx)(n.code,{children:"identityToken"}),", ",(0,i.jsx)(n.code,{children:"turnstileToken"}),", ",(0,i.jsx)(n.code,{children:"chatName"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"\ud83d\udd11"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"GET /customer/me"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"{customer, chat}"})," \u2014 ",(0,i.jsx)(n.code,{children:"customer: null"}),' means "call POST /session"']})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"\ud83d\udd11\u2709\ufe0f"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"PUT /customer/me"})}
1),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:'{"name": "\u2026", "email": "\u2026", "phone": "\u2026"}'})," \u2014 omitted/null fields untouched. Use for a pre-chat form (skip it for JWT-identified users; their claims already filled the profile)"]})]})]})]}),"\n",(0,i.jsx)(n.h3,{id:"conversation",children:"Conversation"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{}),(0,i.jsx)(n.th,{children:"Endpoint"}),(0,i.jsx)(n.th,{children:"Notes"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"\ud83d\udd11"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"GET /chats/{chatId}"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"{id, tenantId, status, handler, createdAt, platformId}"}),' \u2014 poll it if you want to display "you\'re now talking to a human" on handover']})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"\ud83d\udd11"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"GET /chats/{chatId}/messages?pageSize=50"})}),(0,i.jsxs)(n.td,{children:["Newest-first. Response ",(0,i.jsx)(n.code,{children:"{data: Message[], hasMore, nextBefore}"}),". Older pages: repeat with ",(0,i.jsx)(n.code,{children:"&before={nextBefore}"})," (URL-encoded ISO timestamp)"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"\ud83d\udd11\u2709\ufe0f"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"POST /chats/{chatId}/read"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:'{"lastMessageId": "\u2026"}'})," \u2014 marks that message and everything older as read"]})]})]})]}),"\n",(0,i.jsx)(n.h3,{id:"media",children:"Media"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{}),(0,i.jsx)(n.th,{children:"Endpoint"}),(0,i.jsx)(n.th,{children:"Notes"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"\ud83d\udd11\u2709\ufe0f"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"POST /chats/{chatId}/media/temp"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"multipart/form-data"}),", field ",(0,i.jsx)(n.code,{children:"file"}),", one file. ",(0,i.jsx)(n.strong,{children:"25 MiB max"})," (",(0,i.jsx)(n.code,{children:"413"}),"), virus/type-scanned (",(0,i.jsx)(n.code,{children:"400"}),") \u2192 ",(0,i.jsx)(n.code,{children:'{"ref", "contentType", "sizeBytes", "fileName"}'})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"\ud83d\udd13"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"GET {base}/temp-media/{tenantId}/{ref}"})}),(0,i.jsx)(n.td,{children:"Preview a staged upload (the unguessable ref is the auth). Not under the tenants prefix"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"\ud83d\udd11\u2709\ufe0f"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"DELETE /chats/{chatId}/media/temp/{ref}"})}),(0,i.jsxs)(n.td,{children:["Un-stage. Idempotent, ",(0,i.jsx)(n.code,{children:"204"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"\ud83d\udd11\u2709\ufe0f"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"POST /chats/{chatId}/media/from-temp"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:'{"refs": ["\u2026"]}'})," \u2192 ",(0,i.jsx)(n.code,{children:'{"ids": ["\u2026"]}'})," \u2014 promotes staged files; the ",(0,i.jsx)(n.code,{children:"ids"})," go into the message's ",(0,i.jsx)(n.code,{children:"mediaIds"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"\ud83d\udd11\u2709\ufe0f"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"POST /chats/{chatId}/media"})}),(0,i.jsxs)(n.td,{children:["Legacy one-shot multipart upload \u2192 ",(0,i.jsx)(n.code,{children:'{"id"}'})," (the widget uses it for voice notes). Prefer temp\u2192promote"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"\ud83d\udd11"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"GET /chats/{chatId}/message/{messageId}/media"})}),(0,i.jsxs)(n.td,{children:["Attachment metadata: ",(0,i.jsx)(n.code,{children:"[{id, mimeType, name, size, width, height, numOfPages}]"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"\ud83d\udd11"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"GET /chats/{chatId}/message/{messageId}/media/{mediaId}"})}),(0,i.jsxs)(n.td,{children:["The bytes (range requests supported). For ",(0,i.jsx)(n.code,{children:"<img src>"}),"-style loading append ",(0,i.jsx)(n.code,{children:"?access_token={session_token}"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"\ud83d\udd13"}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"GET /chats/{chatId}/handler/photo"})," \xb7 ",(0,i.jsx)(n.code,{children:"GET /default-handler/photo"})]}),(0,i.jsxs)(n.td,{children:["Agent avatar (",(0,i.jsx)(n.code,{children:"204"})," if none)"]})]})]})]}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"Sending a message with attachments \u2014 the full sequence:"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:'1. POST \u2026/media/temp (once per file) \u2192 refs\n2. (optional) preview GET \u2026/temp-media/{tenantId}/{ref}\n3. POST \u2026/media/from-temp {"refs":[\u2026]} \u2192 ids\n4. hub invoke CustomerMessage { content: { text: "\u2026", mediaIds: ids } }\n'})}),"\n",(0,i.jsx)(n.h2,{id:"4-the-signalr-hub-send--receive",children:"4. The SignalR hub (send + receive)"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"wss://app.orki.ai/services/public/ext/v1/chat/hub?tenantId={t}&integrationId={i}&access_token={session_token}\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Use a ",(0,i.jsx)(n.strong,{children:"SignalR client library"})," \u2014 ",(0,i.jsx)(n.code,{children:"@microsoft/signalr"})," (JS / React Native),\n",(0,i.jsx)(n.code,{children:"com.microsoft.signalr"})," (Android), ",(0,i.jsx)(n.code,{children:"Microsoft.AspNetCore.SignalR.Client"}
1)," (.NET/MAUI),\n",(0,i.jsx)(n.code,{children:"signalr_netcore"})," (Flutter). Connect with ",(0,i.jsx)(n.code,{children:"skipNegotiation: true"})," and the WebSockets\ntransport, ",(0,i.jsxs)(n.strong,{children:["only after ",(0,i.jsx)(n.code,{children:"POST /session"})," succeeded"]})," \u2014 the server aborts connections whose\ntoken doesn't map to an existing customer."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-js",children:'import * as signalR from "@microsoft/signalr";\n\nconst BASE = "https://app.orki.ai/services/public/ext/v1";\n\nconst connection = new signalR.HubConnectionBuilder()\n .withUrl(`${BASE}/chat/hub?tenantId=${tenantId}&integrationId=${integrationId}`, {\n accessTokenFactory: () => sessionToken, // appended as ?access_token=\n skipNegotiation: true,\n transport: signalR.HttpTransportType.WebSockets,\n })\n .withAutomaticReconnect()\n .build();\n\nconnection.on("Message", ({ tempId, message }) => {\n if (tempId) reconcileOptimisticBubble(tempId, message); // echo of your own send\n else renderIncoming(message); // AI or human reply\n});\nconnection.on("MessageEdited", ({ message }) => mergeMessage(message));\nconnection.on("TypingIndicator", ({ show, isCustomer }) => {\n if (isCustomer === false) setAgentTyping(show); // auto-clear after ~20 s\n});\nconnection.onreconnected(() => refetchLatestHistoryPage()); // fill any gap\n\nawait connection.start();\n\nawait connection.invoke("CustomerMessage", {\n chatId,\n tempId: crypto.randomUUID(), // echoed back \u2192 reconcile your optimistic bubble\n content: { text: "Hello!" },\n correlationId: crypto.randomUUID(),\n});\n'})}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"You invoke:"})}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Method"}),(0,i.jsx)(n.th,{children:"Payload"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"CustomerMessage"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"{chatId, tempId, content: {text?, mediaIds?}, replyTo?, correlationId}"})})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"Typing"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"{chatId, userId: customer_id, show: true|false}"})," \u2014 throttle to \u22641 ",(0,i.jsx)(n.code,{children:"show:true"})," per 10 s"]})]})]})]}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"You receive:"})}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Event"}),(0,i.jsx)(n.th,{children:"Payload"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"Message"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"{tempId?, message}"})," \u2014 your own echo (with your ",(0,i.jsx)(n.code,{children:"tempId"}),") ",(0,i.jsx)(n.em,{children:"and"})," every agent/human reply; ",(0,i.jsx)(n.code,{children:"message.isCustomer"})," tells you which side"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"MessageEdited"})}),(0,i.jsxs)(n.td,{children:["Same shape \u2014 merge ",(0,i.jsx)(n.code,{children:"status"})," / ",(0,i.jsx)(n.code,{children:"content"})," updates"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"TypingIndicator"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"{chatId, userId, show, isCustomer}"})})]})]})]}),"\n",(0,i.jsxs)(t,{children:[(0,i.jsxs)("summary",{children:[(0,i.jsx)("b",{children:"Raw-frame appendix"})," \u2014 driving the hub from Postman (demos / debugging)"]}),(0,i.jsxs)(n.p,{children:["Postman: ",(0,i.jsx)(n.strong,{children:"New \u2192 WebSocket"}),", paste the URL above, connect, then send these frames in order.\nEvery SignalR frame must end with the invisible ASCII record separator ",(0,i.jsx)(n.code,{children:"0x1E"})," (shown as ",(0,i.jsx)(n.code,{children:"\u241e"}),"\n\u2014 don't type it literally; copy frames from a file where the byte is real, or append it\nprogrammatically)."]}),(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:'{"protocol":"json","version":1}\u241e \u2190 handshake; server answers {}\u241e\n{"type":1,"target":"CustomerMessage","arguments":[{"chatId":"<CHAT_ID>","tempId":"<uuid>","content":{"text":"Hi!"},"correlationId":"<uuid>"}]}\u241e\n{"type":1,"target":"Typing","arguments":[{"chatId":"<CHAT_ID>","userId":"<CUSTOMER_ID>","show":true}]}\u241e\n{"type":6}\u241e \u2190 keepalive; send every ~20 s or the server drops you\n'})}),(0,i.jsxs)(n.p,{children:["Frame ",(0,i.jsx)(n.code,{children:"type"})," values: ",(0,i.jsx)(n.code,{children:"1"})," invocation (both directions), ",(0,i.jsx)(n.code,{children:"3"})," completion, ",(0,i.jsx)(n.code,{children:"6"})," ping, ",(0,i.jsx)(n.code,{children:"7"})," server\nclosing (the ",(0,i.jsx)(n.code,{children:"error"})," field says why)."]})]}),"\n",(0,i.jsx)(n.h2,{id:"5-the-message-model-what-you-render",children:"5. The message model (what you render)"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-jsonc",children:'{\n "id": "665f\u2026", // real Mongo ObjectId once stored\n "chatId": "\u2026",\n "isCustomer": true, // true = the visitor; false = AI agent or human handler\n "sender": "\u2026",\n "content": {\n "text": "Hello!", // markdown-ish (WhatsApp flavour)\n "mediaIds": ["\u2026"], // fetch metadata + bytes via the media endpoints\n "location": [lon, lat],\n "carousel": [ { "header", "footer", "headerImage", "buttonTitle", "buttonLink" } ],\n "choices": [ { "id", "title" } ] // tap-to-reply buttons the agent offered\n },\n "replyTo": { "messageId", "content", "sender", "isCustomer" },\n "timestamp": "2026-08-11T09:12:33.123Z",\n "status": "stored", // stored \u2192 sent \u2192 delivered \u2192 read | failed\n "readBy": [ { "userId", "timestamp" } ]\n}\n'})}),"\n",(0,i.jsx)(n.p,{children:"Rendering rules that make the UX right:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Media messages"})," carry only ",(0,i.jsx)(n.code,{children:"mediaIds"})," \u2014 fetch the metadata endpoint, then render by\n",(0,i.jsx)(n.code,{children:"mimeType"})," prefix (image/video inline, audio player, document card). Only for real\n(24-char id) messages, not your optimistic bubbles."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Voice notes you send"})," (audio upload + empty text) are transcribed server-side and the\nAI answers the transcription \u2014 keep showing your local audio player in the customer bubble."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Handover to a human"})," needs no special handling: replies keep arriving as ",(0,i.jsx)(n.code,{children:"Message"})," with\n",(0,i.jsx)(n.code,{children:"isCustomer: false"}),". Poll ",(0,i.jsx)(n.code,{children:"GET /chats/{chatId}"})," if you want to badge the handler change."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Resolved chats reopen implicitly"})," \u2014 just send another ",(0,i.jsx)(n.code,{children:"CustomerMessage"}),"."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["Worth knowing when designing the attachment UI: ",(0,i.jsx)(n.strong,{children:"images, PDFs and office documents are\npassed into the AI model"})," (it genuinely reads them); ",(0,i.jsx)(n.strong,{children:"voice notes are auto-transcribed"}),";\n",(0,i.jsx)(n.strong,{children:"video"})," is stored for human agents but not interpreted by the AI."]}),"\n",(0,i.jsx)(n.h2,{id:"6-integration-checklist",children:"6. Integration checklist"}),"\n",(0,i.jsxs)(n.ul,{className:"contains-task-list",children:["\n",(0,i.jsxs)(n.li,{className:"task-list-item",children:[(0,i.jsx)(n.input,{type:"checkbox",disabled:!0})," ","Store ",(0,i.jsx)(n.code,{children:"session_token"}),"; resume via ",(0,i.jsx)(n.code,{children:"POST /session"})," with the bearer on every launch"]}),"\n",(0,i.jsxs)(n.li,{className:"task-list-item",children:[(0,i.jsx)(n.input,{type:"checkbox",disabled:!0})," ","Identified users: mint the identity JWT ",(0,i.jsx)(n.strong,{children:"server-side"}),", exchange at bootstrap, refresh\nvia a fresh ",(0,i.jsx)(n.code,{children:"POST /session"})," before ",(0,i.jsx)(n.code,{children:"exp"}),"; handle the ",(0,i.jsx)(n.code,{children:"jwt_*"})," 401 codes and\n",(0,i.jsx)(n.code,{children:"X-Orki-Identity-Error"})]}),"\n",(0,i.jsxs)(n.li,{className:"task-list-item",children:[(0,i.jsx)(n.input,{type:"checkbox",disabled:!0})," ",(0,i.jsx)(n.code,{children:"X-Public-Chat-Client: 1"})," on every mutating request"]}),"\n",(0,i.jsxs)(n.li,{className:"task-list-item",children:[(0,i.jsx)(n.input,{type:"checkbox",disabled:!0})," ","Generate a ",(0,i.jsx)(n.code,{children:"tempId"})," (UUID) per outgoing message; reconcile on the echoed ",(0,i.jsx)(n.code,{children:"Message"})]}),"\n",(0,i.jsxs)(n.li,{className:"task-list-item",children:[(0,i.jsx)(n.input,{type:"checkbox",disabled:!0})," ",(0,i.jsx)(n.code,{children:"withAutomaticReconnect()"}
1)," + refetch the latest history page on reconnect"]}),"\n",(0,i.jsxs)(n.li,{className:"task-list-item",children:[(0,i.jsx)(n.input,{type:"checkbox",disabled:!0})," ","Throttle ",(0,i.jsx)(n.code,{children:"Typing"}),"; ",(0,i.jsx)(n.code,{children:"POST /read"})," when the newest message becomes visible"]}),"\n",(0,i.jsxs)(n.li,{className:"task-list-item",children:[(0,i.jsx)(n.input,{type:"checkbox",disabled:!0})," ","Cap uploads at 25 MiB client-side; surface ",(0,i.jsx)(n.code,{children:"413"})," and scan-rejection ",(0,i.jsx)(n.code,{children:"400"}),"s"]}),"\n",(0,i.jsxs)(n.li,{className:"task-list-item",children:[(0,i.jsx)(n.input,{type:"checkbox",disabled:!0})," ","Anonymous web UIs: render Turnstile for new sessions; native UIs: JWT identity only"]}),"\n"]})]})}function h(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(l,{...e})}):l(e)}},6145(e,n,s){s.d(n,{R:()=>d,x:()=>o});var t=s(7140);const i={},r=t.createContext(i);function d(e){const n=t.useContext(r);return t.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:d(e.components),t.createElement(r.Provider,{value:n},e.children)}},8785(e,n,s){s.d(n,{A:()=>t});const t=s.p+"assets/files/Orki-InApp-Chat.postman_collection-9e25356f999db39dc1cdee2bca5697d0.json"}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.