API Reference
Fetch variable Jar variables from anywhere — iOS/Android apps, CI pipelines — over REST. Read-only, using the same token as the MCP server.
Beta: the API and MCP may change before launch; formats are now versioned (see Platform code output), and breaking changes are announced with a 12-month overlap.
Issuing a token
- variablejar.com/app → a project card’s ⋯ menu → Properties → the API tokens (MCP) section
- Enter a token name and press Issue — the token (
vjt_…) is shown only once, right after issuing. Store it somewhere safe. - A token grants access to that one project only. If it leaks, revoke it immediately on the same screen.
Tokens expire (default 180 days, up to 365). You get a bell notification 14 days before. Only the workspace owner can issue a token that includes text values (STRING) — every language, unreleased translations included. You can also limit a token to selected collections.
A token is only as safe as where you keep it: anyone holding it receives every value within its scope. Data you export through the API or MCP leaves the service and is processed under your responsibility.
Endpoint
GET https://variablejar.com/api/v1/variables
Authorization: Bearer vjt_...
curl -H "Authorization: Bearer vjt_..." \
"https://variablejar.com/api/v1/variables"
Text values (STRING) are not included unless the token has the text scope and you pass ?include=strings.
Response
Values are final, with aliases ({Collection/key}) resolved. Only cells that couldn’t be resolved appear in unresolved with a reason (broken · cycle · inline), keeping their original value.
{
"apiVersion": "v1",
"project": { "name": "My Variables" },
"collections": [
{ "name": "Color", "modes": ["light", "dark"] },
{ "name": "Text", "modes": ["en", "ko"] }
],
"tokens": [
{
"key": "color/primary",
"collection": "Color",
"type": "COLOR",
"valuesByMode": { "light": "#3B82F6", "dark": "#60A5FA" }
},
{ "key": "hello", "collection": "Text", "type": "STRING", "valuesByMode": {}, "omitted": "strings" }
]
}
Without include=strings, STRING tokens are still listed, with an empty valuesByMode and "omitted": "strings".
Platform code output
Add ?format= to receive generated platform code instead of JSON:
| format | Output | Content-Type |
|---|---|---|
css@1 (alias css) | CSS custom properties — default mode in :root, other modes in [data-theme="<mode>"] | text/css |
swift@1 (alias swift) | SwiftUI — one type per collection, one static instance per mode | text/plain |
compose@1 (alias compose) | Jetpack Compose (Kotlin) — same structure as Swift | text/plain |
Formats are versioned. The bare name (css) is a permanent alias for @1 — it never moves to a newer version on its own. Within a version, only two kinds of change are allowed: fixing output that was not valid syntax, and security escaping fixes; both bump the format’s rev (listed by /formats, below) and invalidate caches. Anything that changes a value’s representation or an identifier is a new version (css@2), and the previous version stays available for 12 months (announced with Deprecation and Sunset headers).
curl -H "Authorization: Bearer vjt_..." \
"https://variablejar.com/api/v1/variables?format=css" \
> variables.css
Output is deterministic — identical tokens always produce identical bytes, so ETag/If-None-Match caching works the same as the JSON response. Aliases are always resolved to final values; unresolved references are emitted as comments, never as fabricated values. An unknown format returns 400 { "error": "unknown_format" }.
Identifiers are derived from collection and variable names and do not depend on the order of your data: reordering collections, variables or modes in Figma never renames a token. When two names collapse to the same identifier, the one that sorts first (by its original name) keeps the plain identifier and the others get _2, _3 …; adding a new colliding name can renumber existing suffixes, so avoid names that differ only in separators or case. The same variable gets the same identifier whichever filter you use (collection, include=strings, or a token limited to some collections): names are settled across the whole project first and the response is filtered afterwards.
?collection=<name> limits the output to one collection (400 unknown_collection if the name is not in the project). ?view=ai@1 returns a compact, sanitized view for AI agents (names and values only, text values omitted by default); it can’t be combined with format, and any other view except index (below) returns 400 unknown_view.
?view=index together with format returns, instead of code, the identifier each variable gets in that format — for checking names without parsing the generated code. Identifiers are spelled as in the output (Swift/Kotlin keywords keep their backticks). Text (STRING) variables are left out of the index unless the token has the text scope and you pass include=strings. view=index without format returns 400 unknown_view.
curl -H "Authorization: Bearer vjt_..." \
"https://variablejar.com/api/v1/variables?format=css@1&view=index"
{
"format": "css@1",
"rev": "1-1",
"identifiers": [
{ "collection": "Color", "key": "btn/bg", "identifier": "--color-btn-bg" }
]
}
GET /api/v1/formats lists the formats and versions this server offers, with each one’s current rev, file name and Content-Type. It takes the same token, counts toward neither usage nor the rate limit, and supports ETag/If-None-Match like the variables endpoint (no X-VJ-Stamp — the list is static). A superseded version also carries "deprecated": { "since": "YYYY-MM-DD", "sunset": "YYYY-MM-DD" }; sunset is present only when a date is scheduled.
{
"contract": "1",
"formats": [
{ "id": "css", "version": 1, "spec": "css@1", "rev": "1-1", "alias": "css", "filename": "variables.css", "mime": "text/css; charset=utf-8" },
{ "id": "swift", "version": 1, "spec": "swift@1", "rev": "1-1", "alias": "swift", "filename": "Tokens.swift", "mime": "text/plain; charset=utf-8" },
{ "id": "compose", "version": 1, "spec": "compose@1", "rev": "1-1", "alias": "compose", "filename": "Tokens.kt", "mime": "text/plain; charset=utf-8" }
],
"views": [{ "view": "ai@1", "rev": "1" }]
}
Polling and ETag
Responses include an ETag header. Send it back as If-None-Match and you’ll get 304 (no body) when nothing changed — saving bandwidth for periodic checks from mobile apps and CI. Use the received ETag value as-is (it may include W/).
curl -i -H "Authorization: Bearer vjt_..." \
-H 'If-None-Match: "…"' \
"https://variablejar.com/api/v1/variables"
# HTTP/2 304
Every 200 and 304 response carries these headers:
| Header | Meaning |
|---|---|
ETag / If-None-Match | Send the last ETag back; unchanged data returns 304 with no body |
X-VJ-Stamp | Freshness stamp of the project content + this token’s scope (the same value for every format of the same token) — same stamp, same data for every format |
X-VJ-TTL | Seconds to wait before re-checking (30–600) |
X-VJ-Token-Expires | When this token expires |
X-VJ-Contract | API contract version (1) |
X-VJ-Format | The format spec of a ?format= response, e.g. css@1 |
Errors
Error responses have the body { "error": "<code>" }.
| Status | Meaning |
|---|---|
400 unknown_format · unknown_view · unknown_collection | Unknown format or view value, or a collection name that isn’t in the project |
401 unauthorized | Missing / malformed / revoked token |
401 token_expired | The token has expired — issue a new one |
403 token_inactive | The person who issued the token left the workspace or lost the role the token needs |
403 plan_required | The workspace’s plan doesn’t include API access |
403 scope_denied | include=strings needs a token with the text scope |
404 not_found | Wrong path, or the project was deleted or moved to the trash |
405 method_not_allowed | Method other than GET or OPTIONS (OPTIONS answers CORS preflight with 204) |
413 too_large | The project is over the API’s size limit (25,000 variables or 24 MB of variable data); move some collections to a separate project or contact support@variablejar.com |
426 client_outdated | The reported client (X-VJ-Client) is older than the minimum this server accepts; upgrade @variablejar/mcp |
429 rate_limited | Too many requests — retry after the number of seconds in Retry-After |
500 internal | Server error — if it persists after retrying, contact support@variablejar.com |
502 upstream_unreachable | The API gateway couldn’t reach the backend — retry |
503 not_configured | The service isn’t configured — contact support@variablejar.com |
MCP server (IDE / AI agents)
From IDEs and AI coding agents, the @variablejar/mcp MCP server is more convenient than REST — the same token works for both. You can hand this page to an AI agent and ask it to set the integration up for you.
Add to your MCP configuration (pin the version):
{
"mcpServers": {
"variablejar": {
"command": "npx",
"args": ["-y", "@variablejar/mcp@0.3.1"],
"env": { "VARIABLEJAR_TOKEN": "vjt_your_token_here" }
}
}
}
Where the file lives depends on the client:
| Client | Config file |
|---|---|
| Claude Code | .mcp.json in the project root |
| Cursor | .cursor/mcp.json |
| Other MCP clients | See the client’s MCP server settings |
Available tools:
| Tool | Input | Description |
|---|---|---|
list_collections | — | Collections with modes, token counts, type distribution and top-level groups — start here |
list_tokens | collection? prefix? type? query? format? limit (50, max 200) cursor? include_strings? | Paged tokens with values resolved per mode. The first line of the data block is always total=<n> returned=<m> nextCursor=<c|none>. With format (e.g. css@1) each token also carries its identifier in that format |
get_token | key collection? format? | One token by key (e.g. color/primary); if the key exists in several collections you get the candidates |
list_formats | — | Formats and versions the server can generate, same as GET /formats |
get_tokens | format collection? | The project (or one collection) as generated code. Results above 24,000 characters are not truncated — the tool tells the agent how to narrow by collection. For whole files in CI, use REST and commit the file |
The server keeps results in memory only, re-checks the API once per TTL (30–600 s) and reuses everything while the project’s freshness stamp is unchanged. Tool results carry the API’s error codes (error: unauthorized, error: plan_required, …); when the API can’t be reached, the last result is served with a note of its age. Token data is wrapped in random boundary markers with a fixed preamble stating it is data, not instructions.
Environment variables:
| Variable | Required | Description |
|---|---|---|
VARIABLEJAR_TOKEN | ✓ | Project API token (vjt_…) — one token, one project |
VARIABLEJAR_API_URL | https://variablejar.com/api/v1 (default) or https://staging.variablejar.com/api/v1; any other host is refused |
Upgrading from 0.2.0: that version called a retired endpoint and now receives 410. Tool names are unchanged and inputs were only added, but outputs changed — list_tokens is paged with a total=… first line and short keys (c, k, t, v), get_token returns the same short keys and lists candidates when the key exists in several collections, list_collections no longer returns ids, and get_tokens returns a preamble block plus a code block inside boundary markers.
The MCP server is read-only — this token cannot write or delete anything.
Notes
- Don’t call this directly from a browser (web frontend) — the token would be exposed in client code. Use it from servers, apps, or CI, where the token stays hidden.
- Write APIs and delta (changes-only) queries are not available yet.
- For LLM context, the full documentation is available as /llms.txt (index) and /llms-full.txt (everything in one file).