{"openapi":"3.1.0","info":{"title":"Znatoka API","version":"2.0.0","description":"SaaS chat for buyer↔seller disputes, contact-leak-proof, multi-tenant.\n\n**Tenant authentication:** send the header `X-Api-Key: <api_key>` (obtained when signing up at /signup or from the Portal).\n\n**Integration flow:** (1) create a conversation → get `buyer_url` & `seller_url`, send them to each party. (2) Set the tenant `webhook_url` → on each incoming message, the SaaS POSTs an HMAC-signed `message.created` event to your platform, use `open_conversation_url` to send a notification. (3) Optional: send a system message / close the conversation.","contact":{"name":"Znatoka","url":"https://znatoka.com"}},"servers":[{"url":"https://znatoka.com","description":"Production"}],"tags":[{"name":"Conversations","description":"Manage conversations (requires the tenant X-Api-Key)."},{"name":"Participant","description":"Opened directly by the buyer/seller via a token link — no API key."},{"name":"Widget","description":"Support widget for your own site (visitor → you). Public: uses the widget_key, never the API key."},{"name":"Meta","description":"Service status."}],"components":{"securitySchemes":{"ApiKey":{"type":"apiKey","in":"header","name":"X-Api-Key","description":"Tenant API key."}},"schemas":{"Party":{"type":"object","description":"Party label. `name` = the label the OTHER PARTY SEES (anonymize here).","properties":{"name":{"type":"string","example":"Buyer #4821"},"self":{"type":"string","example":"You (Buyer)","description":"Label for the party themselves (optional)."}}},"NewConversation":{"type":"object","properties":{"external_ref":{"type":"string","nullable":true,"example":"REFUND-PR05001","description":"An ID owned by your platform."},"subject":{"type":"string","nullable":true,"example":"Refund: Gmail can't log in"},"buyer":{"$ref":"#/components/schemas/Party"},"seller":{"$ref":"#/components/schemas/Party"}}},"ConversationCreated":{"type":"object","properties":{"id":{"type":"string","example":"conv_ab12cd34ef"},"status":{"type":"string","example":"open"},"external_ref":{"type":"string","nullable":true},"buyer_url":{"type":"string","format":"uri","description":"Chat link for the buyer."},"seller_url":{"type":"string","format":"uri","description":"Chat link for the seller."}}},"Message":{"type":"object","properties":{"id":{"type":"integer","example":12},"sender_role":{"type":"string","enum":["buyer","seller","system"]},"body":{"type":"string"},"created_at":{"type":"string","format":"date-time"}}},"PlatformMessage":{"type":"object","required":["body"],"properties":{"role":{"type":"string","enum":["buyer","seller","system"],"default":"system"},"body":{"type":"string","example":"Admin: refund approved."},"notify":{"type":"boolean","default":true,"description":"false = enters the transcript + SSE but does not trigger a webhook (mirror)."}}},"WebhookEvent":{"type":"object","description":"Body the SaaS POSTs to the tenant webhook_url. Header `X-Chatdisiniaja-Signature: sha256=<HMAC body, webhook_secret>`.","properties":{"event":{"type":"string","example":"message.created"},"sent_at":{"type":"string","format":"date-time"},"conversation_id":{"type":"string"},"external_ref":{"type":"string","nullable":true},"sender_role":{"type":"string","enum":["buyer","seller"]},"preview":{"type":"string","example":"Hi, the account won't log in..."},"notify_role":{"type":"string","enum":["buyer","seller"],"description":"The party that must be notified."},"open_conversation_url":{"type":"string","format":"uri"}}},"Error":{"type":"object","properties":{"error":{"type":"string","example":"invalid_api_key"}}}}},"paths":{"/api/v1/conversations":{"post":{"tags":["Conversations"],"summary":"Create a new conversation","operationId":"createConversation","security":[{"ApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NewConversation"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationCreated"}}}},"401":{"description":"Invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/conversations/{id}":{"get":{"tags":["Conversations"],"summary":"Conversation detail + transcript","operationId":"getConversation","security":[{"ApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"404":{"description":"Not found"}}}},"/api/v1/conversations/{id}/messages":{"post":{"tags":["Conversations"],"summary":"Send a message on behalf of the platform/role","operationId":"postPlatformMessage","security":[{"ApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformMessage"}}}},"responses":{"201":{"description":"Sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"400":{"description":"Empty body"},"404":{"description":"Not found"}}}},"/api/v1/conversations/{id}/close":{"post":{"tags":["Conversations"],"summary":"Close the conversation","operationId":"closeConversation","security":[{"ApiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Closed"},"404":{"description":"Not found"}}}},"/c/{token}/info":{"get":{"tags":["Participant"],"summary":"Conversation info for a participant","operationId":"participantInfo","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"404":{"description":"Invalid link"}}}},"/c/{token}/messages":{"get":{"tags":["Participant"],"summary":"List messages","operationId":"participantMessages","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}},"post":{"tags":["Participant"],"summary":"Participant sends a message","operationId":"participantSend","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["body"],"properties":{"body":{"type":"string"}}}}}},"responses":{"201":{"description":"Sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"409":{"description":"Conversation closed"}}}},"/c/{token}/stream":{"get":{"tags":["Participant"],"summary":"Realtime SSE (text/event-stream)","operationId":"participantStream","description":"Server-Sent Events. A `message` event for each new message, `ready` on connect, keep-alive every 25s.","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"SSE stream","content":{"text/event-stream":{}}}}}},"/widget.js":{"get":{"tags":["Widget"],"summary":"Support-widget loader script","operationId":"widgetScript","description":"Public, no API key. Paste `<script src=\"/widget.js\" data-site=\"<widget_key>\" async></script>` on your site for a floating support bubble. Attributes: `data-site` (required, public widget_key), `data-position` (left|right), `data-color`, `data-label`, `data-fullscreen` (mobile|always|off), `data-title-alert` (on|off). The chat iframe is loaded hidden on page load so its SSE stream is live before the bubble is clicked; it posts `{type:\"cdj:unread\",count}` to the parent, which draws the unread badge.","responses":{"200":{"description":"JavaScript","content":{"application/javascript":{}}}}}},"/w/{widgetKey}/session":{"post":{"tags":["Widget"],"summary":"Open or restore the visitor conversation","operationId":"widgetSession","description":"Public (CORS `*`), no API key — `widgetKey` is public by design. Returns the participant token for this visitor, restoring their existing OPEN conversation (`external_ref = widget:<visitor_id>`) or creating one. Rate-limited per IP.","parameters":[{"name":"widgetKey","in":"path","required":true,"schema":{"type":"string"},"example":"wk_8b1bee6b..."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"visitor_id":{"type":"string","description":"Stable random id kept in the visitor browser localStorage. Omit to be assigned one."},"idt":{"type":"string","description":"Optional signed identity token, to link the chat to a logged-in account."}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string","description":"Participant token — load /c/{token}?embed=1."},"visitor_id":{"type":"string","description":"Echoed/assigned id. Persist it to restore this thread later."},"created":{"type":"boolean","description":"true = a new conversation was opened, false = an existing one was restored."},"status":{"type":"string","enum":["open","closed"]}}}}}},"402":{"description":"Conversation quota exceeded (Free plan)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown widget key (`invalid_widget_key`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Content blocked by moderation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited per IP (`rate_limited`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/health":{"get":{"tags":["Meta"],"summary":"Service status","operationId":"health","responses":{"200":{"description":"OK"}}}}}}