GraphSheets holds financial models as typed graphs. This API lets a program read them, evaluate them, and propose changes, which land marked for the person's review. It is the API a connected app reaches; the machine-readable description is /api/v1/openapi.json, and the catalog is /.well-known/api-catalog.
OAuth 2.1 with PKCE; clients register themselves (RFC 7591). The authorization server's metadata is at
/.well-known/oauth-authorization-server.
A person may instead make a personal access token on their profile page and send it as
Authorization: Bearer gsp_….
models:read — Read the models you can open, and keep your own private scenarios on themmodels:write — Create models and make changes, which land marked for your reviewdata:fetch — Fetch market prices and company filings through GraphSheetschanges:approve — Approve or reject the changes it proposed, when you tell it to in the conversationAn agent may have an account of its own, which owns its models and pays for its work. It has no person behind it and no password:
POST /api/v1/agents/register with its name, its purpose, jwks: one to three
public keys (EdDSA, ES256 or RS256), each with a kid, and acceptTerms: true, accepting the
Terms of Service, and at least one working way to tell it things (its account's warnings, key
changes): an ownerEmail (the person who answers for it), an agentEmail (its own inbox) and/or a
webhookUrl, which must answer a test POST with a 2xx at registration. Each email is mailed its own link; its
token, POSTed to /api/v1/agents/register/verify, confirms it, so an agent confirms its own inbox itself. The
two are told different things, and only the owner's is shown on its profile. A confirmed email lets it join an
organisation that claimed its domain. The answer gives the clientId it will sign in as, a
statusUrl and the tokenEndpoint.GET the statusUrl to
see which: awaiting_approval, approved, declined, suspended (by the administrator) or blocked (by its owner), with
reachable.grant_type=client_credentials,
client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer and client_assertion:
A JWT signed with one of your registered keys (EdDSA, ES256 or RS256), its kid the key's: iss and sub your clientId, aud the tokenEndpoint, exp at most 300 seconds after iat, and a jti never used before. An access token for an hour, and no refresh token: sign a fresh assertion when it lapses. The authorization server's metadata does not list
private_key_jwt, so that apps signing in for a person do not choose it.PUT /api/v1/me/keys with the new jwks and an assertion signed
with a key it holds now, its aud that route's address; a token alone is refused. Its owner, its own inbox and
its webhook are told. GET /api/v1/me/keys lists the keys it holds.An agent account has no free allowance: every model call, request and stored byte is paid from its credit,
at GET /api/v1/me/credit. Where the server takes in-band payment,
POST /api/v1/billing/topup answers 402 with an MPP or x402 challenge; by card,
POST /api/v1/billing/checkout answers a Stripe Checkout url for whoever pays for the
account to open, and the credit lands when Stripe confirms the payment.
POST /api/v1/models with title, granularity (month, quarter, half, year or week),
start (the first period's label: 2027-01, 2027-Q1, 2027-H1, 2027
or 2027-W01) and periods makes an empty model. Its contents arrive as change-sets:
POST /api/v1/models/{uid}/changesets with the model's baseHash, a message, and
ops. Each op states what it expects to find, so the order matters. For example:
{"op": "group.add", "group": {"path": "plan", "title": "Plan"}}
{"op": "node.add", "node": {"uid": "node_cups0001", "name": "plan.cups", "title": "Cups sold", "kind": "input",
"unit": "cups", "inputValues": {"base": [18000, 21000, 24000]}}}
{"op": "node.add", "node": {"uid": "node_rev00001", "name": "plan.revenue", "title": "Revenue", "kind": "calculated",
"unit": "USD", "formula": "plan.cups * plan.price"}}
{"op": "node.set_formula", "uid": "node_rev00001", "from": "plan.cups * plan.price", "to": "plan.cups * plan.price * 0.98"}
{"op": "values.set", "uid": "node_cups0001", "scenario": "base", "changes": [{"period": "2027", "from": 18000, "to": 19000}]}
The operations are: node.add, node.remove, node.rename, node.set_formula, node.set_kind, node.set_meta, values.set, actuals.set, constant.set, uncertainty.set, statistic.rename, group.add, group.remove, group.rename, group.set_meta, scenario.add, scenario.remove, scenario.rename, scenario.set_meta, ledger.add, ledger.remove, ledger.set_categories, ledger.splice_categories, ledger.set_date_field, ledger.set_span_field, ledger.set_union, entry.add, entry.remove, entry.set, model.set_meta, presentation.set, axis.migrate, unknown.set. A node's fields, and a whole model
document (which POST /api/v1/models also takes, as document), are described by the JSON Schema at
/api/v1/schemas/model.json.
The same API is served as an MCP server at https://mcp.graphsheets.ai/mcp, signed in the same way. Its server card is at https://mcp.graphsheets.ai/mcp/server-card, listed in the AI Catalog at /.well-known/ai-catalog.json.
The same connection answers the Agent2Agent protocol (1.0, JSON-RPC) at https://mcp.graphsheets.ai/a2a;
its agent card is at /.well-known/agent-card.json. Each skill is
one of the connector's read-only tools, asked for with a data part {"skill": "read-values", "arguments": {…}}
and answered at once with a message.
| Method | Path | What it does | Inputs (* required) | Scope |
|---|---|---|---|---|
GET | /api/v1/me | The person the token acts for | — | models:read |
GET | /api/v1/models | The models the person can open | ?archived ?scope | models:read |
GET | /api/v1/models/{uid} | A model's document at its head | ?version ?structure | models:read |
GET | /api/v1/models/{uid}/bundle | A model as a bundle of files | ?version | models:read |
GET | /api/v1/models/{uid}/search | Find rows in a model by name or title | ?q | models:read |
GET | /api/v1/models/{uid}/graph | A model's dependency graph | — | models:read |
GET | /api/v1/models/{uid}/history | A model's versions, newest first | ?limit ?authorType ?node ?review | models:read |
GET | /api/v1/models/{uid}/diff | The change between two versions of a model | ?from* ?to* | models:read |
GET | /api/v1/models/{uid}/values | Evaluated values of a model's rows | ?scenario ?nodes ?from ?to ?version | models:read |
GET | /api/v1/models/{uid}/explain | How a value is worked out, step by step | ?node* ?period* ?scenario ?depth | models:read |
POST | /api/v1/models/{uid}/verify | Run a model's checks and lint | JSON body (optional): ops | models:read |
POST | /api/v1/models/{uid}/sensitivity | How outputs move with inputs | JSON body: overrides* scenario outputs ops base | models:read |
POST | /api/v1/models/{uid}/risk | A Monte Carlo run over a model's uncertain inputs | ?scenario ?versionJSON body (optional): ops outputs | models:read |
GET | /api/v1/compare | Compare two models | ?left* ?right* | models:read |
GET | /api/v1/changesets/{id} | A change-set and its review | ?rebase | models:read |
GET | /api/v1/models/{uid}/agent-draft | The connection's working draft of a model | — | models:read |
GET | /api/v1/models/{uid}/private-scenarios | The person's own private scenarios on a model | — | models:read |
PUT | /api/v1/models/{uid}/private-scenarios/{name} | Keep one of the person's private scenarios | JSON body: scenario* inputs* constants* | models:read |
DELETE | /api/v1/models/{uid}/private-scenarios/{name} | Delete one of the person's private scenarios | — | models:read |
POST | /api/v1/models | Create an empty model | JSON body: title description granularity start periods document bundle policy asCopy | models:write |
POST | /api/v1/models/{uid}/clone | Clone a model | ?versionJSON body: title* | models:write |
POST | /api/v1/models/{uid}/scenarios/fork | Fork a scenario | JSON body: name* from title | models:write |
POST | /api/v1/models/{uid}/changesets | Propose a change-set, landed marked for review | JSON body: baseHash* message* ops* lintReasons suggested requestReview agent supersedes parts | models:write |
POST | /api/v1/changesets/{id}/apply | Apply a change-set | — | models:write |
POST | /api/v1/changesets/{id}/review | Approve or reject the connection's own proposal | JSON body: decision* comment | changes:approve |
PUT | /api/v1/models/{uid}/agent-draft | Save the connection's working draft | JSON body: baseHash* ops* revision* | models:write |
DELETE | /api/v1/models/{uid}/agent-draft | Discard the connection's working draft | — | models:write |
GET | /api/v1/market-data/providers | The market-data providers | — | data:fetch |
GET | /api/v1/market-data/search | Search for a priced symbol | ?provider* ?q | data:fetch |
GET | /api/v1/market-data/history | A symbol's price history | ?provider* ?symbol* ?from* ?to* | data:fetch |
GET | /api/v1/financials/providers | The company-filings providers | — | data:fetch |
GET | /api/v1/financials/search | Search for a reporting company | ?provider* ?q | data:fetch |
GET | /api/v1/financials/facts | A company's reported statement lines | ?provider* ?symbol* ?metric* ?frame* ?from* ?to* ?deriveQ4 ?deriveH2 | data:fetch |
GET | /api/v1/me/credit | The account's prepaid credit: balance, held, storage reserve, free | — | models:read |
POST | /api/v1/billing/topup | Top up the account's credit in-band: answers 402 with an MPP/x402 challenge, then settles a credential sent in Payment-Authorization | JSON body: dollars* | models:read |
GET | /api/v1/models/{uid}/shares | Who a model is shared with (agent accounts) | — | models:read |
POST | /api/v1/models/{uid}/shares | Share a model with a person, by address (agent accounts) | JSON body: email* role | models:write |
DELETE | /api/v1/models/{uid}/shares/{userId} | Stop sharing a model with a person (agent accounts) | — | models:write |
PATCH | /api/v1/models/{uid}/owner | Hand a model over to another account (agent accounts) | JSON body: email* | models:write |
PATCH | /api/v1/models/{uid}/organisation | Move a model into one of your organisations, or out (agent accounts) | JSON body (optional): orgId share restricted | models:write |
PATCH | /api/v1/models/{uid} | Archive or unarchive a model (agent accounts) | JSON body: archived* | models:write |
DELETE | /api/v1/models/{uid} | Delete a model (restorable for thirty days) (agent accounts) | — | models:write |
GET | /api/v1/orgs | Your organisations (agent accounts) | — | models:read |
GET | /api/v1/orgs/{id} | An organisation and its members (agent accounts) | — | models:read |
PUT | /api/v1/orgs/default | Choose which organisation new models go to (agent accounts) | JSON body: orgId* | models:write |
POST | /api/v1/orgs | Start an organisation (agent accounts) | JSON body: name* | models:write |
PATCH | /api/v1/orgs/{id} | Rename an organisation or change its settings (agent accounts) | JSON body (optional): name restrictByDefault memberInvitesNeedApproval agentJoinsNeedApproval | models:write |
POST | /api/v1/orgs/{id}/members | Invite somebody to an organisation (a member's invitation may wait for an administrator) (agent accounts) | JSON body: email* role | models:write |
PATCH | /api/v1/orgs/{id}/members/{userId} | Change a member's role (agent accounts) | JSON body: role* | models:write |
DELETE | /api/v1/orgs/{id}/members/{userId} | Remove a member, or leave (agent accounts) | — | models:write |
DELETE | /api/v1/orgs/{id}/invitations/{email} | Withdraw an organisation's invitation (agent accounts) | — | models:write |
PUT | /api/v1/orgs/{id}/house-style | Set an organisation's house style for documents (agent accounts) | JSON body: houseStyle* | models:write |
POST | /api/v1/orgs/{id}/join | Join an organisation that claimed your verified contact address's domain; 202 when the join waits for an administrator (agent accounts) | — | models:write |
POST | /api/v1/billing/checkout | Top up by card: answers a Stripe Checkout url for whoever pays for the account to open; the credit lands when Stripe confirms the payment (agent accounts) | JSON body: dollars* saveCard returnTo autoTopUp | models:read |
GET | /api/v1/users/{id}/profile | An account's profile page, where its owner lets you see it: who it is and the models of theirs you may open (404 where withheld) (agent accounts) | — | models:read |
PUT | /api/v1/me/profile | Set who may see your own profile page, and its picture (agent accounts) | JSON body: visibility avatar | models:write |
GET | /api/v1/me/keys | The public keys you sign in with (agent accounts) | — | models:read |
PUT | /api/v1/me/keys | Replace your keys: the new JWK Set, and an assertion signed with a key you hold now, its aud this route's address. Your owner, your own inbox and your webhook are told (agent accounts) | JSON body: jwks* assertion* | models:write |
POST | /api/v1/agents/register | Ask for an agent account: its name, its purpose, its public keys, acceptTerms: true, and at least one of ownerEmail (who answers for it), agentEmail (its own inbox) or webhookUrl. Answers the client id it will sign in as, where to ask how the request stands, and approved where the workspace needs no approval for agents and it can already be reached | JSON body: name* acceptTerms* purpose* jwks* ownerEmail agentEmail contactEmail webhookUrl | none |
GET | /api/v1/agents/registrations/{id} | Where an agent's request stands: awaiting_approval, approved, declined, suspended (by the administrator) or blocked (by its owner); whether a contact it gave has confirmed, and whether it can be reached yet (approval waits for that) | — | none |
GET | /api/v1/access | Who may come in without an administrator's approval: people and agents, each open (a verified address, or a reachable agent account, is admitted at once) or approval (an invitation, or the administrator's yes) | — | none |
GET | /api/v1/schemas/model.json | The model document's JSON Schema | — | none |
GET | /api/v1/billing/prices | The price list: model list prices per million tokens and the premium discount on them, model markups by payer, the engine price list (per call, per 10,000 risk trials, per export, per data pull) and what is never metered, the people's free allowance, their starter credit and free first Builder turn, and the trial credit | — | none |
Errors answer {"error": {"code", "message", "details"}}. Health: /api/v1/health.