How MCP clients and API callers authenticate with Savi Finance: OAuth 2.1 with PKCE, dynamic client registration, refresh tokens, scopes and bearer tokens.
Export this page as Markdown or bundle it with a task for your agent.
Preview the prompt that will be copied
Which credentials do I need?
| You are building | Use | What you provide |
|---|---|---|
| An AI assistant or any MCP client | The MCP server with OAuth 2.1 | Nothing to copy. The client opens a Savi sign-in page and the user approves access. |
| A script or app that calls the JSON-RPC API | The JSON-RPC API with a bearer token | A token from the two-call email sign-in below. |
Get an API token
Savi signs you in with an emailed 6-digit code, so there is no password. Two calls give you a bearer token, and a third confirms it works. Each panel below can run against the live API from this page, or you can copy a snippet in any of the ten supported languages.
api-v2.financesavi.com. Reloading clears it. MCP OAuth tokens and API tokens are not interchangeable.Sign in and make your first read
This form calls the live API. It sends an email only after you confirm and press Request code. Your token stays in memory and clears on reload.
- 1. Request code
- 2. Sign in
- 3. Verify token
Raw API sign-in requests and examples
1. Request a sign-in code
Enter your email in the request params. Savi emails you a 6-digit code. Codes can be requested once per minute per address.
https://api-v2.financesavi.com/rpcPubliccurl -sS --max-time 30 -X POST 'https://api-v2.financesavi.com/rpc' \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"sendLoginCodeEmail","params":{"email":"YOUR_EMAIL_ADDRESS","loginCodeType":"SHORT_LIVED","emailType":"LOGIN_CODE"},"id":1}'Captured response
$ POST https://api-v2.financesavi.com/rpc (sendLoginCodeEmail)
HTTP 200
{
"jsonrpc": "2.0",
"result": {
"message": "ok"
},
"id": 1
}Captured from the local sf1/api service and scrubbed. This preview does not send a request.
2. Exchange the code for a token
Enter the code from the email. On success the response contains your token and userId. This page stores both for you; in your own code, save the token.
https://api-v2.financesavi.com/rpcPubliccurl -sS --max-time 30 -X POST 'https://api-v2.financesavi.com/rpc' \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"loginByEmailVerification","params":{"email":"YOUR_EMAIL_ADDRESS","code":"123456","authenticationMethod":"EMAIL_CODE"},"id":1}'Captured response
$ POST https://api-v2.financesavi.com/rpc (loginByEmailVerification)
HTTP 200
{
"jsonrpc": "2.0",
"result": {
"token": "<ACCESS_TOKEN>",
"userId": "user_8f3a1c2d9b"
},
"id": 1
}Captured from the local sf1/api service and scrubbed. This preview does not send a request.
3. Confirm the token works
Send the token as Authorization: Bearer <token>. A good token returns your profile:
https://api-v2.financesavi.com/rpcRequires tokencurl -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":"getCurrentUser","params":{},"id":1}'The snippets read your token from the SAVI_TOKEN environment variable, so it is never pasted into code. Run export SAVI_TOKEN=<your token> first.
No token yet? Get one in two calls. The token stays in this tab and is only sent to api-v2.financesavi.com. Using it sends a read-only getCurrentUser request to find your user id.
Captured response
$ POST https://api-v2.financesavi.com/rpc (getCurrentUser)
HTTP 200
{
"jsonrpc": "2.0",
"result": {
"credentials": {
"id": "uc_1790000000000000000",
"email": "[email protected]",
"username": "u_1790000000000000000",
"isVerified": false,
"lastLoginByEmailCodeType": "SHORT_LIVED",
"createdAt": "2026-10-01T15:48:48.426Z"
},
"users": [
{
"id": "user_8f3a1c2d9b",
"email": "[email protected]",
"userType": "PERSONAL",
"username": "u_1790000000000000000",
"createdAt": "2026-10-01T15:48:48.428Z",
"businessTier": "FREE",
"referralId": "Ab12Cd34",
"referralLink": "https://app.financesavi.com/refer/Ab12Cd34",
"referralCount": 0,
"hasRedeemedReferral": false,
"totalStorageUsedInBytes": 0,
"storageLimitInBytes": 1000000000,
"numFiles": 0,
"widgetIds": [],
"notificationSettings": {
"insightSummary": {
"enabled": true
}
}
}
]
},
"id": 1
}Captured from the local sf1/api service and scrubbed. This preview does not send a request.
Use the token in your own code
# Save the token once, then reuse it in every request.
export SAVI_TOKEN='<paste the token from step 2>'
curl -sS -X POST https://api-v2.financesavi.com/rpc \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $SAVI_TOKEN" \
-d '{"jsonrpc":"2.0","method":"getCurrentUser","params":{},"id":1}'Use the explicit Copy token control after sign-in; live response previews redact it. Store credentials securely in your own application. The browser helper is for manual exploration, and does not persist a session or provide a sandbox. Without a bearer token, production getCurrentUser returned the response below. If your request returns this code, sign in again to get a token:
Production response to getCurrentUser without a bearer token. Captured at .
{
"jsonrpc": "2.0",
"method": "getCurrentUser",
"params": {},
"id": 4
}{
"jsonrpc": "2.0",
"error": {
"code": -32002,
"message": "missing or invalid auth token"
},
"id": 4
}MCP: OAuth 2.1 flow
The MCP server at https://mcp.financesavi.com follows the MCP authorization specification. Compliant clients complete these steps automatically:
- Request
/.well-known/oauth-protected-resourceto find the authorization server. - Request
/.well-known/oauth-authorization-serverfor endpoint metadata. - Register with
POST /register(client_name,redirect_uris) to receive aclient_id. Clients are public: there is no client secret, sotoken_endpoint_auth_methodmust benone. - Open
/authorizein a browser withresponse_type=code,client_id,redirect_uri,stateand a PKCEcode_challengewithcode_challenge_method=S256. Optionally passscopeandresource. - The user enters their email, then the 6-digit code Savi sends to it.
- The user sees a consent page that names the app, where they will be sent back to and what it is asking to do, and approves or denies. Approving without Allow changes grants read-only access. For apps that registered themselves through
/register, Allow changes starts unticked, so the user has to opt in to write access. - Savi redirects to the
redirect_uriwith a single-use authorizationcode, valid for about 60 seconds. If the user denies, it redirects witherror=access_denied. - Exchange the code at
POST /tokenwithgrant_type=authorization_code,code,client_id,redirect_uriandcode_verifier. The response contains anaccess_tokenof typeBearer,expires_in, arefresh_tokenand the grantedscope. - Call
https://mcp.financesavi.com/mcpwithAuthorization: Bearer <access_token>.
PKCE with S256 is required. A 401 response includes a WWW-Authenticate header with a resource_metadata parameter that points clients at the discovery document.
Registering a client
Redirect URIs must be absolute and must be one of: an https URL, an http URL on localhost or another loopback address, or a custom app scheme such as myapp://callback. URIs with a fragment, embedded credentials or a wildcard are rejected, as are schemes that can run code (javascript:, data:, file:). A client can register up to 10 redirect URIs, and the redirect_uri sent to /authorize must match one exactly. Supported grant types are authorization_code and refresh_token. The consent page notes that Savi has not reviewed clients registered this way.
Token lifetime, refresh and revocation
- Access tokens last 1 hour. When one expires the server answers
401and the client should refresh. - Refresh tokens last 30 days and rotate. Send
POST /tokenwithgrant_type=refresh_token,refresh_tokenandclient_idto receive a new access token and a new refresh token. A refresh request can narrowscopebut never widen it. - Refreshing never extends a sign-in past 90 days from its first token: tokens issued near the end expire at that limit, and after it the user must sign in again.
- Using a refresh token or an authorization code a second time is treated as theft: every token from that sign-in is revoked and the user must sign in again.
POST /revokewithtoken(an access or refresh token) revokes the sign-in. It always answers200, whether or not the token existed.- If you send the
resourceparameter it must behttps://mcp.financesavi.comorhttps://mcp.financesavi.com/mcp; anything else is rejected withinvalid_target.
MCP scopes
| Scope | Grants |
|---|---|
read | Every read-only tool. |
write | Every tool that creates, changes or deletes data. These tools also need read, so a request for write alone is granted read write. |
read:profile, read:accounts, read:transactions, read:categories, read:tags, read:analytics | A single read tool each. |
mcp | All tools. Used when a client requests no scope. |
The user can always lower the grant on the consent page by unticking Allow changes. A tool call without the required scope returns a tool result with isError set and an insufficient scope message. See the tool reference for the scope each tool needs.
JSON-RPC API
Methods marked required in the API reference need an Authorization: Bearer <token> header on requests to https://api-v2.financesavi.com/rpc. Methods marked public need no token. MCP access tokens work only on the MCP server and cannot be used with the JSON-RPC API.