---
title: "OpenAI Chat Completions on a PopNow text model"
method: POST
path: "/v1/chat/completions"
tags: ["OpenAI Compatible"]
---

# OpenAI Chat Completions on a PopNow text model

`POST /v1/chat/completions`

## Request

- Base URL: `https://api.popnow.ai`
- URL: `https://api.popnow.ai/v1/chat/completions`
- Auth: HTTP bearer

Accepts an OpenAI Chat Completions request. `messages`, `tools`,
`tool_choice`, `parallel_tool_calls`, `response_format`, `max_tokens`
(or `max_completion_tokens`), `temperature`, `top_p`, `top_k`, `seed`
and `stop` map onto the model's public input fields of the same name
when the model declares them (each model page lists its own set);
`stream`,
`stream_options`, `user`, `metadata` and `store` are handled by this
layer. Pure sampling parameters the model does not declare
(`temperature`, `top_p`, `top_k`, `min_p`, `top_a`,
`frequency_penalty`, `presence_penalty`, `repetition_penalty`), `n: 1`,
an empty `stop`, `stop` on a model without stop sequences,
`reasoning_effort` on a model without reasoning levels, and a
`reasoning` object that cannot be translated are
ignored, and the response lists them in the `X-PopNow-Ignored-Params`
header. Values the model declares but cannot take as sent are changed
to the nearest accepted value and listed as `name=value` in the
`X-PopNow-Adjusted-Params` header: a sampling value outside the model's
range is clamped to the bound, a `stop` list longer than the model
allows keeps its first entries, a `reasoning_effort` level the model
does not offer becomes the nearest level it does (the lower one on a
tie), and `reasoning.effort` (or `reasoning.enabled: false`, as
`none`) becomes `reasoning_effort`. Any other parameter is forwarded to
the model's input schema and rejected with `400` when the model does
not declare it.

`stream: true` returns a Server-Sent Events stream of
`chat.completion.chunk` objects terminated by `data: [DONE]`. Every
chunk has a non-empty `choices` array and each choice ends with exactly
one chunk carrying `finish_reason`. Usage follows OpenAI: a stream
carries no `usage` unless `stream_options.include_usage` is `true`, in
which case one extra chunk with `choices: []` and the final `usage`
comes right before `[DONE]` and every other chunk has `usage: null`.
`stream: false` returns one `chat.completion` object that always has
`usage`. Billing: chat models are priced per token in credits (1 credit = $0.001), listed per 1M input and output
tokens in `credits` on `GET /api/v1/models` and `GET /v1/models`. When the request is accepted, the most it
can cost is held from your balance, computed from the input and `max_tokens` (the model's maximum output when
you omit it, so set `max_tokens` to keep holds small). When the reply ends you are charged its actual usage in
fractional credits, with no per-request minimum, and the rest of the hold is released. A balance below the
hold, or a key spend limit that would be exceeded, returns `402` before anything is sent to the model. A request
that fails or that the model refuses is refunded in full. The `X-PopNow-Task-Id` response header names the task;
`GET /api/v1/jobs/recordInfo` shows the final charge in `cost` (USD) and `exactCredits`. Message text is not
kept after the charge settles. Requests are forwarded to the model as sent and each model's own usage policy applies.

A non-streaming call that is still running after about 30 seconds is
kept open: the `200` status and the `X-PopNow-Keepalive: 1` header go out
first, followed by a newline every 15 seconds and then the JSON body.
A failure after that point is the usual `{"error": {...}}` object in the
body of that `200` response, so check for an `error` key when the
header is present. The same applies to `/v1/responses`, `/v1/messages`
(Anthropic error object) and `:generateContent` (Gemini error object,
whose `error.code` keeps the HTTP status the error would have had).

Errors a client can act on: an unknown `model` returns `404` with
`code: model_not_found`, the ID shape (`publisher/model/chat`) and the
closest callable chat model; a model that is not a chat model returns
`400` with `param: model`; a field or message part the model does not
accept (for example an `image_url` part on a text-only model) returns
`400` with `code: unsupported_parameter` and `param` pointing at it; a
content-policy refusal returns `400` with `code: content_rejected`.

## Headers

- `Idempotency-Key` string

## Request body

- ChatCompletionRequest
  - `model` string, required
  - `messages` object[], required
    - `role` string, required
    - `content` union
      - string, nullable
      - object[]
    - `tool_call_id` string
    - `tool_calls` object[]
    - `reasoning_content` string: Optional assistant reasoning text; availability and message roles follow the selected model input schema.
  - `stream` boolean
  - `max_tokens` integer
  - `max_completion_tokens` integer: Alias of `max_tokens`.
  - `temperature` number
  - `top_p` number
  - `top_k` integer: Sample only from the k most likely next tokens; only on models that declare it.
  - `seed` integer: Best-effort repeatable sampling; only on models that declare it.
  - `stop` union: Up to 4 stop sequences (some models accept only one); only on models that declare it.
    - string
    - string[]
  - `tools` object[]
  - `tool_choice` union
    - string
    - object
  - `parallel_tool_calls` boolean: Set false to allow at most one tool call per turn; only on models that declare it.
  - `response_format` object

## Response `200`

One `chat.completion` object, or an SSE stream when `stream` is true.

- ChatCompletion
  - `id` string, required
  - `object` 'chat.completion', required
  - `created` integer, required
  - `model` string, required
  - `choices` object[], required
    - `index` integer, required
    - `message` object, required
      - `role` string
      - `content` string, nullable
      - `refusal` string
      - `reasoning_content` string: Optional reasoning text returned by models that expose it. Streaming chunks use the same field in delta.
      - `tool_calls` object[]
    - `finish_reason` string, required
    - `logprobs` unknown
  - `usage` object
    - `prompt_tokens` integer
    - `completion_tokens` integer
    - `total_tokens` integer
    - `prompt_tokens_details` object
      - `cached_tokens` integer
      - `cache_creation_input_tokens` integer
      - `cache_read_input_tokens` integer
    - `completion_tokens_details` object
      - `reasoning_tokens` integer: Included in completion_tokens; do not add it again when calculating usage.

## Other responses

- `400`: Error in the OpenAI `{error:{message,type,param,code}}` shape. The HTTP status is the same one `/api/v1` would return. A content-policy refusal is HTTP 400 with `code: content_rejected`, so it can be told apart from a parameter error (`invalid_request` / `unsupported_parameter`, with `param` set when the error is about one field).
- `401`: Error in the OpenAI `{error:{message,type,param,code}}` shape. The HTTP status is the same one `/api/v1` would return. A content-policy refusal is HTTP 400 with `code: content_rejected`, so it can be told apart from a parameter error (`invalid_request` / `unsupported_parameter`, with `param` set when the error is about one field).
- `402`: Error in the OpenAI `{error:{message,type,param,code}}` shape. The HTTP status is the same one `/api/v1` would return. A content-policy refusal is HTTP 400 with `code: content_rejected`, so it can be told apart from a parameter error (`invalid_request` / `unsupported_parameter`, with `param` set when the error is about one field).
- `403`: Error in the OpenAI `{error:{message,type,param,code}}` shape. The HTTP status is the same one `/api/v1` would return. A content-policy refusal is HTTP 400 with `code: content_rejected`, so it can be told apart from a parameter error (`invalid_request` / `unsupported_parameter`, with `param` set when the error is about one field).
- `404`: Error in the OpenAI `{error:{message,type,param,code}}` shape. The HTTP status is the same one `/api/v1` would return. A content-policy refusal is HTTP 400 with `code: content_rejected`, so it can be told apart from a parameter error (`invalid_request` / `unsupported_parameter`, with `param` set when the error is about one field).
- `409`: Error in the OpenAI `{error:{message,type,param,code}}` shape. The HTTP status is the same one `/api/v1` would return. A content-policy refusal is HTTP 400 with `code: content_rejected`, so it can be told apart from a parameter error (`invalid_request` / `unsupported_parameter`, with `param` set when the error is about one field).
- `413`: Error in the OpenAI `{error:{message,type,param,code}}` shape. The HTTP status is the same one `/api/v1` would return. A content-policy refusal is HTTP 400 with `code: content_rejected`, so it can be told apart from a parameter error (`invalid_request` / `unsupported_parameter`, with `param` set when the error is about one field).
- `429`: Error in the OpenAI `{error:{message,type,param,code}}` shape. The HTTP status is the same one `/api/v1` would return. A content-policy refusal is HTTP 400 with `code: content_rejected`, so it can be told apart from a parameter error (`invalid_request` / `unsupported_parameter`, with `param` set when the error is about one field).
- `500`: Error in the OpenAI `{error:{message,type,param,code}}` shape. The HTTP status is the same one `/api/v1` would return. A content-policy refusal is HTTP 400 with `code: content_rejected`, so it can be told apart from a parameter error (`invalid_request` / `unsupported_parameter`, with `param` set when the error is about one field).
- `503`: Error in the OpenAI `{error:{message,type,param,code}}` shape. The HTTP status is the same one `/api/v1` would return. A content-policy refusal is HTTP 400 with `code: content_rejected`, so it can be told apart from a parameter error (`invalid_request` / `unsupported_parameter`, with `param` set when the error is about one field).

---

[API](https://skmtc.dev/popnow/apis/popnow-api.md) · [All operations](https://skmtc.dev/popnow/apis/popnow-api/llms.txt) · [OpenAPI document](https://skmtc.dev/popnow/apis/popnow-api/revisions/fede911a6e9b?raw)
