HTTP API
POST /v1/chat/completions
A chat completion in our format, OpenAI's and OpenRouter's request and response.
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:
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 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.