The API is the same one the Gradiently app uses, so a key sees the same data and passes the same checks as its creator would in the app, limited to one workspace and to its scopes. A key that calls an endpoint none of its scopes cover gets a 403.
Base URL and authentication
All paths below are under https://gradiently.design. Send your key as a bearer token with every request. Bodies are JSON with Content-Type: application/json, except uploads.
curl https://gradiently.design/api/marks/mine \
-H "Authorization: Bearer gr_live_…"- A key is always bound to the workspace it was created in. You don't need to name the workspace; if you send
X-Workspaceor?workspace=, it must be the key's own, or the request fails with 403. - Request bodies are checked strictly: a field the endpoint doesn't know fails with 422.
- Responses are JSON unless noted (images, redirects and the Designer's stream). Error messages to API keys are in English.
- A key acts as its creator. Designs and brands it makes belong to the workspace; Marks it claims are held by its creator.
Workspace
| Method | Path | Scope | What it does |
|---|---|---|---|
| GET | /api/me | workspaces:read and designs:read | You, the key's workspace, and its brands. |
| GET | /api/workspaces | workspaces:read | The key's workspace and your role. |
| GET | /api/workspaces/:id | workspaces:read | One workspace. |
| GET | /api/workspaces/:id/members | workspaces:read | Members with name, email and role. |
| GET | /api/workspaces/:id/audit | workspaces:read | The workspace's audit log. |
| POST | /api/workspaces/:id/invites | members:write | Emails a seven-day invitation. |
GET /api/me
{
"viewer": { "id": "…", "name": "Ada Moss", "handle": "ada", "workspaceId": "…" },
"workspaces": [{ "id": "…", "name": "Hearth", "role": "owner" }],
"activeWorkspaceId": "…",
"personalities": [{ "id": "…", "slug": "hearth", "name": "Hearth", "markId": "…" }]
}POST /api/workspaces/:id/invites
{ "email": "sam@example.com", "role": "editor" }
{ "id": "…", "expiresInDays": 7 }role is admin, editor or viewer. The person must verify the same email address before accepting.Brands
| Method | Path | Scope | What it does |
|---|---|---|---|
| GET | /api/personalities | designs:read | The workspace's brands, as { items }. |
| POST | /api/personalities | personalities:write | Creates a brand. |
| PATCH | /api/personalities/:id | personalities:write | Renames a brand, sets its Mark, profile or style. |
| DELETE | /api/personalities/:id | personalities:write | Deletes a brand and its designs. A workspace keeps at least one. |
| GET | /api/personalities/:id/designs | designs:read | A brand's designs, newest first. |
| GET | /api/personalities/:id/brand | designs:read | A brand's fonts, logos and saved colours. A key can read them, never change them. |
POST /api/personalities
{ "name": "Hearth Bakery", "markId": "…" }
PATCH /api/personalities/:id
{ "profile": { "about": "A neighbourhood bakery", "voice": "warm, plain" } }
{ "id": "…", "slug": "hearth-bakery", "name": "Hearth Bakery", "markId": "…", "profile": { … } }markId on create is optional. A profile can also hold audience, uses, keywords, dos, donts, website, handle and fonts.The designs list takes ?q= to search titles and ?mark= to filter by Mark. Each item has id, personalityId, markId, title, form, thumb, shared and updatedAt, without the document.
Designs
| Method | Path | Scope | What it does |
|---|---|---|---|
| POST | /api/agent/compose | designs:write | Lays out copy, remixes a template or applies edits, and saves. |
| POST | /api/designs | designs:write | Creates a design from a document. |
| GET | /api/designs/:id | designs:read | A design with its document. |
| PATCH | /api/designs/:id | designs:write | Changes its title, document, Mark or sharing. |
| DELETE | /api/designs/:id | designs:write | Deletes a design. |
| POST | /api/designs/:id/duplicate | designs:write | Saves a copy beside it. |
| GET | /api/designs/:id/thumb | designs:read | Its thumbnail image. |
| POST | /api/designs/:id/render | designs:read | Renders it to PNG or PDF. |
The easiest way to make a design from code is POST /api/agent/compose: you describe the content and the layout engine places it with the brand's Mark. The same endpoint remixes a template (template with text, photos, icons, hide) or, with designId and ops, edits a saved design.
POST /api/agent/compose
{
"personalityId": "…",
"composition": {
"size": "ig-post",
"layout": "statement",
"blocks": [
{ "role": "headline", "text": "Fresh bread from 7am" },
{ "role": "cta", "text": "Visit us" }
]
}
}
{ "id": "…", "url": "/studio/…", "issues": [ … ], "elements": [ … ] }issues lists what the review found (margins, collisions, hierarchy, contrast).POST /api/designs
{
"personalityId": "…",
"form": "blank",
"title": "Launch post",
"doc": { "ratio": "custom", "width": 1080, "height": 1350, "shift": [0.5, 0.5, 0.5, 0.5], "elements": [] }
}
{ "id": "…", "personalityId": "…", "markId": null, "title": "Launch post", "form": "blank", "doc": { … }, "updatedAt": "…", "thumb": null, "shared": false }markId is optional; without it the design wears the brand's Mark. Read a design from GET /api/designs/:id to see a full document.PATCH /api/designs/:id takes any of title, doc, markId and shared. Setting shared to true publishes a view link at /d/:id. Send baseUpdatedAt, the updatedAt you last read, to refuse the save with 409 if someone changed the design since.
POST /api/designs/:id/render
{ "width": 1080, "height": 1350, "format": "png", "page": 0 }
{ "id": "…", "data": "iVBORw0KGgo…", "encoding": "base64", "mimeType": "image/png", "width": 1080, "height": 1350, "pages": 1 }page counts from 0; a PDF without page has every page, each at its Studio size, which the request must match. Allow up to three minutes.Rendering needs the same export licence for the design's Mark as the Studio; without it the answer is 402. The render never publishes the design or stores a file.
Uploads
| Method | Path | Scope | What it does |
|---|---|---|---|
| POST | /api/uploads | designs:write | Uploads an image for designs. |
| GET | /api/uploads | designs:read | The workspace's uploads, newest first. |
| GET | /api/uploads/:id | designs:read | Redirects to the image. ?w= asks for a width. |
| DELETE | /api/uploads/:id | designs:write | Deletes one of your uploads. |
curl https://gradiently.design/api/uploads \
-H "Authorization: Bearer $GRADIENTLY_API_KEY" \
-F "file=@shopfront.jpg;type=image/jpeg"
# { "id": "…", "url": "/api/uploads/…" }file field: PNG, JPEG, WebP, GIF or AVIF, at most 8 MB, with a type that matches its contents. Use the returned url as an image src or a template photo.Marks
| Method | Path | Scope | What it does |
|---|---|---|---|
| GET | /api/marks | marks:read | Searches the Market. |
| GET | /api/marks/:code | marks:read | One Mark with its recipe, by code or id. |
| GET | /api/marks/mine | marks:read | Marks held for the workspace, and your drafts in it. |
| POST | /api/agent/mark | marks:read | Builds or edits a recipe and reviews it. Saves nothing. |
| POST | /api/marks | brand:generate | Saves a recipe as a draft Mark. |
| PATCH | /api/marks/:id | brand:generate | Changes a draft you made. |
| POST | /api/marks/:code/claim | marks:claim | Claims an available Mark. |
| POST | /api/marks/:code/buy | marks:claim | Claims a Mark another owner has listed. |
GET /api/marks takes q (name or code), tone (dark or light), material, status (listed, house or sale) and cursor. It answers { "items": [ … ], "nextCursor": "…" } with 24 Marks a page. A Mark has id, code, name, recipe, status, creator, holder and createdAt.
POST /api/marks
{ "name": "Night Harbour", "recipe": { … }, "personalityId": "…" }
{ "id": "…", "code": "GR·K7Q2·MX", "name": "Night Harbour", "status": "draft", "recipe": { … } }recipe from POST /api/agent/mark with a spec. personalityId saves the draft into that brand. Without it, a key saves the draft into its workspace's first brand, and fails with 404 if there is none. A person can keep up to 50 drafts.A claim is made for the key's creator, never the workspace. Free claims, and claims covered by a claim credit, complete at once. A claim that needs payment answers 402 with checkout: true, and buy answers 409 with checkout: true; finish those in Gradiently, because a key can't pay. buy takes { "priceCents": … }, the listed price you saw, and fails with 409 if it changed.
Mark versions
| Method | Path | Scope | What it does |
|---|---|---|---|
| GET | /api/marks/:code/versions | marks:read | Versions, newest first, as { items, next, canEdit }. |
| GET | /api/marks/:code/versions/:vid | marks:read | One version. |
| POST | /api/marks/:code/versions | brand:generate | Saves a version: { label, recipe }, both optional. |
| PATCH | /api/marks/:code/versions/:vid | brand:generate | Renames or stars: { label, starred }. |
| DELETE | /api/marks/:code/versions/:vid | brand:generate | Deletes a version. |
| POST | /api/marks/:code/versions/:vid/restore | brand:generate | Puts the version back as the draft. |
Brand generation
| Method | Path | Scope | What it does |
|---|---|---|---|
| POST | /api/brand/generate | brand:generate | Returns brand candidates. Writes nothing. |
| POST | /api/brand/adopt | brand:generate | Creates a draft Mark, a brand and three starter designs from one candidate. |
POST /api/brand/generate
{ "name": "Hearth", "description": "A neighbourhood bakery", "tone": ["calm", "warm"], "count": 4 }
[{ "key": "…", "name": "Hearth One", "recipe": { … }, "personality": { "name": "Hearth", "handle": "…" }, "starters": [ … ], "why": "…" }]
POST /api/brand/adopt
{ "input": { "name": "Hearth", "description": "A neighbourhood bakery", "tone": ["calm", "warm"], "count": 4 }, "key": "…" }
{ "mark": { … }, "personality": { … }, "designs": [ … ] }The Designer
| Method | Path | Scope | What it does |
|---|---|---|---|
| POST | /api/agent | designs:write | One Designer turn, streamed as newline-delimited JSON. |
| POST | /api/batches | designs:write | Starts a set of designs from one brief. |
| GET | /api/batches | designs:read | Your sets, running and recent. |
| GET | /api/batches/:id | designs:read | A set's progress. |
| DELETE | /api/batches/:id | designs:write | Stops a set. Drawn designs stay. |
| GET | /api/agent/runs?designId= | designs:read | Designer turns still running for a design. |
| DELETE | /api/agent/runs/:id | designs:write | Stops a running turn. |
POST /api/batches
{
"personalityId": "…",
"see": false,
"plan": {
"brief": "Autumn menu launch, Saturday 4 October",
"items": [
{ "size": "ig-post", "brief": "The announcement" },
{ "size": "ig-story", "brief": "Three new loaves, one line each" }
]
}
}
{ "id": "…", "state": "running", "items": [{ "index": 0, "title": "…", "state": "…" }] }GET /api/batches/:id; each item gains a designId and url once it is saved. state ends as done, stopped or failed.The Designer spends the workspace's AI credits. When the balance is below what a turn needs, /api/agent and /api/batches answer 402 with code: "credits". At most two sets run at once per person. The design tool on MCP and AI assistants reads the Designer's stream and saves the result for you, which is simpler than handling the stream yourself.
Errors
An error answers with a status code and a JSON body with a readable error message. Some add fields, named below. The one exception is /api/mcp: a body that isn't valid JSON answers 400 with a JSON-RPC error object instead, { "jsonrpc": "2.0", "id": null, "error": { "code": -32700, "message": "Parse error" } }.
{ "error": "API key lacks required scope for this endpoint." }| Status | When |
|---|---|
| 401 | No key, a malformed key, or a key that is revoked or whose creator is no longer an owner or admin. |
| 402 | Payment or credit is needed: a priced claim (checkout: true), no export licence, or AI credits too low (code: "credits"). |
| 403 | The key lacks a scope, belongs to another workspace, or its creator's role doesn't allow the action. |
| 404 | The item doesn't exist or the key can't see it. |
| 409 | A conflict: the item changed since you read it, a name is taken, or the action needs checkout. |
| 422 | The request didn't pass validation. The message says which field and why. |
| 429 | Too many requests. Wait for the seconds in Retry-After and try again. |
| 503 | Busy or temporarily unavailable, for example the Designer or exports. Honour Retry-After. |
| 504 | A render took too long. Try a smaller size or one page. |
Rate limits
Limits count over a sliding one-minute window unless noted. Going over answers 429 with a Retry-After header in seconds and a retryAfter field in the body. Slow down and retry after that time; don't retry at once.
| Limit | Allowance |
|---|---|
| Every request with a key | 120 a minute per key |
| Writes (POST, PATCH, DELETE) | 90 a minute per account, shared with the app |
| Renders | 10 a minute per account |
| New designs and duplicates | 60 a minute per account |
| Uploads | 60 a minute per account |
| Claims | 20 every ten minutes and 100 a day per account |
| Invitations | 30 an hour per account |
Exports and the Designer also have a shared capacity. When it is full the answer is 503 or 429 with Retry-After, even under your own limits. Through MCP, a tool call counts once for the MCP request and once for each API request the tool makes.
Pagination
Lists return a page at a time. Ask for the next page with ?cursor=.
- Market search (
/api/marks): 24 a page. Pass thenextCursorfrom the answer; it is null on the last page. - Mark versions: pass the
nextvalue from the answer; it is null on the last page. - Designs of a brand: 100 a page, newest first. Pass the
idof the last design you received. - Uploads: 60 a page, newest first. Pass the
idof the last upload. - Brands, workspaces, members and the audit log: 100 a page. Pass the
idof the last item (userIdfor members). Brands are listed from/api/personalitiesas{ items }.
Keys, scopes and rotation are on API keys and scopes. If an endpoint you need isn't here, tell us.

