{"openapi":"3.1.0","info":{"title":"suppy.chat public API","version":"1.0.0","summary":"Status, pricing, widget configuration and customer conversations.","description":"The public HTTP API of suppy.chat, a customer support chat product for websites and mobile apps.\n\nThese endpoints need no API key. A conversation is authorised by the `resume_token` returned when it was\ncreated: that token is the credential for that one conversation and grants nothing else. Endpoints used by\nthe operator dashboard require a signed-in operator session and are intentionally not described here.\n\nErrors are always JSON and carry a stable `code`, a `message`, a `hint` and a documentation link. A 503\nmeans a dependency (usually the database) is unreachable and carries `Retry-After`; retrying is correct.","contact":{"name":"suppy.chat support","email":"support@suppy.chat","url":"https://suppy.chat/contact"}},"servers":[{"url":"https://suppy.chat","description":"Production"}],"externalDocs":{"description":"Developer documentation, MCP server and Markdown content negotiation","url":"https://suppy.chat/developers"},"tags":[{"name":"Status","description":"Whether the service is able to serve requests."},{"name":"Catalog","description":"Public pricing and per-project widget configuration."},{"name":"Conversations","description":"Starting a conversation, posting messages and reading a thread."},{"name":"Feedback","description":"Ratings customers leave after a conversation is closed."}],"paths":{"/api/health":{"get":{"operationId":"getServiceStatus","summary":"Service status","description":"Liveness probe: reports whether the API can reach its database. Answers 200 when healthy and 503 when the database is unreachable, so it stays useful during an outage. Unauthenticated and never cached - suitable for an uptime monitor.","tags":["Status"],"responses":{"200":{"description":"The API is healthy.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceStatus"}}}},"503":{"description":"The API is running but cannot reach its database.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceStatus"}}}}}}},"/api/pricing":{"get":{"operationId":"getPricingCatalog","summary":"Price catalog","description":"The public price list: currency, how many operator seats are included, the price of an additional seat monthly and yearly, the discount applied to modules on a paid plan, the trial length for paid modules, and every purchasable module with its features and metered limits. This is the source the pricing page renders; quote these numbers rather than remembered ones.","tags":["Catalog"],"responses":{"200":{"description":"The current catalog.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PricingCatalog"}}}},"500":{"description":"The catalog could not be read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/widget-config":{"get":{"operationId":"getWidgetConfig","summary":"Public widget configuration for a project","description":"The non-sensitive configuration the embedded widget needs: brand colour, company name, welcome message, and which optional capabilities (assistant, attachments, voice, quick actions, order lookup, business hours) are switched on for the project. Accepts a project slug or a project UUID. No project secrets are exposed.","tags":["Catalog"],"parameters":[{"name":"project","in":"query","required":true,"description":"Project slug (as used in the embed snippet) or project UUID.","schema":{"type":"string","minLength":1,"maxLength":100},"example":"acme-shop"}],"responses":{"200":{"description":"The project’s public widget configuration.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WidgetConfig"}}}},"400":{"description":"The `project` parameter is missing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No project matches that slug or id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The database is unreachable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/chats":{"post":{"operationId":"startConversation","summary":"Start or resume a conversation","description":"Opens a conversation for a visitor of a project and returns its id plus the `resume_token` that authorises later calls about it. Pass `previous_token` to continue an existing conversation instead of creating a second one; pass `defer_create: true` to look for an existing conversation without creating one (the response is then `{ \"id\": null, \"deferred\": true }`). Identify the project by `project_slug` or `project_id`.","tags":["Conversations"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StartConversationRequest"}}}},"responses":{"200":{"description":"The conversation to write into, or a deferred lookup with no conversation yet.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Conversation"},{"$ref":"#/components/schemas/DeferredConversation"}]}}}},"400":{"description":"Neither `project_id` nor `project_slug` was given, or the project does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The database is unreachable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/messages":{"get":{"operationId":"listMessages","summary":"Read the messages of a conversation","description":"Returns the messages of one conversation in chronological order. An anonymous caller must pass the conversation’s `token`; an operator session reads the conversations of projects it belongs to. Internal system notes are omitted from the token-authorised view.","tags":["Conversations"],"parameters":[{"name":"chat_id","in":"query","required":true,"description":"Conversation id returned by startConversation.","schema":{"type":"string","format":"uuid"}},{"name":"token","in":"query","required":false,"description":"The conversation’s resume token. Required for anonymous callers.","schema":{"type":"string","minLength":1,"maxLength":64}},{"name":"lang","in":"query","required":false,"description":"Two- or three-letter language code. Cached translations for that language are attached to operator and assistant messages as `translation`; missing translations are simply absent.","schema":{"type":"string","pattern":"^[a-z]{2,3}$"},"example":"de"}],"responses":{"200":{"description":"The messages, oldest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Message"}}}}},"400":{"description":"`chat_id` is missing, or the resume token does not match the conversation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The database is unreachable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"sendCustomerMessage","summary":"Post a message to a conversation","description":"Adds a message to a conversation. Anonymous callers may only send `sender_type: \"customer\"`; the sender identity is taken from the conversation, never from the body. Operator and system messages require an operator session. When the project’s assistant is handling the conversation the response carries `assistant_pending: true` and the reply arrives asynchronously - poll listMessages for it. Repeating identical text within a few seconds returns the message already stored instead of duplicating it.","tags":["Conversations"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMessageRequest"}}}},"responses":{"200":{"description":"The stored message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}},"400":{"description":"The payload does not match the schema.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"An operator session is required for this sender type.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No conversation with that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many messages in this conversation; slow down and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The database is unreachable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/chats/rating":{"get":{"operationId":"getChatRating","summary":"Read the rating state of a conversation","description":"Tells whether a rating was requested for the current close of a conversation, the rating already given if there is one, and the operator who handled it. Authorised by the conversation’s resume token or by an operator session.","tags":["Feedback"],"parameters":[{"name":"chat_id","in":"query","required":true,"description":"Conversation id.","schema":{"type":"string","format":"uuid"}},{"name":"token","in":"query","required":false,"description":"The conversation’s resume token. Required for anonymous callers.","schema":{"type":"string","minLength":1,"maxLength":64}}],"responses":{"200":{"description":"Rating state for the current close.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RatingState"}}}},"400":{"description":"`chat_id` is not a valid conversation id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The caller is not authorised for this conversation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The database is unreachable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"submitChatRating","summary":"Rate a closed conversation","description":"Stores the customer’s rating of a closed conversation. One rating per close: reopening a conversation and closing it again asks for a new one. A comment is required for ratings of 3 or below.","tags":["Feedback"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitRatingRequest"}}}},"responses":{"200":{"description":"The stored rating.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Rating"}}}},"400":{"description":"Invalid payload, a missing comment for a low rating, or a conversation that cannot be rated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The caller is not authorised for this conversation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"This close was already rated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The database is unreachable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"schemas":{"Error":{"type":"object","description":"Error body returned by every endpoint under /api, including for unknown paths. `error` repeats `message` for older clients.","required":["error","code","message"],"properties":{"error":{"type":"string","description":"Human-readable message (same text as `message`).","examples":["Not found"]},"code":{"type":"string","description":"Stable machine-readable error code. Branch on this rather than on the message text.","examples":["not_found","bad_request","unauthorized","forbidden","rate_limited","server_error","service_unavailable"]},"message":{"type":"string","description":"Human-readable explanation of what went wrong."},"hint":{"type":"string","description":"What the caller can do about it."},"documentation_url":{"type":"string","format":"uri","description":"Documentation covering this endpoint."}}},"ServiceStatus":{"type":"object","description":"Result of the liveness probe.","required":["status","checks","timestamp"],"properties":{"status":{"type":"string","enum":["ok","degraded"],"description":"`degraded` means a dependency failed."},"checks":{"type":"object","description":"Per-dependency result; `ok` or the reason it failed.","properties":{"database":{"type":"string","examples":["ok"],"description":"Result of the database probe."}},"additionalProperties":{"type":"string"}},"latency_ms":{"type":["integer","null"],"description":"Round trip of the database probe, in milliseconds."},"timestamp":{"type":"string","format":"date-time","description":"When the probe ran."}}},"PricingModule":{"type":"object","description":"One purchasable module.","required":["module_key","name","is_free","price_monthly","price_yearly"],"properties":{"module_key":{"type":"string","description":"Stable identifier of the module.","examples":["ai_chat_integration"]},"name":{"type":"string","description":"Display name."},"description":{"type":["string","null"],"description":"What the module does."},"category":{"type":["string","null"],"description":"Grouping used on the pricing page."},"is_free":{"type":"boolean","description":"Whether the module is included at no cost."},"price_monthly":{"type":"number","description":"Monthly price in the catalog currency."},"price_yearly":{"type":"number","description":"Yearly price in the catalog currency."},"pro_price_monthly":{"type":"number","description":"Monthly price with at least one paid seat."},"pro_price_yearly":{"type":"number","description":"Yearly price with at least one paid seat."},"features":{"type":"array","items":{"type":"string"},"description":"What the module includes."},"usage_limit_per_month":{"type":["integer","null"],"description":"Included units per month for metered modules."},"overage_cost":{"type":["number","null"],"description":"Price of one unit beyond the included allowance."}}},"PricingCatalog":{"type":"object","description":"Seats, modules and the rules that price them.","required":["currency","free_seats","seat_price_monthly","seat_price_yearly","modules"],"properties":{"currency":{"type":"string","description":"ISO 4217 currency code of every price below.","examples":["EUR"]},"free_seats":{"type":"integer","description":"Operator seats included at no cost."},"seat_price_monthly":{"type":"number","description":"Price of an additional seat per month."},"seat_price_yearly":{"type":"number","description":"Price of an additional seat per year."},"module_discount_percent":{"type":"number","description":"Discount applied to modules on a paid plan."},"module_trial_days":{"type":"integer","description":"Trial length for a paid module; 0 means trials are off."},"modules":{"type":"array","items":{"$ref":"#/components/schemas/PricingModule"},"description":"Every active module."}}},"WidgetConfig":{"type":"object","description":"Public configuration of one project’s chat widget.","required":["project_id"],"properties":{"project_id":{"type":"string","format":"uuid","description":"The project the widget belongs to."},"color":{"type":["string","null"],"description":"Brand colour as a hex value, when set.","examples":["#0986cd"]},"company_name":{"type":["string","null"],"description":"Name shown in the widget header."},"welcome_message":{"type":["string","null"],"description":"First message shown in an empty conversation."},"llm_enabled":{"type":"boolean","description":"Whether the AI assistant answers first."},"attachments_enabled":{"type":"boolean","description":"Whether customers may attach files."},"quick_actions_enabled":{"type":"boolean","description":"Whether preset quick messages are offered."},"quick_messages":{"type":"array","items":{"type":"string"},"description":"The preset quick messages."},"outreach_auto_open":{"type":"boolean","description":"Whether the panel opens itself on an operator’s first message."},"order_check_enabled":{"type":"boolean","description":"Whether the project offers an order lookup."},"order_preview_enabled":{"type":"boolean","description":"Whether the empty chat shows the visitor’s active orders."},"business_hours_enabled":{"type":"boolean","description":"Whether business hours are published."},"business_hours":{"type":"object","description":"Per weekday opening hours, when published.","additionalProperties":{"type":"object","required":["enabled","start","end"],"properties":{"enabled":{"type":"boolean","description":"Whether the project is open that day."},"start":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Opening time, HH:MM."},"end":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Closing time, HH:MM."}}}},"voice_enabled":{"type":"boolean","description":"Whether voice messages are available."},"voice_input_enabled":{"type":"boolean","description":"Whether the microphone button is shown."},"tts_enabled":{"type":"boolean","description":"Whether messages can be played back as speech."}}},"StartConversationRequest":{"type":"object","description":"Identify the project, optionally the visitor, and optionally a conversation to continue.","anyOf":[{"required":["project_slug"]},{"required":["project_id"]}],"properties":{"project_slug":{"type":"string","description":"Project slug from the embed snippet.","examples":["acme-shop"]},"project_id":{"type":"string","format":"uuid","description":"Project UUID; wins over `project_slug`."},"customer_name":{"type":"string","maxLength":200,"description":"Name to show to operators."},"customer_email":{"type":"string","format":"email","description":"Email for follow-up, when the visitor gives one."},"customer_language":{"type":"string","description":"Language the visitor writes in, as a language code."},"source":{"type":"string","maxLength":64,"description":"Label of the property the conversation comes from."},"started_page_url":{"type":"string","format":"uri","maxLength":2000,"description":"Page the conversation started on."},"previous_token":{"type":"string","description":"Resume token of a conversation to continue instead of creating a new one."},"visitor_key":{"type":"string","pattern":"^[A-Za-z0-9_-]{8,64}$","description":"Presence key linking the conversation to the visitor session."},"defer_create":{"type":"boolean","description":"Look for an existing conversation without creating one."}}},"Conversation":{"type":"object","description":"A conversation the caller may write into.","required":["id","status","created_at"],"properties":{"id":{"type":"string","format":"uuid","description":"Conversation id."},"status":{"type":"string","enum":["active","waiting_operator","operator_handling","closed"],"description":"Who is handling the conversation right now."},"created_at":{"type":"string","format":"date-time","description":"When the conversation was created."},"customer_seen_at":{"type":["string","null"],"format":"date-time","description":"When the customer last read it; the unread baseline."},"resume_token":{"type":"string","description":"Credential for this conversation. Returned for a newly created conversation or to a verified owner; store it to read and continue the thread."}}},"DeferredConversation":{"type":"object","description":"Answer to a `defer_create` lookup that found no conversation.","required":["id","deferred"],"properties":{"id":{"type":"null","description":"Always null: nothing was created."},"deferred":{"type":"boolean","const":true,"description":"Always true: the lookup found no conversation."}}},"SendMessageRequest":{"type":"object","description":"A message to add to a conversation.","required":["chat_id","content","sender_type"],"properties":{"chat_id":{"type":"string","format":"uuid","description":"Conversation to append to."},"content":{"type":"string","minLength":1,"maxLength":10000,"description":"Message text."},"sender_type":{"type":"string","enum":["customer","operator","system"],"description":"Anonymous callers may only use `customer`; the other two need an operator session."},"request_attachment":{"type":"boolean","description":"Operator messages only: asks the widget to highlight its attachment button."}}},"Message":{"type":"object","description":"One message in a conversation.","required":["id","chat_id","content","sender_type","created_at"],"properties":{"id":{"type":"string","format":"uuid","description":"Message id."},"chat_id":{"type":"string","format":"uuid","description":"Conversation the message belongs to."},"content":{"type":"string","description":"Message text. For a voice message this is its transcript."},"sender_type":{"type":"string","enum":["customer","operator","llm","system"],"description":"Who wrote it: the customer, an operator, the AI assistant, or the system."},"sender_id":{"type":["string","null"],"format":"uuid","description":"Profile id of the sender, when there is one."},"created_at":{"type":"string","format":"date-time","description":"When the message was stored."},"assistant_pending":{"type":"boolean","description":"Present on the response to sendCustomerMessage: the assistant is composing a reply that will appear later."},"translation":{"type":"object","description":"Cached translation for the requested `lang`, when one exists.","properties":{"lang":{"type":"string","description":"Language the text was translated into."},"content":{"type":"string","description":"Translated text."}}}}},"RatingState":{"type":"object","description":"Whether this close of the conversation has been rated.","required":["status","rating_requested"],"properties":{"status":{"type":"string","description":"Conversation status; only a closed conversation can be rated."},"rating_requested":{"type":"boolean","description":"Whether a rating was requested for the current close."},"rating":{"anyOf":[{"$ref":"#/components/schemas/Rating"},{"type":"null"}],"description":"The rating already given, if any."},"operator":{"type":["object","null"],"description":"The operator who handled the conversation.","properties":{"name":{"type":["string","null"],"description":"Operator’s display name."},"avatar_url":{"type":["string","null"],"description":"Operator’s avatar image."}}}}},"SubmitRatingRequest":{"type":"object","description":"A customer’s rating of a closed conversation.","required":["chat_id","rating"],"properties":{"chat_id":{"type":"string","format":"uuid","description":"Conversation being rated."},"token":{"type":"string","minLength":1,"maxLength":64,"description":"Resume token; required for anonymous callers."},"rating":{"type":"integer","minimum":1,"maximum":5,"description":"Stars, 1 to 5."},"comment":{"type":"string","maxLength":1000,"description":"Required when `rating` is 3 or below."}}},"Rating":{"type":"object","description":"A stored rating.","required":["rating","created_at"],"properties":{"rating":{"type":"integer","minimum":1,"maximum":5,"description":"Stars, 1 to 5."},"comment":{"type":["string","null"],"description":"What the customer wrote, when they wrote anything."},"created_at":{"type":"string","format":"date-time","description":"When the rating was stored."}}}}},"security":[]}