API & MCP Overview
What this is
GRAILS extracts your design system from Figma at sync time and stores it. The API and the MCP server let you query that stored copy — from a build script, from CI, or from an AI coding agent.
Sync time Figma plugin → captures & normalises → GRAILSQuery time Your agent → API / MCP → GRAILSThe surface is read-only for design system data. The one exception is documentation, which the API can also write.
Two transports, one API
| REST | https://api.grails.design/v1/… |
| MCP | https://api.grails.design/mcp/v1 |
They are the same application over the same data, and they authenticate with the same API key. There is no separate MCP login and no OAuth flow.
Pick REST for build scripts, CI checks and anything that fits a URL. Pick MCP when an AI agent is the consumer — the tools carry descriptions the agent reads before calling them.
Getting a key
Team settings → Integrations → Manage API keys.
Any admin or editor can create their own key. Keys look like this:
grails_sk_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXA key expires after 90 days by default, and you get an email 7 days and 1 day before. Rotating early gives you a 7-day overlap where both keys work, so nothing breaks mid-deploy.
Removing a member from the team deletes their keys immediately.
Scopes
A key carries a set of scopes. There are eight.
| Scope | What it opens |
|---|---|
read:library | Everything this API reads from your design system: components and their specs, build order, validation, entity search and the graph, design tokens and their resolved values per mode, styles and their properties, drift and health |
read:docs | Read documentation. Also validates it — a dry run writes nothing |
write:docs | Create, update and delete documentation |
write:code | Upload your code components and design tokens with the Grails Code Sync CLI (REST only, /v1/code/*) |
write:tokens | Edit how your code’s tokens are matched to Figma’s: the naming rules of each code source and the colour threshold (REST and MCP) |
read:patterns | Read your project’s patterns: the list, and one pattern whole with whether it still fits your design system |
write:patterns | Create and replace patterns, drafts and published alike. It does not include reading them |
write:pairing | Link and unlink which part of a Figma component is which part of its code component: elements, props and their values. It does not include reading them |
write:docs, write:code, write:tokens, write:patterns and write:pairing are the ones to think about. They are the only permissions on
this API that change anything (write:patterns creates patterns and replaces them whole, write:pairing links and unlinks a component’s design and code parts): write:docs edits and deletes documentation, write:code uploads code
components and design tokens (a complete upload can mark components deleted in code), and write:tokens
edits and deletes the naming rules that the code ↔ Figma token match applies. Everything else is read-only, fed by
the Figma sync. A key that reads your design
system and cannot touch your documentation is read:library plus read:docs.
Scope checks are exact. There is no read:* wildcard, so a key without read:library gets a
403 — with the missing scope named in the error, so you know what to add. Over MCP it is the tool
that refuses, not the connection: see Answers about your key.
Rate limits
Every key has a requests-per-minute figure, 60 by default.
Every response to an authenticated request carries four headers — success and error alike:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Your key’s requests-per-minute |
X-RateLimit-Remaining | What is left in the current minute — may be absent, see below |
X-RateLimit-Reset | Unix epoch seconds when the window ends — an absolute instant, not a countdown |
X-RateLimit-Enforced | false today. Read this one first |
Reset is an absolute epoch rather than “N seconds from now” on purpose: a delta forces your client
to know when the response was issued, and network latency or a retry puts that out of step.
A request rejected for a missing or invalid key carries none of them, and that is deliberate:
all four describe a specific key’s budget, and there is no key to describe. Neither does a 403 for a
plan without API access (GRAILS_AUTH_PLAN_REQUIRED) nor a 503: the gateway answers those before it
counts. All four are readable from browser JavaScript — they are in the CORS expose list, not merely sent.
The OpenAPI document declares them on each response, in components.headers: three required and
X-RateLimit-Remaining optional, and none required on a 403, which the gateway can answer too.
Reading the contract
/docs | Every operation, its scope and its parameters — readable, no credential |
/openapi.json | The same thing, for a machine |
Both are public, and for the same reason: a contract you need a credential to read cannot help you decide whether to ask for a credential.
An OpenAPI 3.1 document, generated from the same schemas the server validates against. A test fails the build if a route exists that the document does not describe, so it cannot drift out of date by omission.
Errors
Every error has the same shape, on both transports:
{ "error": { "code": "GRAILS_NOT_FOUND", "message": "Component with id '…' not found in this project", "status": 404, "request_id": "req_abc123def456", "details": { "entity": "Component", "id": "…" } }}Branch on code, never on message — messages get clearer over time, codes do not change. Include
request_id when you report a problem; it is how we find your exact request.
Answers about your key
These are decided before your request reaches the API, so they arrive the same way on both
transports: as the HTTP status of the request itself — for MCP, of the POST to /mcp/v1.
| Status | code | What it means | What to do |
|---|---|---|---|
401 | GRAILS_AUTH_MISSING_TOKEN | No Authorization: Bearer <key> header, or a malformed one | Send the header |
401 | GRAILS_AUTH_INVALID_TOKEN | No key matches what you sent | Check you copied the whole key. If it was deleted, create a new one |
401 | GRAILS_AUTH_REVOKED_TOKEN | The key was revoked | Create a new key, or ask a team admin |
401 | GRAILS_AUTH_EXPIRED_TOKEN | The key is past its expiry date | Rotate it |
403 | GRAILS_AUTH_PLAN_REQUIRED | The team’s plan does not include API access | A plan change. No key and no retry fixes it |
503 | GRAILS_SERVICE_UNAVAILABLE | We could not check your key right now | Your key is fine. Wait the seconds in Retry-After and retry |
Every 401 carries WWW-Authenticate: Bearer realm="grails". There is no OAuth flow behind it —
the fix is always the key, as the table says.
A missing scope is the one that differs by transport, because the key itself is valid:
| REST | 403 GRAILS_AUTH_INSUFFICIENT_SCOPE, with details.required_scope, and WWW-Authenticate: Bearer error="insufficient_scope", scope="<the scope>" |
| MCP | The connection works; that one tool answers with isError and the same code in its result. The other tools stay usable |
Either way the fix is a key that carries that scope.
What “this project” means
Your key belongs to one project, and every request operates on it. There is no project_id
parameter anywhere — not in a URL, not in a body, not in an MCP tool argument. Sending one has no
effect; it is ignored, not honoured.
If you work across several projects, you need a key per project.
Where to go next
- MCP tools — connect Claude Code, Cursor or another agent, and what each tool does
- REST endpoints — the routes, their parameters and their responses
- Reading responses — the fields you have to read before the data, and why. Start here if you are writing anything that consumes these responses programmatically.