# Savi developer documentation Build on Savi Finance. Connect AI assistants with the Savi MCP server or call the Savi JSON-RPC API. Savi securely connects account data to AI clients through MCP and to apps through the JSON-RPC API. YOUR DATA**Accounts & activity** SAVI AI CLIENTS**MCP server** YOUR APPS**JSON-RPC API** Last updated October 1, 2026 [**Savi MCP server**Connect Claude, ChatGPT, Gemini and other MCP clients to your Savi data. 71 tools.](https://financesavi.com/developers/mcp)[**Authentication**OAuth 2.1 with PKCE for MCP clients and bearer tokens for the API.](https://financesavi.com/developers/authentication)[**JSON-RPC API**21 documented methods at `https://api-v2.financesavi.com/rpc`.](https://financesavi.com/developers/api) ## Quickstart: MCP Add `https://mcp.financesavi.com/mcp` as a remote MCP server in any client that supports the Model Context Protocol. The client discovers the authorization server, registers itself, and sends you to Savi to sign in with an emailed code. No token to copy. Pick your client: ### Claude Code ```bash claude mcp add --transport http savi https://mcp.financesavi.com/mcp ``` ### Gemini CLI ```bash gemini mcp add --scope user --transport http savi https://mcp.financesavi.com/mcp # Then inside Gemini CLI, sign in through Savi: # /mcp auth savi ``` ### ChatGPT ```text Settings → Connectors → Advanced → Developer mode, then Create: Name: Savi Server URL: https://mcp.financesavi.com/mcp Auth: OAuth ``` ### Cursor ```json { "mcpServers": { "savi": { "url": "https://mcp.financesavi.com/mcp" } } } ``` ### VS Code ```json { "servers": { "savi": { "type": "http", "url": "https://mcp.financesavi.com/mcp" } } } ``` ### Claude Desktop bridge ```json { "mcpServers": { "savi": { "command": "npx", "args": [ "-y", "mcp-remote", "https://mcp.financesavi.com/mcp" ] } } } ``` ### MCP Inspector ```bash npx @modelcontextprotocol/inspector --server-url https://mcp.financesavi.com/mcp --transport http # Open the authenticated local URL printed by Inspector, then complete OAuth. ``` ### Other clients ```text Server URL: https://mcp.financesavi.com/mcp Transport: Streamable HTTP Auth: OAuth 2.1 with PKCE (discovered automatically) ``` More in [the MCP guide](https://financesavi.com/developers/mcp). ## Quickstart: API 1. **Check connectivity.** `ping` is public, so it needs no token. Press _Send live request_ to call the live API, or copy the snippet. ## ping request POST https://api-v2.financesavi.com/rpc · Public ### cURL ```bash curl -sS --max-time 30 -X POST 'https://api-v2.financesavi.com/rpc' \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","method":"ping","params":{},"id":1}' ``` ### JavaScript ```javascript const response = await fetch("https://api-v2.financesavi.com/rpc", { method: 'POST', signal: AbortSignal.timeout(30_000), headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ "jsonrpc": "2.0", "method": "ping", "params": {}, "id": 1 }), }); console.log(await response.json()); ``` ### TypeScript ```typescript const response = await fetch("https://api-v2.financesavi.com/rpc", { method: 'POST', signal: AbortSignal.timeout(30_000), headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ "jsonrpc": "2.0", "method": "ping", "params": {}, "id": 1 }), }); console.log(await response.json()); ``` ### Python ```python import os import requests response = requests.post( "https://api-v2.financesavi.com/rpc", json={ "jsonrpc": "2.0", "method": "ping", "params": {}, "id": 1, }, timeout=30, ) print(response.json()) ``` ### Go ```go package main import ( "fmt" "io" "net/http" "strings" "time" ) func main() { payload := strings.NewReader("{\"jsonrpc\":\"2.0\",\"method\":\"ping\",\"params\":{},\"id\":1}") req, err := http.NewRequest("POST", "https://api-v2.financesavi.com/rpc", payload) if err != nil { panic(err) } req.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 30 * time.Second} res, err := client.Do(req) if err != nil { panic(err) } defer res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { panic(err) } fmt.Println(res.Status, string(body)) } ``` ### C# ```csharp // .NET 6 or later using System; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) }; using var content = new StringContent(@"{""jsonrpc"":""2.0"",""method"":""ping"",""params"":{},""id"":1}", Encoding.UTF8, "application/json"); using var response = await client.PostAsync(@"https://api-v2.financesavi.com/rpc", content); Console.WriteLine($"HTTP {(int)response.StatusCode}"); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ### Java ```java // Java 11+: save as SaviExample.java, then run java SaviExample.java import java.net.URI; import java.net.http.*; import java.time.Duration; class SaviExample { public static void main(String[] args) throws Exception { var client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build(); var builder = HttpRequest.newBuilder(URI.create("https://api-v2.financesavi.com/rpc")) .timeout(Duration.ofSeconds(30)) .header("Content-Type", "application/json"); var request = builder.POST(HttpRequest.BodyPublishers.ofString("{\"jsonrpc\":\"2.0\",\"method\":\"ping\",\"params\":{},\"id\":1}")).build(); var response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("HTTP " + response.statusCode()); System.out.println(response.body()); } } ``` ### PHP ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 30, CURLOPT_TIMEOUT => 30, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => '{"jsonrpc":"2.0","method":"ping","params":{},"id":1}', ]); $body = curl_exec($curl); if ($body === false) { throw new RuntimeException(curl_error($curl)); } echo "HTTP " . curl_getinfo($curl, CURLINFO_HTTP_CODE) . PHP_EOL; echo $body . PHP_EOL; curl_close($curl); ``` ### Ruby ```ruby require 'net/http' require 'uri' uri = URI('https://api-v2.financesavi.com/rpc') request = Net::HTTP::Post.new(uri) request['Content-Type'] = 'application/json' request.body = '{"jsonrpc":"2.0","method":"ping","params":{},"id":1}' response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', open_timeout: 30, read_timeout: 30, write_timeout: 30 ) { |http| http.request(request) } puts "HTTP #{response.code}" puts response.body ``` ### Swift ```swift // Swift 5.5+: save as SaviExample.swift, then run swift SaviExample.swift import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif var request = URLRequest(url: URL(string: "https://api-v2.financesavi.com/rpc")!) request.httpMethod = "POST" request.timeoutInterval = 30 request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = Data("{\"jsonrpc\":\"2.0\",\"method\":\"ping\",\"params\":{},\"id\":1}".utf8) let configuration = URLSessionConfiguration.ephemeral configuration.timeoutIntervalForResource = 30 let session = URLSession(configuration: configuration) let (data, response) = try await session.data(for: request) if let http = response as? HTTPURLResponse { print("HTTP", http.statusCode) } print(String(decoding: data, as: UTF8.self)) ``` ### Production capture (HTTP 200) ```json { "jsonrpc": "2.0", "result": { "reply": "pong" }, "id": 1 } ``` 2. **Get a token.** Two calls: request an emailed code, then exchange it. Follow [Get an API token](https://financesavi.com/developers/authentication#get-a-token). 3. **Call an authenticated method.** With a token saved in this tab, this panel lists your most recent transactions: ## listTransactions request POST https://api-v2.financesavi.com/rpc · Bearer token required ### cURL ```bash curl -sS --max-time 30 -X POST 'https://api-v2.financesavi.com/rpc' \ -H 'Content-Type: application/json' \ -H "Authorization: Bearer $SAVI_TOKEN" \ -d '{"jsonrpc":"2.0","method":"listTransactions","params":{"pagination":{"limit":10,"skip":0}},"id":1}' ``` ### JavaScript ```javascript const response = await fetch("https://api-v2.financesavi.com/rpc", { method: 'POST', signal: AbortSignal.timeout(30_000), headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.SAVI_TOKEN}`, }, body: JSON.stringify({ "jsonrpc": "2.0", "method": "listTransactions", "params": { "pagination": { "limit": 10, "skip": 0 } }, "id": 1 }), }); console.log(await response.json()); ``` ### TypeScript ```typescript const response = await fetch("https://api-v2.financesavi.com/rpc", { method: 'POST', signal: AbortSignal.timeout(30_000), headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.SAVI_TOKEN}`, }, body: JSON.stringify({ "jsonrpc": "2.0", "method": "listTransactions", "params": { "pagination": { "limit": 10, "skip": 0 } }, "id": 1 }), }); console.log(await response.json()); ``` ### Python ```python import os import requests response = requests.post( "https://api-v2.financesavi.com/rpc", headers={"Authorization": f"Bearer {os.environ['SAVI_TOKEN']}"}, json={ "jsonrpc": "2.0", "method": "listTransactions", "params": { "pagination": { "limit": 10, "skip": 0, }, }, "id": 1, }, timeout=30, ) print(response.json()) ``` ### Go ```go package main import ( "fmt" "io" "net/http" "os" "strings" "time" ) func main() { payload := strings.NewReader("{\"jsonrpc\":\"2.0\",\"method\":\"listTransactions\",\"params\":{\"pagination\":{\"limit\":10,\"skip\":0}},\"id\":1}") req, err := http.NewRequest("POST", "https://api-v2.financesavi.com/rpc", payload) if err != nil { panic(err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer " + os.Getenv("SAVI_TOKEN")) client := &http.Client{Timeout: 30 * time.Second} res, err := client.Do(req) if err != nil { panic(err) } defer res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { panic(err) } fmt.Println(res.Status, string(body)) } ``` ### C# ```csharp // .NET 6 or later using System; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("SAVI_TOKEN")); using var content = new StringContent(@"{""jsonrpc"":""2.0"",""method"":""listTransactions"",""params"":{""pagination"":{""limit"":10,""skip"":0}},""id"":1}", Encoding.UTF8, "application/json"); using var response = await client.PostAsync(@"https://api-v2.financesavi.com/rpc", content); Console.WriteLine($"HTTP {(int)response.StatusCode}"); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` ### Java ```java // Java 11+: save as SaviExample.java, then run java SaviExample.java import java.net.URI; import java.net.http.*; import java.time.Duration; class SaviExample { public static void main(String[] args) throws Exception { var client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build(); var builder = HttpRequest.newBuilder(URI.create("https://api-v2.financesavi.com/rpc")) .timeout(Duration.ofSeconds(30)) .header("Content-Type", "application/json"); builder.header("Authorization", "Bearer " + System.getenv("SAVI_TOKEN")); var request = builder.POST(HttpRequest.BodyPublishers.ofString("{\"jsonrpc\":\"2.0\",\"method\":\"listTransactions\",\"params\":{\"pagination\":{\"limit\":10,\"skip\":0}},\"id\":1}")).build(); var response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("HTTP " + response.statusCode()); System.out.println(response.body()); } } ``` ### PHP ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 30, CURLOPT_TIMEOUT => 30, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: Bearer ' . getenv('SAVI_TOKEN'), ], CURLOPT_POSTFIELDS => '{"jsonrpc":"2.0","method":"listTransactions","params":{"pagination":{"limit":10,"skip":0}},"id":1}', ]); $body = curl_exec($curl); if ($body === false) { throw new RuntimeException(curl_error($curl)); } echo "HTTP " . curl_getinfo($curl, CURLINFO_HTTP_CODE) . PHP_EOL; echo $body . PHP_EOL; curl_close($curl); ``` ### Ruby ```ruby require 'net/http' require 'uri' uri = URI('https://api-v2.financesavi.com/rpc') request = Net::HTTP::Post.new(uri) request['Content-Type'] = 'application/json' request['Authorization'] = 'Bearer ' + ENV.fetch('SAVI_TOKEN') request.body = '{"jsonrpc":"2.0","method":"listTransactions","params":{"pagination":{"limit":10,"skip":0}},"id":1}' response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', open_timeout: 30, read_timeout: 30, write_timeout: 30 ) { |http| http.request(request) } puts "HTTP #{response.code}" puts response.body ``` ### Swift ```swift // Swift 5.5+: save as SaviExample.swift, then run swift SaviExample.swift import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif var request = URLRequest(url: URL(string: "https://api-v2.financesavi.com/rpc")!) request.httpMethod = "POST" request.timeoutInterval = 30 request.setValue("application/json", forHTTPHeaderField: "Content-Type") guard let token = ProcessInfo.processInfo.environment["SAVI_TOKEN"] else { fatalError("Set SAVI_TOKEN first") } request.setValue("Bearer " + token, forHTTPHeaderField: "Authorization") request.httpBody = Data("{\"jsonrpc\":\"2.0\",\"method\":\"listTransactions\",\"params\":{\"pagination\":{\"limit\":10,\"skip\":0}},\"id\":1}".utf8) let configuration = URLSessionConfiguration.ephemeral configuration.timeoutIntervalForResource = 30 let session = URLSession(configuration: configuration) let (data, response) = try await session.data(for: request) if let http = response as? HTTPURLResponse { print("HTTP", http.statusCode) } print(String(decoding: data, as: UTF8.self)) ``` ### Local API capture (scrubbed) (HTTP 200) ```json { "jsonrpc": "2.0", "result": { "transactions": [ { "id": "tx_9bLm4Rt7Yc", "accountId": "account_5kQe2Xv9Lm", "name": "Coffee Shop", "amount": -450, "currency": "CAD", "source": "MANUAL", "category": "FOOD", "type": "EXPENSE", "categoryId": "cat_3nTq8Wd2Zs", "categoryDecision": { "source": "USER", "locked": true, "decidedAt": "2026-10-01T15:48:50.481Z", "lockedAt": "2026-10-01T15:48:50.481Z" }, "transactedAt": "2026-10-01T09:30:00Z", "createdAt": "2026-10-01T15:48:50.481Z", "updatedAt": "2026-10-01T15:48:50.481Z", "fileIds": [], "isApproved": true, "tagIds": [], "lineItems": [], "activityLog": [], "oneOffSplitParticipants": [], "netAmount": -450, "refundStatus": "NONE" } ], "numItems": 1 }, "id": 1 } ``` ## Choose your next step [**Verify an MCP connection**Sign in, discover tools, and make a first read-only call.](https://financesavi.com/developers/mcp#first-tool)[**Sign in to the API**Use the guided email-code flow and verify your token.](https://financesavi.com/developers/authentication#get-a-token)[**Recover from an error**Check transport, credentials, and the JSON-RPC result.](https://financesavi.com/developers/api#troubleshooting) Source: https://financesavi.com/developers