Conventions
Every route follows the same rules.
Base URL
https://app.spendbaker.com/sb_back/api/v1
Every path in these pages is relative to it: GET /budgets means
GET https://app.spendbaker.com/sb_back/api/v1/budgets.
Requests
- Every route is a
GET. Filters and options are query parameters. - HTTPS only.
- Send your key in
Authorization: Bearer gak_…(see Authentication).
Responses
- JSON, in UTF-8.
- Field names are in
snake_case. - A field with no value is
null: it is not left out. The one exception iskey.userinGET /organization. - Ignore the fields you don't know. v1 may add fields at any time.
- Enum fields, such as a budget's
status, list their values in these pages. Handle a value you don't know without failing.
Ids
Ids are opaque strings shaped like a UUID: 7d2f9a40-3c8e-4b16-a5d7-2e9c0f81b4a3.
- Use them as you get them. Don't parse them or build them.
- They are the same ids as in the app's URLs, so you can link to a budget in Spendbaker.
- An id that isn't one of ours gets
404 not_foundin a path, and400 invalid_parameterin a filter. - An id of something you can't see gets
404 not_foundin a path, and an empty list in a filter.
Dates and months
| Kind | Format | Example | Where |
|---|---|---|---|
| Timestamp | RFC 3339 | 2026-03-01T00:00:00Z | in responses: start_at, end_at, created_at |
| Day | YYYY-MM-DD | 2026-03-31 | the from and to filters of expenses |
| Month | YYYY-MM | 2026-03 | monthly breakdowns, and the month, from and to filters of budgets and categories |
Amounts and currencies
Amounts are decimal strings with two decimals, always next to a currency field, which holds an ISO 4217 code:
{ "amount": "1234.50", "currency": "EUR" }
Parse amounts with a decimal type, never a float, so cents stay exact. An amount may be negative.
An expense has two amounts:
amountandcurrency: the amount of the expense itself, in its own currency.budget_amountandbudget_currency: the same amount converted to the budget's currency. Budget totals sum this one.
Pagination
Routes that return a list return one page at a time:
{
"data": [ ... ],
"next_cursor": "N2QyZjlhNDAtM2M4ZS00YjE2LWE1ZDctMmU5YzBmODFiNGEz"
}
limitsets how many items a page holds: from 1 to 200, 50 by default.- To get the next page, send the same request with
cursorset to thenext_cursoryou got. next_cursorisnullon the last page.- Items come in a fixed order, which is neither by name nor by date: sort them yourself if you need to.
- Pages follow that order, so an item never shows up on two pages, even if data changes while you read.
- A cursor is opaque: don't build or change it.
To read every page:
import os
import requests
BASE_URL = "https://app.spendbaker.com/sb_back/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SPENDBAKER_KEY']}"}
def read_all(path, params=None):
"""Yields every item of a list route, page after page."""
params = dict(params or {}, limit=200)
while True:
page = requests.get(f"{BASE_URL}{path}", headers=HEADERS, params=params, timeout=30)
page.raise_for_status()
body = page.json()
yield from body["data"]
if body["next_cursor"] is None:
return
params["cursor"] = body["next_cursor"]
for expense in read_all("/expenses", {"status": "spent", "from": "2026-01-01"}):
print(expense["budget_name"], expense["amount"], expense["currency"])
Versioning
The version is in the path: /api/v1.
- v1 may gain routes, parameters, fields and error codes at any time.
- Nothing is removed or renamed in v1. A change that would break a client ships as
/api/v2, announced in advance.
See the changelog.