Base URL
https://gramspec.com/gramapi
Authentication
Every request must carry an X-Api-Key header with a
per-user API key. Keys are issued from
your profile and begin with the
prefix gramspec_. A key identifies a single GramSpec
user and inherits that user's permissions; only
Enterprise and Admin accounts
can use the API. If the plan behind a key lapses, the key stops
working immediately.
X-Api-Key: gramspec_your_api_key_here
The same key is also accepted as a bearer token —
Authorization: Bearer gramspec_… —
which is the form MCP clients standardize on. The two carriers
are interchangeable; X-Api-Key wins when both are
present.
Missing or invalid keys get 401 Unauthorized. A
valid key on a non-Enterprise account gets
403 Forbidden. Keep keys out of client-side
code — treat them like a password.
The MCP server
Everything below is also served over the Model Context Protocol, so any MCP-capable agent — Claude Code, Claude Desktop, Cursor, or your own app on an agent framework — becomes a working analyst over your graphs with one connection:
https://gramspec.com/mcp
The connected agent gets eight tools composing the complete
loop: list_projects, get_graph,
graph_overview, describe_entity,
search_graph, generate_sql,
validate_sql, and run_query
(read-only execution against a connected graph's home
database, behind the same guard stack in-app Reason
uses). Every tool declares itself read-only, so clients can
relax per-call confirmation prompts. When a question has no
path through the graph, the tools say exactly what is
missing: the refusal contract, as a callable.
Alongside the tools, the canonical graph is an attachable
resource at
gramspec://project/{projectId}/graph,
and the analyst_system_prompt
prompt takes projectName and
databaseType and turns any agent into the
GramSpec analyst for that project.
A graph with no home connection still serves every
non-executing tool, so you can reason over a model before it
is wired to a database. generate_sql spends the
account's AI allowance under the same limit as
Query below; the rest are budget-free.
Orientation is cheap on purpose: graph_overview
returns a compact index of every table and column, which on a
large warehouse graph costs a small fraction of
get_graph.
Claude Code — one command:
claude mcp add --transport http gramspec https://gramspec.com/mcp \
--header "X-Api-Key: gramspec_your_api_key_here"
Cursor — in mcp.json:
{
"mcpServers": {
"gramspec": {
"url": "https://gramspec.com/mcp",
"headers": { "X-Api-Key": "gramspec_your_api_key_here" }
}
}
}
claude.ai and Claude Desktop — add a custom connector with the URL above and sign in with your GramSpec account when prompted (OAuth; PKCE; the connection asks for your consent once). An organization admin can alternatively enter the API key as a fixed request header where that beta is available.
Your own app — any framework with MCP support connects the same way; a complete branded-analyst example in about a hundred lines ships in the Builder's Guide.
Rate limits
Requests are throttled per user. The prompt-and-export
endpoints (SystemPrompt, AnalystKit,
Projects, Graph,
BuildPrompt, Rules) allow up to
120 requests per minute;
Query, which spends AI budget, allows
30 per minute. Over the limit you get
429 Too Many Requests with body
{ "error": "rate_limited" } — wait a moment
and retry. This is separate from the Query-only
AI-budget 429 (budget_cooldown, see
Error responses).
Endpoints
GET /gramapi/SystemPrompt
The primary endpoint. Returns the complete LLM system message — GRAM rules, SQL dialect profile, and your project's knowledge graph, stitched into one string. Cache it, pair it with the user's question, and send it to whatever model you like. GramSpec does not run the LLM and does not see your data.
Parameters
| Field | Required | Notes |
|---|---|---|
projectName | Yes (or projectId) | Exact project title as shown in Grammar. Case-insensitive; URL-encode spaces. |
projectId | Yes (or projectName) | Stable GUID from /gramapi/Projects; survives renames. |
databaseType | Yes | sqlserver, snowflake, postgresql, mysql, mariadb, databricks. Pass the dialect your app executes SQL against. |
promptMode | No | chat (default): markdown fences, multi-query, chart configs. singleshot: raw SQL only. |
Request
curl "https://gramspec.com/gramapi/SystemPrompt?projectName=YourProjectName&databaseType=snowflake" \
-H "X-Api-Key: gramspec_your_api_key_here"
Response 200
{
"systemPrompt": "<complete string to send as the LLM system role>",
"projectName": "YourProjectName",
"projectId": "6f1e7a5c-4b12-4d9e-9a31-6c0f88b2a4d2",
"databaseType": "snowflake",
"promptMode": "chat",
"graphHash": "sha256:7a8b..."
}
Send systemPrompt verbatim as the system role. Do
not append, prepend, or re-wrap it — the graph and rules
are already inside.
Caching
The response carries an ETag header equal to
graphHash. Send it back on later calls:
curl "https://gramspec.com/gramapi/SystemPrompt?projectName=YourProjectName&databaseType=snowflake" \
-H "X-Api-Key: gramspec_your_api_key_here" \
-H 'If-None-Match: "sha256:7a8b..."'
Unchanged → 304 Not Modified, no body; keep
your cache. The hash covers the fully assembled prompt, so any
graph edit, dialect update, or rule change invalidates it
automatically.
GET /gramapi/AnalystKit
The agentic path. Everything needed to stand up
your own Analyst-style tool loop against your own data plane:
the assembled analyst system prompt, the full graph, a compact
name index, and the JSON Schemas for the seven analyst tools
(run_query, search_graph,
describe_entity, ask_user,
record_finding, record_ledger_entry,
end_turn). You run the loop; there is no server
orchestration on this endpoint.
Parameters
| Field | Required | Notes |
|---|---|---|
projectName / projectId | Yes (one) | Same resolution rules as SystemPrompt. |
databaseType | Yes | Same dialect list as SystemPrompt. |
connectionName | No | Display name echoed into the prompt's "Connected data source" line. |
syncedSqlSchema | No | Physical schema qualifier, when your copy of the data lives under one. |
Response 200
{
"systemPrompt": "<assembled analyst system prompt>",
"graph": { "entities": [...], "factTypes": [...] },
"graphJson": "{\"entities\":[...]}",
"compactIndex": "<name-only graph index>",
"entityDossiers": {
"sales.SalesOrder": "<pre-rendered markdown dossier>",
"catalog.Product": "<...>"
},
"tools": [
{
"name": "run_query",
"description": "...",
"inputSchema": { "type": "object", "properties": { ... } },
"inputSchemaJson": "{...}"
}
],
"databaseType": "sqlserver",
"projectId": "6f1e7a5c-...",
"projectName": "YourProjectName",
"promptHash": "sha256:9c2d..."
}
Register the seven tools with your LLM exactly as returned
— the system prompt references them by name, and their
order is stable for prompt caching. Implement the handlers
locally: run_query executes read-only SQL against
your database, describe_entity returns
entityDossiers[name] (a pre-rendered dossier per
table — key, columns, aliases, enums, sample values,
derived formulas, relationships — from the same renderer
GramSpec's own apps use), search_graph searches
the returned graph across names, aliases, readings, and
sample values, and end_turn terminates the loop.
promptHash covers the prompt and the dossiers and
works with If-None-Match for 304
caching, same as SystemPrompt.
GET /gramapi/Projects
Lists the Grammar projects owned by the authenticated user. Use
it to render a project picker, or to cache the stable
projectId.
curl https://gramspec.com/gramapi/Projects \
-H "X-Api-Key: gramspec_your_api_key_here"
[
{
"projectId": "6f1e7a5c-4b12-4d9e-9a31-6c0f88b2a4d2",
"title": "<project title>",
"modifiedUtc": "2026-08-01T14:22:03Z"
}
]
GET /gramapi/Graph
Exports a project's GRAM knowledge graph as JSON, without
building a prompt. Useful when your app renders the graph in its
own explorer — the graph is already inside
systemPrompt, so the LLM call doesn't need this.
Parameters
| Field | Required | Notes |
|---|---|---|
projectName / projectId | Yes (one) | Same resolution rules as SystemPrompt. |
download | No | 1 or true returns the CANONICAL artifact as an attachment named <title>.gramspec-graph.json — sorted, id-free, timestamp-free, the file a repo tracks. |
versionId | No | Serve a sealed HISTORICAL version's graph (see Versions below) instead of the live one. Its ETag never changes — the artifact keeps the title frozen at seal time, so a later project rename cannot invalidate history. |
Inline (default)
curl "https://gramspec.com/gramapi/Graph?projectName=YourProjectName" \
-H "X-Api-Key: gramspec_your_api_key_here"
{
"projectId": "6f1e7a5c-4b12-4d9e-9a31-6c0f88b2a4d2",
"title": "<project title>",
"graph": { "entities": [...], "factTypes": [...] },
"graphJson": "{\"entities\":[...],\"factTypes\":[...]}"
}
graph is the parsed object; graphJson
is the raw string, byte-identical to what
SystemPrompt embeds. Prefer graphJson
when you round-trip the graph back to the API.
File download
curl "https://gramspec.com/gramapi/Graph?projectName=YourProjectName&download=1" \
-H "X-Api-Key: gramspec_your_api_key_here" \
-OJ
Every Graph response also carries
canonicalJson and contentHash
(sha256 of the canonical bytes), and an ETag of
"sha256:<contentHash>". The hash is the
graph's meaning identity: layout changes and
re-exports never move it, a modeling change always does.
GET /gramapi/Versions
The project's version history: one entry per sealed version
— versions mint automatically at editing-session
granularity inside GramSpec, plus named milestones. Each entry
carries the version's canonical contentHash and
its witnessed renameJournal (renames recorded
from the editor's own stable identities, never inferred).
curl "https://gramspec.com/gramapi/Versions?projectName=YourProjectName" \
-H "X-Api-Key: gramspec_your_api_key_here"
{
"projectId": "6f1e7a5c-...",
"versions": [
{ "versionId": "…", "label": "Launch cut", "contentHash": "…",
"createdUtc": "…", "renameCount": 1,
"renameJournal": "[{\"kind\":\"entity\",\"from\":\"Customer\",\"to\":\"Client\"}]" }
]
}
Version your graph in git
The product does this for you: connect a GitHub repository once from Grammar's History panel and every sealed version commits the canonical artifact with a message that reads as the change. The recipe below is the pull-based alternative, for teams who want their own CI cadence, a non-GitHub host, or pull rather than push.
Because the artifact is canonical, mirroring it into a repo gives you dbt-style change management with diffs that read as modeling changes, never noise. The whole recipe is a scheduled pull that commits when the hash moves:
# nightly CI step — commit only when the graph's meaning changed
curl -sS "https://gramspec.com/gramapi/Graph?projectName=YourProjectName&download=1" \
-H "X-Api-Key: $GRAMSPEC_API_KEY" \
-H "If-None-Match: $(cat graph.etag 2>/dev/null)" \
-o your-project.gramspec-graph.json -D headers.txt -w "%{http_code}" | grep -q 200 && {
grep -i '^etag:' headers.txt | cut -d' ' -f2 > graph.etag
git add -A && git commit -m "graph: $(date -u +%F)" && git push
}
A ready-made Node version of this recipe (ETag sidecar, exit
codes for CI) ships in the GramSpec repo as
tests/graph-mirror/mirror.ts. A rename shows in
the PR as one entity block moving plus one-line
entityName updates in each referencing role; a
constraint change is a one-line isMandatory /
isUnique flip. Anyone on the team can read the
diff aloud.
POST /gramapi/BuildPrompt
Same assembly as SystemPrompt, returned as
structured pieces: the full prompt plus the
rules template, the dialect block, and
the graph as separate fields. Accepts a raw
graphJson string as an alternative to a project
reference — useful for inspecting how a graph you hold
locally would be framed.
Parameters (JSON body)
| Field | Required | Values |
|---|---|---|
graphJson | One of these three | Raw GRAM knowledge-graph JSON string. |
projectName | Exact project title as shown in Grammar. | |
projectId | GUID from /gramapi/Projects. | |
databaseType | Yes | Same dialect list as SystemPrompt. |
promptMode | No | chat (default) or singleshot. |
Response 200
{
"prompt": "<assembled GRAM system prompt>",
"graphJson": "{\"entities\":[...]}",
"graph": { "entities": [...], "factTypes": [...] },
"rules": "<rules template, graph slot empty>",
"dialect": "<dialect profile>",
"databaseType": "sqlserver",
"promptMode": "chat"
}
GET /gramapi/Rules
The GRAM rules template and dialect profile with no graph
embedded. For inspection — use SystemPrompt
for production assembly, which handles the substitution
correctly.
curl "https://gramspec.com/gramapi/Rules?databaseType=snowflake" \
-H "X-Api-Key: gramspec_your_api_key_here"
{
"dialect": "<dialect profile>",
"rules": "<GRAM rules template>"
}
POST /gramapi/Query
The managed path: runs a natural-language prompt through
GramSpec's own LLM pipeline against a saved dataset and returns
the parsed result. This is the one endpoint that spends your
GramSpec AI allowance — if you run your own LLM, use
SystemPrompt instead and keep model control local.
Parameters (JSON body)
| Field | Required | Notes |
|---|---|---|
prompt | Yes | Natural-language question. |
chatConfigId | Yes | GUID of a saved dataset you own. |
provider | No | gemini (default), chatgpt, claude, grok. |
promptMode | No | chat (default) or singleshot. |
Request
curl -X POST https://gramspec.com/gramapi/Query \
-H "Content-Type: application/json" \
-H "X-Api-Key: gramspec_your_api_key_here" \
-d '{
"prompt": "Top 10 customers by revenue last quarter",
"chatConfigId": "b1d2e3f4-5678-4abc-9def-1234567890ab"
}'
Response 200
{
"content": "Here are the top 10 customers...",
"sqlBlocks": [ "SELECT TOP 10 ..." ],
"sharePointBlocks": [],
"chartConfig": {
"type": "bar",
"xColumn": "CustomerName",
"yColumn": "Revenue",
"title": "Top customers by revenue"
},
"raw": "<full model output>",
"provider": "gemini"
}
sqlBlocks is an array of SQL strings — a
question can legitimately produce more than one query, so loop
the array rather than taking the first element.
chartConfig is present only when the model proposed
a chart; its optional seriesColumn adds multi-series
grouping. If the model call itself fails, the response is still
200 with an error field set and empty
content — transport worked, the model call didn't.
Supported databases
databaseType accepts the dialects the GramSpec
prompt engine ships rules for:
sqlserver— Microsoft SQL Server (T-SQL)snowflake— Snowflakepostgresql— PostgreSQLmysql— MySQLmariadb— MariaDBdatabricks— Databricks SQL
Anything else falls back to a generic SQL profile. SharePoint
graphs are not supported by this API —
databaseType: "sharepoint" is rejected with
400 on every endpoint, because synced SharePoint
data lives on GramSpec's private storage that remote callers
cannot execute against.
End-to-end example
Python
import requests
API_KEY = "gramspec_your_api_key_here"
BASE_URL = "https://gramspec.com/gramapi"
HEADERS = {"X-Api-Key": API_KEY}
# Fetch (or revalidate) the system prompt for a project
resp = requests.get(
f"{BASE_URL}/SystemPrompt",
headers=HEADERS,
params={"projectName": "YourProjectName", "databaseType": "sqlserver"},
)
resp.raise_for_status()
body = resp.json()
system_prompt = body["systemPrompt"]
graph_hash = body["graphHash"] # send back via If-None-Match next time
# Hand system_prompt to your LLM of choice as the system role,
# with the user's question as the user message.
C# / .NET
using System.Net.Http;
using System.Net.Http.Json;
var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-Api-Key", "gramspec_your_api_key_here");
var body = await client.GetFromJsonAsync<JsonElement>(
"https://gramspec.com/gramapi/SystemPrompt" +
"?projectName=YourProjectName&databaseType=sqlserver");
string systemPrompt = body.GetProperty("systemPrompt").GetString();
string graphHash = body.GetProperty("graphHash").GetString();
Error responses
| Status | Meaning |
|---|---|
400 | Missing or invalid parameters; unsupported databaseType; or a typed condition — connection_password_expired (the dataset owner must re-enter their database password) or graph_member_unresolved (a composed dataset references a graph that no longer resolves). |
401 | Missing, invalid, revoked, or expired X-Api-Key. |
403 | Valid key without an active Enterprise plan — or a dataset you don't own. |
404 | Project or dataset not found for this user. |
429 | Two cases, both JSON: rate_limited — per-user request rate exceeded (any endpoint; see Rate limits); or budget_cooldown — the account's AI allowance is used for now, body includes resetsAtUtc (Query only). Distinguish them by the error field. |
500 | Server error while loading or exporting the project. |
503 | Authentication service temporarily unavailable; retry. |
Error responses are JSON of the form
{ "error": "message" }. One deliberate exception:
an LLM-level failure on Query returns
200 with the error field set on the
parsed response, so your parser handles one shape for both
outcomes.
Getting an API key
- Make sure your GramSpec account is on the Enterprise plan. See Pricing.
- Open your profile and scroll to the API keys section.
- Generate a new key, copy it immediately (the full value is shown only once), and store it in your secret manager. Keys are perpetual by default; set an optional expiry in days at creation if your security policy wants rotating credentials.
- Send it as
X-Api-Keyon every request.
If you suspect a key has leaked, revoke it from the same profile section and issue a new one — revoked keys stop working immediately.
See also
- GRAM Specification v1.1 — the formal standard the API's rules are built on.
- About — why GRAM exists.