API quickstart
Mint a token with write access under Settings then API, export it, and you can create a board and add cards in two requests. This page has the whole loop — authenticate, create, add, upload an image, read, update, move, delete — as copy-pasteable curl, TypeScript and Python, with the retry and error handling an agent needs.
1. Get a token
In the app: Settings → API → New token. Tick Allow writes if you intend to change anything. Copy it immediately — it is shown once.
export SOLEIL_TOKEN="undefined…"
export SOLEIL_API="https://clusters.soleilpictures.com/api/v1"2. Check it works
curl -s "$SOLEIL_API/me" -H "Authorization: Bearer $SOLEIL_TOKEN"{
"user_id": "…", "display_name": "Andrew", "tier": "paid",
"scopes": ["read", "write"],
"rate_limit": { "limit": 1000, "remaining": 993, "reset": 1786000000 }
}Always do this first. It confirms the token is live, tells you which scopes you have before you find out the hard way, and reports your remaining rate budget.
3. Create a board and add cards
BOARD=$(curl -s -X POST "$SOLEIL_API/boards" \
-H "Authorization: Bearer $SOLEIL_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Scene 4 — Diner"}' | jq -r .board.id)
curl -s -X POST "$SOLEIL_API/boards/$BOARD/cards" \
-H "Authorization: Bearer $SOLEIL_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"cards":[
{"kind":"note","title":"Tone","body":"Warm, low-key, practicals only"},
{"kind":"link","url":"https://example.com/reference"}
]}'Cards without x/y are placed in free space, so they cannot land on top of what is already there.
4. Add an image
Two requests: bytes up, then a card referencing what came back.
KEY=$(curl -s -X POST "$SOLEIL_API/uploads?board=$BOARD" \
-H "Authorization: Bearer $SOLEIL_TOKEN" \
-H "Content-Type: image/jpeg" \
--data-binary @frame.jpg | jq -r .image_key)
curl -s -X POST "$SOLEIL_API/boards/$BOARD/cards" \
-H "Authorization: Bearer $SOLEIL_TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d "{\"kind\":\"image\",\"image_key\":\"$KEY\",\"alt\":\"Diner counter, night\"}"See Images API for formats, the 25 MB ceiling and quota behaviour.
TypeScript
const API = "https://clusters.soleilpictures.com/api/v1";
const TOKEN = process.env.SOLEIL_TOKEN!; // never ship this to a browser
async function soleil<T>(path: string, init: RequestInit = {}): Promise<T> {
const res = await fetch(`${API}${path}`, {
...init,
headers: {
authorization: `Bearer ${TOKEN}`,
"content-type": "application/json",
// Idempotency-Key makes a retried POST replay rather than duplicate.
...(init.method === "POST" ? { "idempotency-key": crypto.randomUUID() } : {}),
...init.headers,
},
});
const body = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(`${res.status} ${body.error ?? res.statusText}`);
return body as T;
}
const { board } = await soleil<{ board: { id: string } }>("/boards", {
method: "POST",
body: JSON.stringify({ name: "Scene 4 — Diner" }),
});
await soleil(`/boards/${board.id}/cards`, {
method: "POST",
body: JSON.stringify({
cards: [{ kind: "note", title: "Tone", body: "Warm, low-key" }],
}),
});Python
import os, uuid, requests
API = "https://clusters.soleilpictures.com/api/v1"
TOKEN = os.environ["SOLEIL_TOKEN"]
def soleil(method, path, json=None):
headers = {"Authorization": f"Bearer {TOKEN}"}
if method == "POST":
headers["Idempotency-Key"] = str(uuid.uuid4())
r = requests.request(method, f"{API}{path}", json=json, headers=headers)
body = r.json() if r.content else {}
if not r.ok:
raise RuntimeError(f"{r.status_code} {body.get('error', r.reason)}")
return body
board = soleil("POST", "/boards", {"name": "Scene 4 — Diner"})["board"]
soleil("POST", f"/boards/{board['id']}/cards", {
"cards": [{"kind": "note", "title": "Tone", "body": "Warm, low-key"}]
})The rest of the loop
# Read everything on the board
curl -s "$SOLEIL_API/boards/$BOARD/cards" -H "Authorization: Bearer $SOLEIL_TOKEN"
# Change one card
curl -s -X PATCH "$SOLEIL_API/boards/$BOARD/cards/$CARD" \
-H "Authorization: Bearer $SOLEIL_TOKEN" -H "Content-Type: application/json" \
-d '{"title":"Tone — revised"}'
# Move cards to another board
curl -s -X POST "$SOLEIL_API/boards/$BOARD/cards/move" \
-H "Authorization: Bearer $SOLEIL_TOKEN" -H "Content-Type: application/json" \
-d "{\"to_board_id\":\"$OTHER\",\"card_ids\":[\"$CARD\"]}"
# Delete — the response body IS the undo
curl -s -X DELETE "$SOLEIL_API/boards/$BOARD/cards/$CARD" \
-H "Authorization: Bearer $SOLEIL_TOKEN"DELETE on a card returns the full card it removed. There is no undo toast on an HTTP call, so the response body is the undo — POST it back to restore it.
Notes for agents
- Check
scopesfrom/mefirst. Writing needswrite; deleting needsdelete, which is a separate grant. - Always send
Idempotency-KeyonPOST. Network retries are otherwise duplicate writes. - Batch card creation. Up to 1000 cards per call; one call with fifty cards beats fifty calls.
- Watch
x-ratelimit-remainingon every response rather than waiting for the429. On a429, honourretry-after. - Paginate. List endpoints return 100 by default; follow
next_offsetuntil it isnull. - Do not assume
live: true. Afalsemeans saved-but-not-pushed to open canvases. - *
404means "not found or not yours".* Do not retry it as if it were transient. - Branch on
code, not the message. Error prose may be reworded; codes are the contract.
Full error semantics: Errors and status codes.
Frequently asked questions
What is the smallest useful request?
GET /me with your bearer token. It confirms the token works and tells you which scopes it has and how much rate budget is left, before you try to write anything.
Do I need to create a workspace first?
No. Creating a board without a workspace_id puts it in your personal workspace, creating one if it does not exist.
How do I add an image?
Two requests. POST the bytes to /uploads to get an image_key, then create a card with kind image and that key.
Machine-readable: /docs/api/quickstart.md · /llms.txt