# POST /v1/ui

> An AG-UI run (protocol 1.0), what the MorphScreen SDKs read.

## 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

An AG-UI run, which `connectAgent` sends for you; the model and the options go in `forwardedProps`:
```bash
curl -N https://api.morphscreen.com/v1/ui \
  -H "X-MorphScreen-Key: $MORPHSCREEN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"threadId": "t1", "runId": "r1", "tools": [], "context": [], "state": {},
       "messages": [{"id": "m1", "role": "user", "content": "Quiz me on photosynthesis"}],
       "forwardedProps": {"model": "deepseek/deepseek-v4.1-flash", "packs": ["interactive"]}}'
```

## Request

### RunAgentInput

AG-UI's RunAgentInput (protocol 1.0). Tools aren't supported yet: `tools` must be empty.

| Field | Type | Required | What it is |
|---|---|---|---|
| `threadId` | string |  |  |
| `runId` | string |  |  |
| `messages` | list of object |  |  |
| `tools` | list of any |  |  At most 0. |
| `state` | any |  |  |
| `context` | list of any |  |  |
| `forwardedProps` | object | yes | The model and the options. output: agui (AG-UI events, the default) or ops (the core's op stream). |

### forwardedProps

The model and the options. output: agui (AG-UI events, the default) or ops (the core's op stream).

| Field | Type | Required | What it is |
|---|---|---|---|
| `model` | string | yes |  |
| `packs` | list of string |  |  |
| `components` | list of object |  |  |
| `image_hosts` | list of string |  |  |
| `images` | list of object |  |  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` | `agui`, `ops` |  |  |
| `repair` | boolean |  |  |

### AppImage

One of your own pictures. The model shows it as ![alt](img:ID) 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 | <span class="nb">Server-sent</span> events: <span class="nb">AG-UI</span> events (output agui), or the core's ops (output ops). |
| 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 <span class="nb">Retry-After</span> 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](/docs/production/limits) says what to do about each.
