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 three.
| 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:docs is the one to think about. It is the only permission on this API that can change or
destroy anything: 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.
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. All four are readable from browser JavaScript — they are in the CORS expose list, not merely sent.
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.
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.