Reference

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_0123456789abcdef01234567

A 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.

Lifetime24 hours from when the session was opened in the console
ScopeOne app, these endpoints only. No conversations, users or secret keys
SessionsOne open session per app. Opening a new one in the console cancels the last
FilesEach file fetched mints a new token and invalidates the previous file's
ClaimThe first POST /start owns the session. Every other endpoint answers 409 not_started until then
EndPOST /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

LimitValue
Requests per session120 a minute, counted on the server. Over it: 429 rate_limited with Retry-After
JSON body2 MB (413 too_large)
Logo2 MB; PNG, JPEG, WebP or SVG
Knowledge25 items per request; a text up to 200,000 characters
Actions50 items per request
Screens50 declared and 50 archived per request
Events50 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.

StatusCodeMeaningWhat to do
400invalid_jsonThe body is not JSONFix and resend
400invalid_bodyThe body does not match the schema; unknown fields are refused by nameFix the named field and resend
400empty_filePOST /logo with a file of no bytesSend the image itself
401missing_tokenNo Authorization: Bearer rd_st_… headerSend the token
401invalid_tokenNot a token Rendel issued, or replaced by a newer fileGet a new file
404unknown_codeGET /file: a code Rendel never issuedCopy the command again
404app_not_foundThe session's app was deleted—
409not_startedCalled before POST /startCall POST /start
409already_claimedAnother agent started this sessionGet a new file
409app_liveA setup session on an app that has gone liveUse a sync session
409draft_invalidPOST /logo while the appearance draft is not validPUT /appearance first
410code_used, code_expiredGET /file: the code was used, or is over 15 minutes oldGet a new command
410session_closedThe session is completed, failed, cancelled or expiredGet a new file
413too_largeBody or logo over its limitSend less per request
415unsupported_typeA logo that is not PNG, JPEG, WebP or SVG—
422invalid_appearance, invalid_experienceValid JSON the console would refuse tooRead message
422—/knowledge or /actions where no item was savedRead each results[i].error
429rate_limitedOver 120 requests a minuteWait Retry-After seconds; batch events
500internalOurs. Nothing was changed by the requestRetry 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.

idPanelClosed by
waitingWaiting for your agentserver: POST /start
connectedAgent connectedserver: POST /start
discoverReading your codeagent
installInstalling the SDKagent
identityConnecting your usersagent
appearanceApplying your lookeither
experienceDefining the assistanteither
knowledgeFilling the knowledge baseeither; also ticked once a source is indexed
screensConnecting screenseither; also ticked once a build registers a screen
actionsConnecting actionseither
verifyBuilding and verifyingserver: the app's first handshake
testTest conversationserver: a passing smoke test
doneReadyeither; 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.md

Answers 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.

FieldRequiredWhat it is
toolYesclaude_code, cursor, codex, windsurf, copilot, gemini, or your own name, up to 40 characters
modelNoThe model, up to 80 characters
osNoUp to 40 characters
detectedNoA 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.

KeyWhat it holds
appid, name, slug, platform_hint, live
sessionid, kind, expires_at, writes (apply or propose)
profileWhat the person entered at intake: links, description, is_live; brand_hints; the last detected report
intake.knowledgeWhat Rendel already read from those links: title, kind, address, status and an excerpt each
configThe 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
keysThe test and live publishable keys
sdkThe Flutter git URL and ref, the web SDK version and URL, ready-to-use snippets, and the docs address
apibase: the address of this API
stepsEvery step, and the ones an agent may report
factsAs in GET /status
schemasThe 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": {...} }.

FieldRequiredWhat it is
stepYesAny step id except waiting and connected
statusYesstarted, progress, done, skipped, failed or waiting
titleNoUp to 120 characters; the panel's line
detailNoUp to 2,000 characters, such as an assumption made
dataNoAny 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.

FieldRequiredWhat it is
themeYesThe 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
themeDarkNoThe app's own dark theme: the same tokens, any subset. What it leaves out is derived
copilotNolauncherLabel (up to 24 characters), composerHint, attachIcon, voiceIcon, sendIcon
reasonNoWhere 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.

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.

FieldLimitWhat it is
assistantName40One name, for the persona and the thread header
tone4,000How it speaks
instructions4,000What it helps with
guardrails4,000One Always … or Never … per line
topics20 × 120Topics it won't discuss
languages12Language subtags, the default first
welcomeTitle80The welcome screen's headline
welcomeBody160The line under it
suggestions4 × 60Openers on the welcome screen
reason1,000Where 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.

ItemFields
Textexternal_key, kind: "text", title (up to 160), content (up to 200,000), reason
Linkexternal_key, kind: "url", title (optional), uri (a public http or https address), reason
Archiveop: "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.

FieldRequiredWhat it is
external_keyYesYour name for it, such as GET /v1/orders/{id}
nameYessnake_case, up to 64 characters: the tool name the model sees
descriptionYesUp to 500 characters: when to call it
methodYesGET, POST, PUT, PATCH or DELETE
riskNoread (default), write or destructive
confirm_templateFor a writeUp to 300 characters, {param} where a value goes
url_templateYesThe production https address, {{param.x}} where a value goes
body_templateNoA JSON object as a string, for POST, PUT and PATCH
paramsNoUp 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_limitNoWhat of the answer the model sees; result_limit 1–50, default 5
timeout_msNo1,000–30,000, default 10,000
authNonone (default) or user: sent with the signed-in person's own token
auth_nameNoFor auth: "user" when the token is not sent as Bearer: header:X-Auth-Token, query:token or cookie:session
needs_keyNo{ kind: "bearer" | "header" | "query", name?, note? }: the endpoint wants a shared key, on its own or (with auth: "user") beside the user's token
reasonNoWhy

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: a bearer key needs the token moved with auth_name (header:X-Auth-Token), and a header key 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.

KeyWhat it holds
app_readytrue once the app has called in and sent its manifest
knowledge_readytrue once no source is waiting for indexing or being indexed
screens_missingHow 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.handshakeAtWhen the app first called in
facts.manifestThe last manifest: sdkVersion, actionCount, routeCount, and their names
facts.knowledgesources, indexed, pending, indexing, failed, chunks
facts.declaredScreensScreens declared and not archived
facts.httpActionsenabled, and needsKey
facts.smokeThe last smoke test: passed, ok of total
facts.liveAtWhen the app went live, or null
go_liveThe four Go live checks, each { key, label, ok, detail }
stepsEvery 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.

FieldRequiredWhat it is
summaryYesMarkdown, up to 20,000 characters: what was done
files_changedNoUp to 500 paths
human_todosNoUp 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.

FieldRequiredWhat it is
reasonYesUp to 2,000 characters
stepNoAn agent step id
{ "ok": true, "status": "failed", "token": "revoked" }