HTTP API

POST /v1/chat/completions

A chat completion in our format, OpenAI's and OpenRouter's request and response.

View as Markdown

Keys

Your MorphScreen key in X-MorphScreen-Key: the call counts against your plan. Your own OpenRouter key in Authorization: Bearer sk-or-... when you want a model other than the one in your plan: it runs on that key, and goes to OpenRouter and nowhere else. Leave it out to use the model in your plan.

Example

Stream an answer on the model included in your plan:

Terminal
curl https://api.morphscreen.com/v1/chat/completions \
  -H "X-MorphScreen-Key: $MORPHSCREEN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "deepseek/deepseek-v4.1-flash", "stream": true,
       "messages": [{"role": "user", "content": "Plan a weekend in Lisbon"}],
       "morphscreen": {"packs": ["data"]}}'

Any other model on OpenRouter runs on your own key, sent as Authorization: Bearer sk-or-.... The stream is OpenAI's: chat.completion.chunk events whose delta.content is the answer in our format, then a last chunk with finish_reason and a morphscreen field holding the core's verdict (and the repaired text, when a repair was needed), then data: [DONE].

Request

ChatCompletionRequest

OpenAI's chat completion request. Every field not listed here (temperature, max_tokens, models, provider, reasoning...) goes to the model as you set it. DeepSeek V4 Flash answers with reasoning off unless you set reasoning.

Field Type Required What it is
model string yes An OpenRouter model name, like deepseek/deepseek-v4.1-flash.
messages list of object yes
stream boolean Default false.
stream_options object
n 1
morphscreen object output: text (the answer in our format, the default) or html (drawn by the core; not streamed).

Message

A chat message, as OpenAI and OpenRouter take it.

Field Type Required What it is
role system, developer, user, assistant, tool yes
content string or array or null

stream_options

Field Type Required What it is
include_usage boolean

Options

output: text (the answer in our format, the default) or html (drawn by the core; not streamed).

Field Type Required What it is
packs list of string The core's packs to add: data, commerce, scheduling, media, docs, inputs, device, interactive, or all. Left out, every pack ("all"); [] for the core alone, a prompt about half as long. At most 16.
components list of object Your own packs, in the shape of spec/packs/*.json; 256 KB at most. At most 16.
image_hosts list of string Hosts the HTML's pictures may load from, besides the app's images' hosts and its photo provider's. At most 16.
images list of object Your own pictures, which the model shows by id. At most 200.
photos one of the shapes below The app's own photo key, for one provider: with it the model may ask for photos, searched there on the app's quota, each loading from the provider's own address with its credit. Without one (or false) the model isn't asked for photos.
output string What comes back; each endpoint has its own choices.
repair boolean Repair an answer with errors, and give a blank one a fresh try. Default true.

AppImage

One of your own pictures. The model shows it as alt and the API puts its URL and credit in; its host is allowed with it.

Field Type Required What it is
id string yes Letters, digits, '-', '_' or '.'.
url string yes Where it loads from, http(s).
alt string What it shows, told to the model.
credit string Shown under it, as written (Markdown links allowed).

Responses

Status What comes back
200 The answer whole, or with stream true as server-sent events: chat.completion.chunk after chunk, the last with finish_reason and morphscreen (and the repaired text when a repair was needed), a usage chunk when asked, then data: [DONE].
400 The request can't be run as written; model_needs_your_key: a model other than the included one, asked for without your own OpenRouter key.
401 No key, or one turned down: your MorphScreen key (morphscreen_key_required, morphscreen_key_invalid) or your OpenRouter key (openrouter_key_required, openrouter_key_invalid).
402 This month's calls on your plan are used (plan_quota_reached), a paid plan's month and as many again as extra (extra_limit_reached), the free plan's calls with the model included (included_calls_used, or included_calls_paused while our credit is low), or the OpenRouter account is out of credit (openrouter_out_of_credit).
403 The account is suspended (account_suspended), or the model's moderation refused the request (model_refused).
404 No such model.
413 The body is over 1 MB (payload_too_large), or a call with the model included has more than about 8,000 tokens of conversation (conversation_too_long).
429 A limit reached, with Retry-After saying how long to wait: requests a minute (rate_limited), answers at once (too_many_streams), the free tier's day (daily_limit_reached), renders this month (render_limit_reached), or OpenRouter's own (openrouter_rate_limited).
502 The model failed or couldn't be reached, after the retries.
503 Accounts can't be read just now; try again.
504 The model took too long to start.

Errors come back as {"error": {"code", "message", "type", "metadata": {"error_type", "reason"}}}: type is the reason to act on (rate_limited, plan_quota_reached...). Limits and errors says what to do about each.