API referenceChat completions

Chat completions

POST https://app.syntic.ai/v1/chat/completions

Send a conversation and get back the assistant’s next message. The reply arrives as a stream of server-sent events unless the request sets "stream": false. When you supply tools, the reply may instead ask your code to run one or more functions.

Request headers

HeaderRequiredValue
AuthorizationYesBearer <key>. See Authentication.
Content-TypeYesapplication/json
OriginOnly for keys with an origin allowlistThe calling site’s origin. Browsers set it automatically.

Request body

FieldTypeRequiredDetails
messagesarray of message objectsYesThe conversation so far, oldest first. 1 to 500 items. An empty or missing array returns 400 messages_required.
modelstringNoA Syntic model name, for example "Amara S1.2" or "Amara Quick". Names must match exactly, including spaces and capitals. A missing name uses the default model, Amara S1.2. Today an unknown name does too; not live yet — on develop: an unknown name returns 404 model_not_found. See Models overview.
streambooleanNoDefaults to true. Set false to get a single JSON object.
max_tokensintegerNoThe most tokens the reply may contain. When the cap is reached, finish_reason is length.
toolsarray of tool objectsNoFunctions the model may ask you to call.
tool_choicestring or objectNo"auto" lets the model decide, "none" forbids tool calls, and {"type": "function", "function": {"name": "..."}} asks for a specific function.

Sampling settings have no effect. temperature, top_p, n, presence_penalty and frequency_penalty are accepted so existing code does not break, but they are removed before the request reaches the model. Every request returns exactly one choice.

Other fields are ignored. In particular, response_format, seed, stop, logprobs and user are not passed on.

Message object

FieldTypeUsed onDescription
rolestringallsystem, user, assistant or tool.
contentstringallThe text of the message. May be empty or null on an assistant message that only carries tool_calls.
tool_callsarrayassistantThe calls the model asked for in an earlier reply. Return them unchanged in the next request.
tool_call_idstringtoolThe id of the tool call this result answers.

Put instructions in a single system message at the start of the array. The API places a short Syntic identity instruction ahead of it; your text follows that instruction unchanged. See What the API adds to your request.

Tool object

FieldTypeDescription
typestringAlways "function".
function.namestringThe name the model uses when it calls the function. Letters, digits, underscores and hyphens are the safe choice.
function.descriptionstringA plain-language explanation the model reads to decide when to call it.
function.parametersobjectA JSON Schema object describing the arguments.

Examples

A single JSON reply

curl https://app.syntic.ai/v1/chat/completions \
  -H "Authorization: Bearer $SYNTIC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "Amara S1.2",
    "stream": false,
    "max_tokens": 300,
    "messages": [
      { "role": "system", "content": "You write product descriptions for a garden centre." },
      { "role": "user", "content": "A frost-hardy lavender in a 2-litre pot." }
    ]
  }'

A streamed reply

Leave stream out (or set it to true) and read the body as it arrives. curl -N turns off output buffering so you see each event as it lands:

curl -N https://app.syntic.ai/v1/chat/completions \
  -H "Authorization: Bearer $SYNTIC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "Amara Quick",
    "messages": [{ "role": "user", "content": "Give me three names for a bakery van." }]
  }'

Parsing code for Python and JavaScript is on Stream format.

A tool call

Offer the model a function. If it decides to use it, the reply carries tool_calls instead of text:

curl https://app.syntic.ai/v1/chat/completions \
  -H "Authorization: Bearer $SYNTIC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "Amara S1.2",
    "stream": false,
    "tools": [{
      "type": "function",
      "function": {
        "name": "check_stock",
        "description": "Look up how many units of a product are in a given warehouse.",
        "parameters": {
          "type": "object",
          "properties": {
            "sku": { "type": "string", "description": "Product code, e.g. LAV-2L" },
            "warehouse": { "type": "string", "enum": ["leeds", "bristol"] }
          },
          "required": ["sku", "warehouse"]
        }
      }
    }],
    "messages": [{ "role": "user", "content": "Do we have LAV-2L in Leeds?" }]
  }'

A typical reply:

{
  "id": "chatcmpl-...",
  "object": "chat.completion",
  "created": 1767225600,
  "model": "...",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "",
        "tool_calls": [
          {
            "id": "tc_01",
            "type": "function",
            "function": { "name": "check_stock", "arguments": "{\"sku\":\"LAV-2L\",\"warehouse\":\"leeds\"}" }
          }
        ]
      },
      "finish_reason": "tool_calls"
    }
  ],
  "usage": { "prompt_tokens": 142, "completion_tokens": 27, "total_tokens": 169 }
}

Run the function yourself, then send the whole conversation back with the assistant message (unchanged) and one tool message per call:

{
  "model": "Amara S1.2",
  "stream": false,
  "tools": [ "...same tools as before..." ],
  "messages": [
    { "role": "user", "content": "Do we have LAV-2L in Leeds?" },
    { "role": "assistant", "content": "", "tool_calls": [ "...exactly as received..." ] },
    { "role": "tool", "tool_call_id": "tc_01", "content": "{\"units\": 38}" }
  ]
}

arguments is a JSON string, not an object; parse it before use and be ready for it to be invalid. The format of id is not fixed, so treat it as an opaque string. A complete loop is in Write a tool loop and Tool calling.

Response: stream: false

200 OK, Content-Type: application/json

{
  "id": "chatcmpl-...",
  "object": "chat.completion",
  "created": 1767225600,
  "model": "...",
  "choices": [
    {
      "index": 0,
      "message": { "role": "assistant", "content": "Bring year-round fragrance to borders and patios..." },
      "finish_reason": "stop"
    }
  ],
  "usage": { "prompt_tokens": 58, "completion_tokens": 91, "total_tokens": 149 }
}
FieldTypeDescription
idstringIdentifier of the completion. Use x-syntic-request-id when contacting support.
objectstringAlways chat.completion.
createdintegerUnix time in seconds.
modelstringThe model identifier reported for this completion. Do not use it to detect a model fallback; read the x-syntic-model-downgraded header instead.
choicesarrayAlways one item.
choices[].indexintegerAlways 0.
choices[].message.rolestringAlways assistant.
choices[].message.contentstring or nullThe generated text. Empty or null when the reply only holds tool calls.
choices[].message.tool_callsarrayPresent when the model asks you to run functions. Each item has id, type: "function" and function with name and arguments (a JSON string).
choices[].finish_reasonstringstop (the model finished), length (hit max_tokens or the context window) or tool_calls (waiting for tool results).
usage.prompt_tokensintegerInput tokens, including the Syntic identity instruction.
usage.completion_tokensintegerOutput tokens.
usage.total_tokensintegerThe sum of the two.

When part of the prompt is served from cache, usage can also include a cached_tokens count. Cached input is billed at the lower cached rate shown on Pricing.

Response headers

HeaderDescription
x-syntic-request-idUnique id of the request.
x-syntic-billing-sourcesubscription or api_credit.
x-syntic-model-downgradedPresent only when the requested model is not included in your plan; holds the name of the model that answered.

Response: streamed

200 OK, Content-Type: text/event-stream, with the same x-syntic-* headers.

Each event is a data: line holding a chat.completion.chunk object, followed by a blank line. Text arrives in choices[0].delta.content; tool calls arrive in pieces in choices[0].delta.tool_calls. A chunk near the end carries usage, and the stream closes with data: [DONE]. If something fails after the stream has started, a final event {"error": "stream_error", "detail": "..."} is sent before the connection closes.

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1767225600,"model":"...","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1767225600,"model":"...","choices":[{"index":0,"delta":{"content":"Crumb & Co"},"finish_reason":null}]}

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1767225600,"model":"...","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":61,"completion_tokens":24,"total_tokens":85}}

data: [DONE]

The full event reference, including how to assemble tool calls, is on Stream format.

Model selection and fallback

You sendModel that answersHeader
No modelDefault model (Amara S1.2)none
A name that does not existDefault modelnone
A model your plan includesThat modelnone
A model your plan does not includeDefault modelx-syntic-model-downgraded
A model that needs standard data retention, from a zero-data-retention organizationNone400 model_unavailable

Not live yet — on develop: the second and fourth rows change. A name that does not exist returns 404 model_not_found, and a model your plan does not include returns 403 model_not_in_plan; the x-syntic-model-downgraded header is retired. See Errors.

Until then an unknown name silently falls back, so check the spelling of model names carefully. The list of names, prices and plan requirements is on Models overview.

Billing

Each request is metered by its usage. Tokens come first from your organization’s plan allowance; once that is used up they are charged to the usage-credit wallet at the model’s retail rate, if credits are turned on. The x-syntic-billing-source header tells you which applied.

If you close a streamed connection early, the tokens already generated are still billed. When the final usage chunk never arrived, the count is estimated from the text that was sent.

See Usage & cost and Pricing.

Errors

Every error uses the error envelope: a type fixed by the HTTP status, a machine-readable code, a message, param and request_id.

{
  "error": {
    "type": "invalid_request_error",
    "code": "too_many_messages",
    "message": "The request has too many messages.",
    "param": null,
    "request_id": "req_6f1c2a9e0b7d4c3a8e5f1b2d",
    "max_messages": 500
  }
}
Statustypecode values
400invalid_request_errorinvalid_json, messages_required, invalid_request, context_length_exceeded, model_unavailable (provisional, not in the public catalog)
401authentication_errorunauthenticated, invalid_api_key, revoked_api_key, expired_api_key, invalid_token, token_revoked
402insufficient_quotaquota_exhausted
403permission_errorforbidden, public_key_rejected, origin_not_allowed, insufficient_scope, org_membership_required, account_disabled, org_unavailable, plan_required
404not_found_errormodel_not_found (on develop, not live yet)
405invalid_request_errormethod_not_allowed, endpoint_not_supported
413invalid_request_errorpayload_too_large, too_many_messages
429rate_limit_errorrate_limited, model_busy, capacity_busy
500api_errorinternal_error, server_misconfigured
502api_errorupstream_error, upstream_unavailable; stream_error inside a stream
503service_unavailable_errorauthorization_unavailable, usage_check_unavailable, plan_check_unavailable, capacity_busy, service_unavailable
504timeout_errorupstream_timeout

Each code has its own page in the error code index; the full rules are on Error codes.

Limits

LimitValue
Request bodyAbout 2 MB
Messages per request500
Context window262,144 tokens (prompt plus reply)
Requests per minute600 per API key

More on Limits.

Planned additions

⚠️
Preview — this feature is being built; the design below is the spec. Everything above describes the endpoint as it works today. The fields below are the agreed design for upcoming releases; each links to its own page, which carries the full specification. Until a field ships it is ignored, exactly as described in Request body.

Chat completions stays the stable public surface. New capabilities are added to this same request body rather than to new endpoints, and every new field will be validated: it is either honoured or rejected with a clear 400 naming the field, never silently dropped.

Field or changePurposeSpec
stop, user, metadataStop strings, end-user id, your own tagsRequest parameters
temperature, top_p, top_k honoured per modelSampling, with 400 unsupported_parameter where a model cannot honour themSampling
Fixed finish_reason listAdds stop_sequence, refusal, content_filter, pauseStop reasons
response_formatGuaranteed JSON, optionally matching a schemaJSON output
image_url / image_file content partsImage inputVision
file content partsPDF and document inputPDF and documents
file_id referencesReuse uploaded filesFiles API
citations on documents, message.annotationsAnswers that point to their sourcesCitations
prompt_cache_key, usage.prompt_tokens_details.cached_tokensControl and report prompt cachingPrompt caching
stream_options.include_usage, keep-alives, error envelopeStreaming polishStreaming
reasoningStep-by-step thinking with an effort or budgetReasoning
context_managementServer-side clearing and compactionContext management
parallel_tool_calls, function.strictTool-call controlParallel tool calls, Strict tool arguments
Built-in tools in tools (syntic_web_search, …)Tools that Syntic runs for youWeb search
mcp_serversConnect remote MCP serversMCP connector
POST /v1/chat/completions/count_tokensCount input tokens without running the modelToken counting
Error envelope {error:{type, code, message, param}, request_id}One error shape across all endpointsError format

A second request shape built on typed content blocks is specified on Messages. Both shapes share one engine, billing and limits.

Was this page helpful?