Skip to content

POST /chat/completions

2 min read

This endpoint speaks the OpenAI chat completions format. If your chatbot already uses an OpenAI client library, point the client’s base URL at Snoowl Answers, give it a secret key, and the assistant answers from your documents.

POST https://answers.snoowl.com/api/v1/chat/completions
Authorization: Bearer ansk_…
Content-Type: application/json

Accepts a secret key only. A website or preview key gets a 403.

from openai import OpenAI

client = OpenAI(base_url="https://answers.snoowl.com/api/v1", api_key="YOUR_ENDPOINT_KEY")
reply = client.chat.completions.create(
    model="snoowl-answers",  # any name works; the assistant is set by the key
    messages=[{"role": "user", "content": "How do I get a refund?"}],
)
print(reply.choices[0].message.content)
# Sources and status ride along in the response's "snoowl" field.
FieldTypeDescription
messagesarrayRequired. The conversation, oldest first. The last user message is the question. Earlier user and assistant messages are the conversation so far, of which the last six are used.
streambooleanOptional. true sends the answer as server-sent events. See Streaming.
userstringOptional. Your ID for this conversation, shown as the session in Conversations. Up to 80 characters are kept.
modelstringAccepted and ignored. The key decides which assistant answers.

A message’s content can be a string or a list of parts. From a list, only the text parts are read.

The format is OpenAI’s, but the assistant behind it isn’t a general chat model:

  • System messages are ignored, and so are tool and other roles. The assistant’s name, tone and guidance come from Assistant settings, so a caller can’t change the rules that keep answers to your documents.
  • Other parameters are ignored, including temperature, max_tokens, n, tools and response_format.
  • usage is always zero. Each answer’s real token count and cost are in Conversations.
  • Streaming isn’t faster. The whole answer is written and checked before the first chunk is sent. See Streaming.
  • Every answer is checked against your documents. A question the documents don’t cover gets your decline message as the reply, with snoowl.status set to declined.
{
  "id": "chatcmpl-3f2a9c1e5b7d40a8b1c2",
  "object": "chat.completion",
  "created": 1794211200,
  "model": "snoowl-answers",
  "choices": [
    {
      "index": 0,
      "finish_reason": "stop",
      "message": {
        "role": "assistant",
        "content": "You can ask for a refund within 30 days of paying, from Billing › Invoices."
      }
    }
  ],
  "usage": { "prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0 },
  "snoowl": {
    "status": "answered",
    "citations": [
      {
        "doc": "Refunds",
        "url": "https://help.example.com/billing/refunds",
        "quote": "You can ask for a full refund within 30 days of payment…"
      }
    ],
    "answer_id": "3f2a9c1e5b7d40a8b1c2"
  }
}

The standard fields are filled as OpenAI fills them. The snoowl field carries what OpenAI’s format has no place for:

FieldDescription
snoowl.statusanswered, or declined when your documents don’t cover the question
snoowl.citationsThe documents the answer used, in the same form as POST /answer
snoowl.answer_idThe answer’s ID, for POST /feedback. The id field is the same ID with chatcmpl- in front.

OpenAI client libraries keep fields they don’t know. In the Python SDK, read them with reply.model_extra["snoowl"]; in JavaScript, with reply.snoowl.

With "stream": true, the response is text/event-stream. Each event is a data: line holding a chat.completion.chunk:

  1. The first chunk has delta: {"role": "assistant", "content": ""}.
  2. The answer follows in pieces of about 40 characters, each as delta: {"content": "…"}.
  3. The last chunk has an empty delta, finish_reason: "stop" and the snoowl field.
  4. The stream ends with data: [DONE].
data: {"id": "chatcmpl-3f2a…", "object": "chat.completion.chunk", "created": 1794211200, "model": "snoowl-answers", "choices": [{"index": 0, "delta": {"role": "assistant", "content": ""}, "finish_reason": null}]}

data: {"id": "chatcmpl-3f2a…", "object": "chat.completion.chunk", "created": 1794211200, "model": "snoowl-answers", "choices": [{"index": 0, "delta": {"content": "You can ask for a refund within 30 days o"}, "finish_reason": null}]}

data: {"id": "chatcmpl-3f2a…", "object": "chat.completion.chunk", "created": 1794211200, "model": "snoowl-answers", "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}], "snoowl": {"status": "answered", "citations": […], "answer_id": "3f2a9c1e5b7d40a8b1c2"}}

data: [DONE]
from openai import OpenAI

client = OpenAI(base_url="https://answers.snoowl.com/api/v1", api_key="YOUR_ENDPOINT_KEY")
stream = client.chat.completions.create(
    model="snoowl-answers",
    messages=[{"role": "user", "content": "How do I get a refund?"}],
    stream=True,
)
for chunk in stream:
    print(chunk.choices[0].delta.content or "", end="")

The answer is complete before the first chunk is sent, because it’s checked against your documents as a whole. Streaming exists so that clients built for streaming work unchanged. It doesn’t make the first words arrive sooner. If something goes wrong, the error comes back as an ordinary JSON response with its status code, never partway through a stream.

Errors use the error format, which OpenAI SDKs raise as their usual exceptions. A request with no user message gets a 400 “Send at least one user message”. The key is checked first, so a bad key gets a 401 even when the body is wrong too.