Skip to main content

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 is key.user in GET /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_found in a path, and 400 invalid_parameter in a filter.
  • An id of something you can't see gets 404 not_found in a path, and an empty list in a filter.

Dates and months​

KindFormatExampleWhere
TimestampRFC 33392026-03-01T00:00:00Zin responses: start_at, end_at, created_at
DayYYYY-MM-DD2026-03-31the from and to filters of expenses
MonthYYYY-MM2026-03monthly 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:

  • amount and currency: the amount of the expense itself, in its own currency.
  • budget_amount and budget_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"
}
  • limit sets how many items a page holds: from 1 to 200, 50 by default.
  • To get the next page, send the same request with cursor set to the next_cursor you got.
  • next_cursor is null on 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.