> For the complete documentation index, see [llms.txt](https://docs.frayme.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.frayme.ai/api-reference/compose.md).

# POST /v1/compose

Compose a validated json-render UI spec from a natural-language prompt — streamed live as SSE by default, or returned as a single JSON envelope with `stream: false`.

```
POST https://api.frayme.ai/v1/compose
Authorization: Bearer fr_live_…
Content-Type: application/json
```

The request body is `snake_case` and **strict**: any unknown field is rejected with `400 BAD_REQUEST`.

## Request fields

| Field               | Type    | Required | Limits / values                                                                                                                                                                                                            |
| ------------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `prompt`            | string  | yes      | 1–4,000 chars. What the UI should be.                                                                                                                                                                                      |
| `stream`            | boolean | no       | Default `true` (SSE). `false` returns a buffered envelope.                                                                                                                                                                 |
| `ui_type`           | enum    | no       | Closest archetype, steers layout: `form` · `dashboard` · `pricing_table` · `list_table` · `detail_view` · `settings_panel` · `wizard` · `confirmation` · `empty_state` · `calendar` · `board` · `map` · `editor` · `other` |
| `mode`              | enum    | no       | `create` (default) · `edit` (pair with `prior_spec`) · `continue_journey` (pair with `action_context`)                                                                                                                     |
| `data`              | object  | no       | ≤ 24,000 chars serialized. Display facts the UI must render **verbatim** — names, prices, rows.                                                                                                                            |
| `actions`           | array   | no       | ≤ 20 declared actions — see [action fields](#action-fields). Frayme guarantees each `required` action is wired to a control.                                                                                               |
| `action_policy`     | enum    | no       | `open` (default) — model-authored actions beyond your contract stay live · `declared_only` — the server strips every non-builtin action you didn't declare.                                                                |
| `prior_spec`        | object  | no       | ≤ 48,000 chars serialized. Your current spec; the server returns a minimal patch (edit mode). Validated before use.                                                                                                        |
| `action_context`    | object  | no       | ≤ 16,000 chars serialized. What the user just did — see [action\_context fields](#action_context-fields). Pair with `mode: "continue_journey"`.                                                                            |
| `custom_components` | array   | no       | ≤ 20 BYOC manifests (your plan's limit may be lower). Compiled manifests share a 12,000-char per-request budget.                                                                                                           |
| `max_operations`    | integer | no       | 1–200. Caps the number of streamed operations.                                                                                                                                                                             |
| `context`           | object  | no       | `theme` (≤ 40 chars) and `framework_hint` (≤ 60 chars) hints.                                                                                                                                                              |
| `metadata`          | object  | no       | ≤ 24,000 chars serialized. Free-form; recorded with the generation for your own correlation. Not shown to the model.                                                                                                       |

### Action fields

Each entry in `actions` declares one action your agent can handle:

| Field         | Type    | Limits / values                                                                                                                                                             |
| ------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`        | string  | 1–60 chars. The bound action name, e.g. `"approveRefund"`.                                                                                                                  |
| `params`      | object  | JSON Schema (object) for the action's params; keys bind to live UI state.                                                                                                   |
| `role`        | string  | ≤ 40 chars. Hint for control selection, e.g. `"approve"`.                                                                                                                   |
| `required`    | boolean | If `true`, a control bound to this action is guaranteed present.                                                                                                            |
| `description` | string  | ≤ 160 chars. What firing this action means.                                                                                                                                 |
| `kind`        | enum    | How the server wires it: `agent` (default — event routes back to you) · `recompose` (re-generates with a fixed `prompt`) · `host` (posts to a `channel`) · `false` (inert). |
| `prompt`      | string  | ≤ 4,000 chars. `recompose` only — the fixed prompt used when fired.                                                                                                         |
| `channel`     | string  | ≤ 120 chars. `host` only — the postMessage channel name.                                                                                                                    |

### action\_context fields

| Field           | Type   | Limits                                           |
| --------------- | ------ | ------------------------------------------------ |
| `action`        | string | required, 1–60 chars — the action the user fired |
| `event`         | string | ≤ 40 chars — the canonical event verb            |
| `params`        | object | resolved param values at fire time               |
| `state`         | object | full live state snapshot                         |
| `element_id`    | string | ≤ 120 chars                                      |
| `generation_id` | string | ≤ 120 chars — the generation the user acted on   |

## Streaming response (SSE)

With `stream: true` (the default) the response is `text/event-stream`. Every frame carries both an SSE `event:` name and a matching `type` field.

| Order | Event                     | Payload                                                                                                                 | Meaning                                                                                                                                                      |
| ----- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 1     | `compose.started`         | `generation_id`, `model`                                                                                                | Composition began.                                                                                                                                           |
| 2     | `op` × N                  | `op`, `path`, `value?`, `from?`                                                                                         | One json-render operation. **Provisional** — safe to render live, but not final.                                                                             |
| 2b    | `compose.restarted` × 0–2 | `generation_id`, `model` (next attempt), `reason.code`                                                                  | The previous attempt failed validation and is being retried on a stronger model. **Discard all rendered state** — subsequent `op` frames build a fresh spec. |
| 3     | `compose.completed`       | `generation_id`, `model`, `operation_count`, `usage`, `validated: true`, `interactions`, `components_used`, `replayed?` | The **only** finalizer. The spec passed validation; this is the billing moment.                                                                              |
| —     | `error`                   | `error.code`, `error.message`                                                                                           | In-band terminal failure. Never billed. Codes match the [error taxonomy](/api-reference/errors.md).                                                          |

A `: ping` comment is sent every 15 seconds to keep proxies from idling the connection.

Treat every rendered op as provisional until `compose.completed` arrives. The `@frayme/runtime` renderer and the `@frayme/api` stream handle restart-discard for you; hand-rolled SSE clients must implement it (see [troubleshooting](/resources/troubleshooting.md)).

## Non-streaming response

With `stream: false`, one envelope after composition finishes:

```json
{
  "success": true,
  "data": {
    "generation_id": "gen_5f0c…",
    "spec": { "state": {}, "root": "card", "elements": { "…": {} } },
    "model": "…",
    "operation_count": 42,
    "validated": true,
    "usage": { "input_tokens": 1874, "output_tokens": 912 },
    "interactions": [
      { "element": "submitBtn", "event": "commit", "action": "approveRefund", "kind": "agent", "params": null }
    ],
    "components_used": ["Button", "Card", "TextField"]
  }
}
```

`interactions` is the "what can this UI do" summary a host agent reads; `components_used` lists the unique component types in the spec. Both are omitted on an [idempotent replay](/api-reference/idempotency.md), which instead carries `"replayed": true`. `model` is an opaque identifier — log it, never branch on it.

## Examples

{% tabs %}
{% tab title="curl (SSE)" %}

```bash
curl -N https://api.frayme.ai/v1/compose \
  -H "Authorization: Bearer $FRAYME_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "A refund approval card for order #4821: amount, reason, approve/deny",
    "ui_type": "confirmation",
    "data": { "order": { "id": "#4821", "amount": "$62.00" } },
    "actions": [
      { "name": "approveRefund", "role": "approve", "required": true },
      { "name": "denyRefund", "role": "deny" }
    ]
  }'
```

{% endtab %}

{% tab title="curl (buffered)" %}

```bash
curl https://api.frayme.ai/v1/compose \
  -H "Authorization: Bearer $FRAYME_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: refund-4821-v1" \
  -d '{ "prompt": "A refund approval card for order #4821", "stream": false }'
```

{% endtab %}

{% tab title="TypeScript (stream)" %}

```ts
import Frayme from '@frayme/api';

const frayme = new Frayme({ apiKey: process.env.FRAYME_API_KEY });

const stream = frayme.compose.stream({
  prompt: 'A refund approval card for order #4821: amount, reason, approve/deny',
  ui_type: 'confirmation',
  actions: [{ name: 'approveRefund', role: 'approve', required: true }],
});

stream.on('op', (op, snapshot) => render(snapshot)); // progressive snapshot
stream.on('restarted', () => clearRendered());       // discard on restart

const { spec, generationId } = await stream.finalSpec(); // resolves at compose.completed
```

{% endtab %}

{% tab title="TypeScript (buffered)" %}

```ts
import Frayme from '@frayme/api';

const frayme = new Frayme({ apiKey: process.env.FRAYME_API_KEY });

const result = await frayme.compose.create({
  prompt: 'A refund approval card for order #4821',
});

console.log(result.generation_id, result.components_used);
// result.spec is a validated json-render spec — render it with @frayme/runtime
```

{% endtab %}
{% endtabs %}

## Timing

Most composes stream their first operations within a few seconds and complete shortly after; the worst case (a retry on the stronger model) can take a few minutes. Prefer streaming for interactive surfaces, and give non-streaming calls a generous timeout — the SDK and the `: ping` keepalives handle long tails for you.

## Related

* [Errors](/api-reference/errors.md) — every code this endpoint can return
* [Idempotency](/api-reference/idempotency.md) — `Idempotency-Key` semantics
* [The spec](/core-concepts/the-spec.md) — anatomy of what comes back


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.frayme.ai/api-reference/compose.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
