Agent API reference
Saivify exposes your household ledger to the AI agent you already use through one Model Context Protocol endpoint. Every call runs as you, sees only what you can see in the app, and is recorded in the group's audit log. This page is generated from the same contract the server uses, so it lists exactly the tools the endpoint has.
Endpoint
One endpoint, POST /api/mcp, speaking JSON-RPC 2.0 over Streamable HTTP: initialize, tools/list and tools/call. Present your credential as Authorization: Bearer <token>. The handshake below returns the contract version beside the protocol version.
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18"
}
}Quickstart
Connect Claude Desktop or Claude Code to https://saivify.shiftclawco.com/api/mcp, or try the endpoint with three curl calls. Configuration and curl only: there is no SDK to install.
Get a credential
- OAuth 2.1. Connect from an MCP client through the consent screen: OAuth 2.1 with PKCE through WorkOS AuthKit. The client starts at /api/mcp/authorize and receives its access token at /api/mcp/callback.
- Personal token. In Settings › Agents, create a connection with at least read:accounts. The token is shown exactly once, right after you create it: copy it then, it cannot be shown again. Below, replace
<your-personal-token>with it.
One curl round trip
The handshake, the tool list, then one list_accounts call. Each answers 200 with the JSON-RPC result shown under it.
export SAIVIFY_TOKEN='<your-personal-token>'
# 1. initialize
curl -sS https://saivify.shiftclawco.com/api/mcp \
-H "Authorization: Bearer $SAIVIFY_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}'
# 2. tools/list
curl -sS https://saivify.shiftclawco.com/api/mcp \
-H "Authorization: Bearer $SAIVIFY_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
# 3. tools/call list_accounts
curl -sS https://saivify.shiftclawco.com/api/mcp \
-H "Authorization: Bearer $SAIVIFY_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_accounts","arguments":{}}}'Expected response: 1. initialize
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": {
"listChanged": false
}
},
"serverInfo": {
"name": "saivify",
"version": "2026-09-01"
},
"contractVersion": "2026-09-01"
}
}Expected response: 2. tools/list
Abridged: every entry also carries its description, inputSchema and outputSchema.
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "list_accounts",
"requiredScope": "read:accounts"
},
{
"name": "search_transactions",
"requiredScope": "read:transactions"
},
{
"name": "get_forecast",
"requiredScope": "read:transactions"
},
{
"name": "create_transaction",
"requiredScope": "write:transactions"
},
{
"name": "set_tags",
"requiredScope": "write:tags"
}
]
}
}Expected response: 3. tools/call list_accounts
One entry per account you can see in the active group; ended and unshared accounts are absent.
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "{\"accounts\":[{\"publicRef\":\"<account-ref>\",\"label\":\"<account label>\",\"kind\":\"checking\",\"currency\":\"EUR\",\"balance\":\"<balance>\"}]}"
}
],
"structuredContent": {
"accounts": [
{
"publicRef": "<account-ref>",
"label": "<account label>",
"kind": "checking",
"currency": "EUR",
"balance": "<balance>"
}
]
},
"isError": false
}
}Leave optional arguments out
Every nullable argument may be omitted and then means null. A search needs only limit, and a forecast needs no argument at all. A wrong type or an unknown argument is still refused with validation_failed, naming the argument.
# tools/call search_transactions {"limit":5}
curl -sS https://saivify.shiftclawco.com/api/mcp \
-H "Authorization: Bearer $SAIVIFY_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"search_transactions","arguments":{"limit":5}}}'
# tools/call get_forecast {}
curl -sS https://saivify.shiftclawco.com/api/mcp \
-H "Authorization: Bearer $SAIVIFY_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"get_forecast","arguments":{}}}'Expected response: tools/call search_transactions {"limit":5}
Abridged: up to five movements, newest first. Follow nextCursor until it is null.
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"content": [
{
"type": "text",
"text": "{\"movements\":[{\"movementId\":\"<movement-ref>\",\"accountRef\":\"<account-ref>\",\"date\":\"<YYYY-MM-DD>\",\"amount\":-12.5,\"currency\":\"EUR\",\"merchant\":\"<merchant>\",\"description\":null,\"tagIds\":[\"<tag-ref>\"],\"tags\":[{\"ref\":\"<tag-ref>\",\"name\":\"<tag name>\"}],\"forMember\":null}],\"nextCursor\":null}"
}
],
"structuredContent": {
"movements": [
{
"movementId": "<movement-ref>",
"accountRef": "<account-ref>",
"date": "<YYYY-MM-DD>",
"amount": -12.5,
"currency": "EUR",
"merchant": "<merchant>",
"description": null,
"tagIds": [
"<tag-ref>"
],
"tags": [
{
"ref": "<tag-ref>",
"name": "<tag name>"
}
],
"forMember": null
}
],
"nextCursor": null
},
"isError": false
}
}Expected response: tools/call get_forecast {}
Abridged: one band per projected month over the default horizon.
{
"jsonrpc": "2.0",
"id": 5,
"result": {
"content": [
{
"type": "text",
"text": "{\"currency\":\"EUR\",\"minimumBalance\":0,\"minimumMonth\":\"<YYYY-MM>\",\"monthsUnderBuffer\":0,\"bands\":[{\"month\":\"<YYYY-MM>\",\"low\":0,\"expected\":0,\"high\":0}],\"basis\":\"<how the projection was computed>\"}"
}
],
"structuredContent": {
"currency": "EUR",
"minimumBalance": 0,
"minimumMonth": "<YYYY-MM>",
"monthsUnderBuffer": 0,
"bands": [
{
"month": "<YYYY-MM>",
"low": 0,
"expected": 0,
"high": 0
}
],
"basis": "<how the projection was computed>"
},
"isError": false
}
}What a failure looks like
Every failure is the same envelope with a stable code. Quote the requestId when you contact support.
Tools
5 tools. Read tools never change data; write tools run the same checks as the app. No tool can delete an account or a group, remove a member, revoke a share or export raw statements.
Scopes
Every credential carries the scopes chosen when it is created. A call to a tool outside them is refused with 403 before its arguments are read.
Transports
Two ways to obtain a credential, one credential underneath: whichever you use, the agent presents it as a Bearer header to POST /api/mcp over Streamable HTTP.
Error codes
Every refusal is the same envelope: a stable code to branch on, a human message and a request id. Branch on the code, never on the message.
{
"code": "validation_failed",
"message": "That request could not be processed.",
"requestId": "req_example0001"
}Present on validation_failed when a tool argument fails its schema: which argument, and why. The argument value is never echoed.
{
"code": "validation_failed",
"message": "to has an invalid format.",
"requestId": "req_example0001",
"details": {
"field": "to",
"reason": "format"
}
}details.field— JSON path of the offending argument, dot-joined (e.g. `to`, `tagIds.2`); `arguments` for the whole object.details.reason— Stable reason token: one of type, required, format, range, enum.
Versioning and deprecation
The contract is versioned by date. The current version is 2026-09-01, and every initialize response carries it as contractVersion, so your agent can tell which contract it is talking to.