Developers

API reference

Every endpoint here takes JSON over HTTPS and an API key in the Authorization header. A key works in one workspace and only reaches the endpoints its scopes allow.

Updated October 1, 2026

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.

bash
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-Workspace or ?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

MethodPathScopeWhat it does
GET/api/meworkspaces:read and designs:readYou, the key's workspace, and its brands.
GET/api/workspacesworkspaces:readThe key's workspace and your role.
GET/api/workspaces/:idworkspaces:readOne workspace.
GET/api/workspaces/:id/membersworkspaces:readMembers with name, email and role.
GET/api/workspaces/:id/auditworkspaces:readThe workspace's audit log.
POST/api/workspaces/:id/invitesmembers:writeEmails a seven-day invitation.
json
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": "…" }]
}
Shortened. Brands are called personalities in the API.
json
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

MethodPathScopeWhat it does
GET/api/personalitiesdesigns:readThe workspace's brands, as { items }.
POST/api/personalitiespersonalities:writeCreates a brand.
PATCH/api/personalities/:idpersonalities:writeRenames a brand, sets its Mark, profile or style.
DELETE/api/personalities/:idpersonalities:writeDeletes a brand and its designs. A workspace keeps at least one.
GET/api/personalities/:id/designsdesigns:readA brand's designs, newest first.
GET/api/personalities/:id/branddesigns:readA brand's fonts, logos and saved colours. A key can read them, never change them.
json
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

MethodPathScopeWhat it does
POST/api/agent/composedesigns:writeLays out copy, remixes a template or applies edits, and saves.
POST/api/designsdesigns:writeCreates a design from a document.
GET/api/designs/:iddesigns:readA design with its document.
PATCH/api/designs/:iddesigns:writeChanges its title, document, Mark or sharing.
DELETE/api/designs/:iddesigns:writeDeletes a design.
POST/api/designs/:id/duplicatedesigns:writeSaves a copy beside it.
GET/api/designs/:id/thumbdesigns:readIts thumbnail image.
POST/api/designs/:id/renderdesigns:readRenders 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.

json
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).
json
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.

json
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 }
Width and height are 1 to 4096. 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

MethodPathScopeWhat it does
POST/api/uploadsdesigns:writeUploads an image for designs.
GET/api/uploadsdesigns:readThe workspace's uploads, newest first.
GET/api/uploads/:iddesigns:readRedirects to the image. ?w= asks for a width.
DELETE/api/uploads/:iddesigns:writeDeletes one of your uploads.
bash
curl https://gradiently.design/api/uploads \
  -H "Authorization: Bearer $GRADIENTLY_API_KEY" \
  -F "file=@shopfront.jpg;type=image/jpeg"

# { "id": "…", "url": "/api/uploads/…" }
One file in the 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

MethodPathScopeWhat it does
GET/api/marksmarks:readSearches the Market.
GET/api/marks/:codemarks:readOne Mark with its recipe, by code or id.
GET/api/marks/minemarks:readMarks held for the workspace, and your drafts in it.
POST/api/agent/markmarks:readBuilds or edits a recipe and reviews it. Saves nothing.
POST/api/marksbrand:generateSaves a recipe as a draft Mark.
PATCH/api/marks/:idbrand:generateChanges a draft you made.
POST/api/marks/:code/claimmarks:claimClaims an available Mark.
POST/api/marks/:code/buymarks:claimClaims 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.

json
POST /api/marks
{ "name": "Night Harbour", "recipe": { … }, "personalityId": "…" }

{ "id": "…", "code": "GR·K7Q2·MX", "name": "Night Harbour", "status": "draft", "recipe": { … } }
Get a valid 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

MethodPathScopeWhat it does
GET/api/marks/:code/versionsmarks:readVersions, newest first, as { items, next, canEdit }.
GET/api/marks/:code/versions/:vidmarks:readOne version.
POST/api/marks/:code/versionsbrand:generateSaves a version: { label, recipe }, both optional.
PATCH/api/marks/:code/versions/:vidbrand:generateRenames or stars: { label, starred }.
DELETE/api/marks/:code/versions/:vidbrand:generateDeletes a version.
POST/api/marks/:code/versions/:vid/restorebrand:generatePuts the version back as the draft.

Brand generation

MethodPathScopeWhat it does
POST/api/brand/generatebrand:generateReturns brand candidates. Writes nothing.
POST/api/brand/adoptbrand:generateCreates a draft Mark, a brand and three starter designs from one candidate.
json
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": [ … ] }
Adopt with exactly the input that made the candidate. The server makes the candidates again and never trusts a recipe from the client.

The Designer

MethodPathScopeWhat it does
POST/api/agentdesigns:writeOne Designer turn, streamed as newline-delimited JSON.
POST/api/batchesdesigns:writeStarts a set of designs from one brief.
GET/api/batchesdesigns:readYour sets, running and recent.
GET/api/batches/:iddesigns:readA set's progress.
DELETE/api/batches/:iddesigns:writeStops a set. Drawn designs stay.
GET/api/agent/runs?designId=designs:readDesigner turns still running for a design.
DELETE/api/agent/runs/:iddesigns:writeStops a running turn.
json
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": "…" }] }
Up to twelve items. Poll 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" } }.

json
{ "error": "API key lacks required scope for this endpoint." }
StatusWhen
401No key, a malformed key, or a key that is revoked or whose creator is no longer an owner or admin.
402Payment or credit is needed: a priced claim (checkout: true), no export licence, or AI credits too low (code: "credits").
403The key lacks a scope, belongs to another workspace, or its creator's role doesn't allow the action.
404The item doesn't exist or the key can't see it.
409A conflict: the item changed since you read it, a name is taken, or the action needs checkout.
422The request didn't pass validation. The message says which field and why.
429Too many requests. Wait for the seconds in Retry-After and try again.
503Busy or temporarily unavailable, for example the Designer or exports. Honour Retry-After.
504A 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.

LimitAllowance
Every request with a key120 a minute per key
Writes (POST, PATCH, DELETE)90 a minute per account, shared with the app
Renders10 a minute per account
New designs and duplicates60 a minute per account
Uploads60 a minute per account
Claims20 every ten minutes and 100 a day per account
Invitations30 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 the nextCursor from the answer; it is null on the last page.
  • Mark versions: pass the next value from the answer; it is null on the last page.
  • Designs of a brand: 100 a page, newest first. Pass the id of the last design you received.
  • Uploads: 60 a page, newest first. Pass the id of the last upload.
  • Brands, workspaces, members and the audit log: 100 a page. Pass the id of the last item (userId for members). Brands are listed from /api/personalities as { items }.

Keys, scopes and rotation are on API keys and scopes. If an endpoint you need isn't here, tell us.

Need a hand?

Send us a request with the topic API and MCP, and a person will reply.

Send a request