Setup API
Every endpoint the setup and sync files call: authentication, bodies, answers, step ids, limits and errors. For writing your own agent or running setup from CI.
The setup file your coding agent runs is one client of this API. You need this page only if you are writing another one: your own agent, a script, a CI job. Everything here is what the setup file itself relies on, and the server holds every client to the same rules.
Base URL: https://rendel.ai/api/setup
This is not the SDK's API at api.rendel.ai (see REST API). The setup API writes what the console writes, so it runs beside the console and applies the console's own checks.
Authentication
Every endpoint except GET /file takes the session's setup token, and only in this header:
Authorization: Bearer rd_st_0123456789abcdef01234567A token is rd_st_ and 24 lowercase hex characters. It is in the file the console hands out, and nowhere else: Rendel keeps only its hash. A token in a query string is not read, so it never ends up in an access log.
| Lifetime | 24 hours from when the session was opened in the console |
| Scope | One app, these endpoints only. No conversations, users or secret keys |
| Sessions | One open session per app. Opening a new one in the console cancels the last |
| Files | Each file fetched mints a new token and invalidates the previous file's |
| Claim | The first POST /start owns the session. Every other endpoint answers 409 not_started until then |
| End | POST /complete, POST /fail, Stop in the console, going live (for a setup session), or the 24 hours. The token stops working at once |
There are two kinds of session. A setup session (rendel-setup.md) is refused on an app that has gone live. A sync session (rendel-sync.md) is not, but on a live app its writes become proposals: they answer 202 with proposed and wait for a person in the console. GET /context says which applies, as session.writes: apply or propose.
Limits
| Limit | Value |
|---|---|
| Requests per session | 120 a minute, counted on the server. Over it: 429 rate_limited with Retry-After |
| JSON body | 2 MB (413 too_large) |
| Logo | 2 MB; PNG, JPEG, WebP or SVG |
| Knowledge | 25 items per request; a text up to 200,000 characters |
| Actions | 50 items per request |
| Screens | 50 declared and 50 archived per request |
| Events | 50 per request; data up to 8,000 characters of JSON each |
Errors
Every error has one shape, the same as the SDK API's:
{ "error": { "code": "invalid_body", "message": "items.0.content: Required" } }message is a sentence a person or an agent can act on, and for a bad body it names the field. code is what to branch on.
| Status | Code | Meaning | What to do |
|---|---|---|---|
400 | invalid_json | The body is not JSON | Fix and resend |
400 | invalid_body | The body does not match the schema; unknown fields are refused by name | Fix the named field and resend |
400 | empty_file | POST /logo with a file of no bytes | Send the image itself |
401 | missing_token | No Authorization: Bearer rd_st_… header | Send the token |
401 | invalid_token | Not a token Rendel issued, or replaced by a newer file | Get a new file |
404 | unknown_code | GET /file: a code Rendel never issued | Copy the command again |
404 | app_not_found | The session's app was deleted | — |
409 | not_started | Called before POST /start | Call POST /start |
409 | already_claimed | Another agent started this session | Get a new file |
409 | app_live | A setup session on an app that has gone live | Use a sync session |
409 | draft_invalid | POST /logo while the appearance draft is not valid | PUT /appearance first |
410 | code_used, code_expired | GET /file: the code was used, or is over 15 minutes old | Get a new command |
410 | session_closed | The session is completed, failed, cancelled or expired | Get a new file |
413 | too_large | Body or logo over its limit | Send less per request |
415 | unsupported_type | A logo that is not PNG, JPEG, WebP or SVG | — |
422 | invalid_appearance, invalid_experience | Valid JSON the console would refuse too | Read message |
422 | — | /knowledge or /actions where no item was saved | Read each results[i].error |
429 | rate_limited | Over 120 requests a minute | Wait Retry-After seconds; batch events |
500 | internal | Ours. Nothing was changed by the request | Retry once |
The steps
The Overview panel has thirteen steps, in this order. Who closes a step decides what an agent's own report can do: for a server step, only something Rendel saw ticks it, and an agent saying done is recorded but changes nothing.
| id | Panel | Closed by |
|---|---|---|
waiting | Waiting for your agent | server: POST /start |
connected | Agent connected | server: POST /start |
discover | Reading your code | agent |
install | Installing the SDK | agent |
identity | Connecting your users | agent |
appearance | Applying your look | either |
experience | Defining the assistant | either |
knowledge | Filling the knowledge base | either; also ticked once a source is indexed |
screens | Connecting screens | either; also ticked once a build registers a screen |
actions | Connecting actions | either |
verify | Building and verifying | server: the app's first handshake |
test | Test conversation | server: a passing smoke test |
done | Ready | either; ticked by POST /complete |
Every write endpoint reports its own step: the first write to a step marks it started, and every write adds a line with what it did. A write step counts as finished once the agent moves on to a later step. So an agent needs POST /events only for the steps that happen in code — discover, install, identity, verify — and to mark a step skipped or failed.
GET /file
GET /file?code=<32 hex> — no token. This is the console's curl line:
curl -fsSL "https://rendel.ai/api/setup/file?code=<code>" -o rendel-setup.mdAnswers the file as text/markdown, with a newly minted token in it. The code works once and for 15 minutes; after that it answers 410. The console's Download button makes the same file.
POST /start
Claims the session. The panel moves from Waiting for your agent to Agent connected.
| Field | Required | What it is |
|---|---|---|
tool | Yes | claude_code, cursor, codex, windsurf, copilot, gemini, or your own name, up to 40 characters |
model | No | The model, up to 80 characters |
os | No | Up to 40 characters |
detected | No | A discovery report; see POST /events |
{ "ok": true, "session": { "id": "…", "kind": "setup", "status": "running", "expires_at": "…" }, "app": { "id": "…", "name": "…" }, "next": "GET /context" }Calling it again with the same tool, model and os is a restart and answers the same. A different agent gets 409 already_claimed.
GET /context
What the agent needs before touching the code. Read-only.
| Key | What it holds |
|---|---|
app | id, name, slug, platform_hint, live |
session | id, kind, expires_at, writes (apply or propose) |
profile | What the person entered at intake: links, description, is_live; brand_hints; the last detected report |
intake.knowledge | What Rendel already read from those links: title, kind, address, status and an excerpt each |
config | The current configuration, each item with its origin and external_key: appearance (draft and published), experience.persona, knowledge, actions (with needs_key, never a secret), declared screens, and the last manifest's action and route names |
keys | The test and live publishable keys |
sdk | The Flutter git URL and ref, the web SDK version and URL, ready-to-use snippets, and the docs address |
api | base: the address of this API |
steps | Every step, and the ones an agent may report |
facts | As in GET /status |
schemas | The JSON Schema of every request body below, generated from the same definitions the server validates with |
POST /events
The agent's own progress. One event, an array of up to 50, or { "events": [...], "detected": {...} }.
| Field | Required | What it is |
|---|---|---|
step | Yes | Any step id except waiting and connected |
status | Yes | started, progress, done, skipped, failed or waiting |
title | No | Up to 120 characters; the panel's line |
detail | No | Up to 2,000 characters, such as an assumption made |
data | No | Any JSON object, up to 8,000 characters |
detected, the discovery report, takes any of: platform, framework, router, http, auth, locales (up to 40), screens and apiCalls (counts), themeFile, darkTheme (boolean), fonts (up to 10), logoFile. It is merged into what was there and kept with the app.
{ "accepted": 2, "ids": [101, 102], "detected": true }PUT /appearance
The theme, into the draft. A setup token never publishes.
| Field | Required | What it is |
|---|---|---|
theme | Yes | The console's theme: surface, onSurface, accent, onAccent (all four together, opaque #rrggbb), surfaceRaised, surfaceHigh, surfaceSunk, radius, fontFamily, style and the rest. The whole schema is schemas.appearance in the context |
themeDark | No | The app's own dark theme: the same tokens, any subset. What it leaves out is derived |
copilot | No | launcherLabel (up to 24 characters), composerHint, attachIcon, voiceIcon, sendIcon |
reason | No | Where it came from, such as the theme file. Shown in the console beside the values |
The theme replaces the draft's, keeping a logo already uploaded. The assistant's name, welcome and suggestions are Experience's, not this endpoint's.
{ "saved": true, "changed": true, "draft": true, "warnings": [
{ "check": "Body text on the surface", "what": "prose and titles", "ratio": 3.9, "minimum": 4.5, "fix": { "token": "onSurface", "suggested": "#2B2B2B" } }
] }warnings lists every colour pair that fails its contrast minimum, light and dark (a dark one names a themeDark. token), with a value on the same hue that would pass where there is one. They are warnings, not refusals: the SDK deepens an illegible ink on the device either way. Apply the suggestions and send again.
POST /logo
Multipart form data, the file in a field named file:
curl -X POST "https://rendel.ai/api/setup/logo" \
-H "Authorization: Bearer $RENDEL_TOKEN" \
-F "file=@assets/images/logo.svg"PNG, JPEG, WebP or SVG, up to 2 MB. Stored where the console stores a logo, and set in the draft theme.
{ "saved": true, "draft": true, "logoAssetId": "…" }PUT /experience
The assistant, into the draft persona. A patch: fields left out keep their value.
| Field | Limit | What it is |
|---|---|---|
assistantName | 40 | One name, for the persona and the thread header |
tone | 4,000 | How it speaks |
instructions | 4,000 | What it helps with |
guardrails | 4,000 | One Always … or Never … per line |
topics | 20 × 120 | Topics it won't discuss |
languages | 12 | Language subtags, the default first |
welcomeTitle | 80 | The welcome screen's headline |
welcomeBody | 160 | The line under it |
suggestions | 4 × 60 | Openers on the welcome screen |
reason | 1,000 | Where it came from |
{ "saved": true, "draft": true, "personaVersion": 3, "changed": true }POST /knowledge
Up to 25 items, { "items": [...] }. Each is upserted by its external_key (up to 200 characters), so sending a key again updates that source.
| Item | Fields |
|---|---|
| Text | external_key, kind: "text", title (up to 160), content (up to 200,000), reason |
| Link | external_key, kind: "url", title (optional), uri (a public http or https address), reason |
| Archive | op: "archive", external_key, reason |
A link is fetched by Rendel's server, through the same guard as a link added in the console; the client sends only the address. Archiving takes a source out of service without deleting it. New and changed text is indexed right away.
{ "created": 2, "updated": 0, "unchanged": 1, "archived": 0, "failed": 0,
"results": [{ "external_key": "plans", "ok": true, "op": "created", "id": "…", "chunks": 3 }],
"note": "…" }One item failing does not fail the others. The answer is 422 only when none was saved.
POST /actions
Up to 50 HTTP actions, { "items": [...] }, upserted by external_key, and checked with the console's own rules.
| Field | Required | What it is |
|---|---|---|
external_key | Yes | Your name for it, such as GET /v1/orders/{id} |
name | Yes | snake_case, up to 64 characters: the tool name the model sees |
description | Yes | Up to 500 characters: when to call it |
method | Yes | GET, POST, PUT, PATCH or DELETE |
risk | No | read (default), write or destructive |
confirm_template | For a write | Up to 300 characters, {param} where a value goes |
url_template | Yes | The production https address, {{param.x}} where a value goes |
body_template | No | A JSON object as a string, for POST, PUT and PATCH |
params | No | Up to 20: name, description, required, example, type (string, number, boolean or array), items (a list's item type), enum (up to 50 allowed values) |
result_path, result_fields, result_limit | No | What of the answer the model sees; result_limit 1–50, default 5 |
timeout_ms | No | 1,000–30,000, default 10,000 |
auth | No | none (default) or user: sent with the signed-in person's own token |
auth_name | No | For auth: "user" when the token is not sent as Bearer: header:X-Auth-Token, query:token or cookie:session |
needs_key | No | { kind: "bearer" | "header" | "query", name?, note? }: the endpoint wants a shared key, on its own or (with auth: "user") beside the user's token |
reason | No | Why |
An archive item is { "op": "archive", "external_key": "…" }.
There is no field a secret could go in. An endpoint that wants a shared key says so in needs_key — the kind of key and where it goes, never the key — and is saved switched off until a person enters it with Add key, where it becomes a shared key for the endpoint's host.
- With
auth: "none", the shared key is the only credential, and the action may only read. - With
auth: "user", it is sent beside the signed-in person's token: the shape of an API that wants the app's key and the user's session together. The action may read or write. The key must not go where the token goes: abearerkey needs the token moved withauth_name(header:X-Auth-Token), and aheaderkey must not name the token's header. Such an item is refused with a reason.
Anything that changes data needs auth: "user". A key a person already entered on an action is kept when the action is sent again, and an action that already has a shared key is not put back to waiting for one.
{ "external_key": "GET /v1/pets", "name": "my_pets", "description": "The signed-in person's pets.",
"method": "GET", "url_template": "https://api.example.com/v1/pets",
"auth": "user", "needs_key": { "kind": "header", "name": "X-App-Key" } }{ "failed": 0, "needs_key": ["find_stores"],
"results": [{ "external_key": "GET /v1/stores", "ok": true, "name": "find_stores", "needs_key": true }] }POST /screens
Declares the screens the client registered in code with registerRoute. A declaration is a plan: the console shows these as planned until a build's manifest names them.
{
"screens": [{
"name": "order_detail",
"title": "Orders › Order",
"description": "One order: its items, delivery status and payment.",
"params": { "order_id": { "type": "string", "required": true, "fromActions": true } },
"file": "lib/rendel/routes.dart",
"deep_link": "/orders/:id"
}],
"archive": [{ "name": "old_settings", "reason": "Removed in 2.3" }]
}name is snake_case; title 1–60 characters; description 1–300; params as in registerRoute. Declared again by the same name, a screen is updated.
{ "planned": 2, "added": 1, "updated": 0, "unchanged": 1, "archived": 1, "unknown": [], "note": "…" }GET /status
What Rendel has seen, whatever anyone reported. The setup file polls it while it waits.
| Key | What it holds |
|---|---|
app_ready | true once the app has called in and sent its manifest |
knowledge_ready | true once no source is waiting for indexing or being indexed |
screens_missing | How many declared screens the latest manifest does not register, by count. Above 0, a registerRoute call did not run, or the app has not called in since it was added |
facts.handshakeAt | When the app first called in |
facts.manifest | The last manifest: sdkVersion, actionCount, routeCount, and their names |
facts.knowledge | sources, indexed, pending, indexing, failed, chunks |
facts.declaredScreens | Screens declared and not archived |
facts.httpActions | enabled, and needsKey |
facts.smoke | The last smoke test: passed, ok of total |
facts.liveAt | When the app went live, or null |
go_live | The four Go live checks, each { key, label, ok, detail } |
steps | Every step's state: pending, active, done, failed or skipped |
POST /smoke
No body. Rendel asks the copilot three to five questions drawn from the app's own configuration — its suggestions, its knowledge, a read action — through the test side, and judges each answer: grounded if it used knowledge, action-backed if it called an action. It passes when at least 80% are one or the other. Allow it up to 150 seconds.
{ "id": "…", "passed": true, "createdAt": "…", "platformFailure": false,
"results": [{ "question": "Where is my order?", "from": "suggestion", "answer": "…", "actions": ["get_order"], "grounded": false, "actionBacked": true, "ok": true }] }platformFailure is true when every answer was an error on Rendel's side rather than a weak answer. Nothing in the configuration would change that result, so change nothing and run it again later.
The last run is what the Go live check reads. The console's Run smoke test button runs the same test without a token.
GET /smoke
The latest run, in the shape POST /smoke answers with, for a client that lost that answer. It runs nothing. Before the first run it answers { "run": null }.
POST /complete
Ends the session. The token stops working.
| Field | Required | What it is |
|---|---|---|
summary | Yes | Markdown, up to 20,000 characters: what was done |
files_changed | No | Up to 500 paths |
human_todos | No | Up to 50: what only a person can do, such as entering a key |
{ "ok": true, "status": "completed", "token": "revoked" }POST /fail
The client cannot go on. The step it names — or the last one with an event — turns red on the panel with the reason, and the session ends.
| Field | Required | What it is |
|---|---|---|
reason | Yes | Up to 2,000 characters |
step | No | An agent step id |
{ "ok": true, "status": "failed", "token": "revoked" }