# Getting started > Sign in, plan your first month, and track an expense. Source: https://docs.spendrock.com/getting-started/ This page is a short outline for now. Step-by-step guides with screenshots are coming. ## 1. Sign in [#1-sign-in] SpendRock is **invite-only** for now. Sign in with the Google account that was invited (or that's on the list). Your first sign-in creates your own household, named after you, with an empty budget. ## 2. Plan the month [#2-plan-the-month] Open the current month and choose **Start Planning**. SpendRock copies your most recent budget, or starts you from a simple template if this is your first month. * Add your **income** for the month. * Add groups (like *Housing* or *Food*) and items (like *Rent* or *Groceries*) with a **planned** amount each. * Keep going until **Left to Budget** reaches zero: every dollar has a job. ## 3. Track what you spend [#3-track-what-you-spend] Add an expense or income with its amount, date, and merchant, and pick the budget item it belongs to. You can split one purchase across several items. Each item shows what you've **spent** and what's **remaining**. ## 4. Invite your household [#4-invite-your-household] Anyone in a household can invite someone by email from **Settings → Households**. Everyone in the household edits the same budget. ## What's next [#whats-next] More help is on the way. In the meantime, the [API docs](/api/) describe everything SpendRock can do, in detail. --- # Welcome > SpendRock is a small zero-based budgeting app for your household. Source: https://docs.spendrock.com/ The help section is just getting started. More pages (how budgeting works, funds, splits, households, saving while offline) are on the way. If something here is missing or wrong, tell us. SpendRock helps you **give every dollar a job**. Each month you plan where your income goes, then track what you actually spend against that plan. Everyone in your household sees and edits the same budget. Sign in, plan your first month, and add an expense. Build on SpendRock with a personal access token. ## Tips for reading these docs [#tips-for-reading-these-docs] * Every page is also available as Markdown: add `.md` to its address, or use **Copy Markdown** at the top of the page. * Use the search box (or press ⌘ K / Ctrl K) to find anything across Help and the API docs. --- # Authentication & scopes > Personal access tokens, the scopes they carry, and what they can't do. Source: https://docs.spendrock.com/api/authentication/ ## Personal access tokens [#personal-access-tokens] Scripts authenticate with a **personal access token** (PAT) in the `Authorization` header: ```http Authorization: Bearer srp_… ``` * Create, list, and revoke tokens in **Account → API tokens** ([open it](https://app.spendrock.com/account/api-tokens)). Each token has a name, optional expiry (30, 90, or 365 days, or never), and a set of scopes. * The secret is shown **once**, when you create the token. SpendRock stores only a hash of it. * The list shows each token's name, scopes, when it was created, when it was last used, and when it expires. * **A token acts as you.** It sees the households you belong to and can do what you can do there, limited by its scopes. * A revoked or expired token gets `401` with the code `unauthenticated`. The API also accepts the web app's session cookie, but only from the web app itself. Calls from anywhere else, including the playground in these docs, must use a bearer token. Cross-origin requests never send or accept cookies. ## Scopes [#scopes] Every operation needs one scope. The reference shows it on each endpoint (and the spec has it as `x-spendrock-scope`). | Scope | Allows | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `budget:read` | Read months and their groups, items, funds, and favorites. | | `budget:write` | Create and reset months; add, edit, reorder, and delete groups and items; funds, favorites order, and harvest. | | `transactions:read` | List and read transactions, and merchant suggestions. | | `transactions:write` | Add, edit, delete, and restore transactions. | | `households:read` | List households, members, and invites. | | `households:manage` | Create, rename, delete, archive, and leave households; set the default; transfer ownership; remove members; send, revoke, accept, and decline invites. | | `account:read` | Read your profile (`/me`), settings, and sign-in methods. | | `account:write` | Change your settings. | * A **write** scope includes its **read** scope (and `households:manage` includes `households:read`). * When creating a token, the presets are **Read only** (every `*:read` scope) and **Full access** (every scope). You can also pick scopes one by one. * A call outside the token's scopes gets `403` with the code `insufficient_scope`. The error names the scope it needs in `details.required_scope`. For example: ```json { "code": "insufficient_scope", "message": "This token doesn't have the transactions:write scope.", "details": { "required_scope": "transactions:write" } } ``` ## What tokens can't do [#what-tokens-cant-do] Some things need you, signed in, in a browser: * **Managing tokens.** A token can't create, list, or revoke tokens. * **Changing your password.** These return `403` with the code `session_required`. ## Sessions [#sessions] The web app (a cookie) and the mobile app (a bearer session token from signing in) use **sessions** instead of personal access tokens. Sessions have every scope and aren't subject to the per-token [rate limit](/api/rate-limits/). You don't need them for scripts; they're documented in the reference under **auth** for completeness. ## No authentication [#no-authentication] A few endpoints need no credentials: signing in, and looking up an invite by its link token. The spec itself is public too (`/openapi.json`, `/openapi.yaml`). --- # Errors > The error format, HTTP statuses, and every error code. Source: https://docs.spendrock.com/api/errors/ ## Format [#format] Every error response has a JSON body with a stable, machine-readable `code` and a human-readable `message`. Some add `details`. ```json { "code": "splits_do_not_sum", "message": "The splits add up to $50.00, but the amount is $54.20.", "details": { } } ``` * **Branch on `code`**, never on `message`. Codes are stable within `/v1`; messages may change. * `message` is safe to show to a user. * `details` is an object whose contents depend on the code (for example `details.required_scope` for `insufficient_scope`). Ignore keys you don't know. * New codes may be added in `/v1`. Treat an unknown code by its HTTP status. ## Statuses [#statuses] | Status | Meaning | | ------ | ---------------------------------------------------------------------------------------------------- | | `400` | The request didn't parse: malformed JSON, a wrong type, or a bad parameter (`invalid`). | | `401` | No valid credentials: missing, revoked, or expired token. | | `403` | Authenticated, but not allowed: missing scope, not a member, not the owner, and so on. | | `404` | No such thing (or no budget for that month yet). | | `409` | Conflicts with what exists: a name that's taken, a month that exists, a reused transaction ID. | | `410` | A link that was valid but isn't any more (an expired or used invite or email link). | | `422` | Well-formed, but breaks a budgeting rule (the splits don't add up, the item isn't in that month, …). | | `429` | Too many requests. See [Rate limits](/api/rate-limits/). | | `500` | Something went wrong on our side (`internal`). It's reported to us automatically; retry later. | ## Codes [#codes] ### Authentication and access [#authentication-and-access] | Code | Status | Meaning | | -------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------- | | `unauthenticated` | 401 | No credentials, or the token or session is revoked or expired. | | `insufficient_scope` | 403 | The token lacks the scope this operation needs; `details.required_scope` names it. See [scopes](/api/authentication/#scopes). | | `session_required` | 403 | Only a signed-in session can do this (managing tokens, changing your password). | | `not_invited` | 403 | Signing in: the account isn't on the invite list and has no pending invite. | | `not_a_member` | 403 | You aren't a current member of that household. See [Households](/api/households/). | | `not_owner` | 403 | Only the household's owner can do this. | | `rate_limited` | 429 | Too many requests; wait `Retry-After` seconds. | ### Budgets, groups, and items [#budgets-groups-and-items] | Code | Status | Meaning | | ------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `not_found` | 404 | No such group, item, transaction, household, or invite (or it isn't in this household). | | `month_not_created` | 404 / 422 | The month has no budget yet: `404` when reading it, `422` when writing to it. Create it with `POST /months/{month}`. | | `month_exists` | 409 | The month already has a budget. | | `no_previous_month` | 409 | Resetting with "replace" needs an earlier budget to copy, and there isn't one. | | `name_taken` | 409 | A group or item with that name already exists (names are unique within a budget). | | `income_group_protected` | 422 | The Income group can't be deleted. | | `kind_mismatch` | 422 | The item's kind doesn't allow this: income items can't be funds, favorites, or harvested, can't leave Income; funds can't be harvested; an expense can't be split to an income item. | | `already_fund` | 422 | The item is already a fund. | | `not_fund` | 422 | The operation only applies to funds. | | `nothing_to_harvest` | 422 | The item has nothing left over to move back to Left to Budget. | | `invalid` | 400 / 422 | Generic validation failure; `message` says what's wrong. | ### Transactions [#transactions] | Code | Status | Meaning | | ---------------------- | ------ | ------------------------------------------------------------------------------------------------- | | `id_conflict` | 409 | That transaction ID was already used with a different body. See [Idempotency](/api/idempotency/). | | `amount_not_positive` | 422 | Amounts must be greater than zero (the `type` gives the direction). | | `split_not_positive` | 422 | Every split's amount must be greater than zero. | | `splits_do_not_sum` | 422 | The splits must add up to the transaction's amount. | | `duplicate_split_item` | 422 | Two splits point at the same item. | | `item_not_in_month` | 422 | A split's item doesn't exist in the transaction's budget month. | ### Households and invites [#households-and-invites] | Code | Status | Meaning | | ----------------------- | ------ | ---------------------------------------------------------------------------- | | `already_member` | 409 | That person is already in the household. | | `invite_email_mismatch` | 403 | The invite was sent to a different email than the one you're signed in with. | | `invite_unavailable` | 410 | The invite expired, or was revoked, declined, or already accepted. | | `last_household` | 422 | You can't leave, delete, or archive your only (non-archived) household. | | `personal_household` | 422 | Your personal household can't be deleted, and its owner can't leave it. | | `owner_must_transfer` | 422 | Transfer ownership before leaving a household others still belong to. | | `default_household` | 422 | Your default household can't be archived; pick another default first. | | `household_archived` | 422 | An archived household can't be your default. | | `choose_default` | 422 | Leaving your default household: say which household becomes the new default. | ### Email + password accounts [#email--password-accounts] These apply only where email + password sign-in is turned on. | Code | Status | Meaning | | ------------------ | ------ | ------------------------------------------- | | `bad_credentials` | 401 | Email or password is incorrect. | | `email_unverified` | 403 | Verify the email address first. | | `invalid_link` | 410 | The email link expired or was already used. | ### Server [#server] | Code | Status | Meaning | | ---------- | ------ | ------------------------------------------------------------------ | | `internal` | 500 | Unexpected error. Retry later; if it keeps happening, let us know. | --- # Households > Pick which household a request acts on with the X-SpendRock-Household header. Source: https://docs.spendrock.com/api/households/ A **household** owns a budget. You can belong to several: your own personal household (created the first time you sign in) plus any you've been invited to or created. Your token belongs to **you**, not to a household. ## Choosing the household [#choosing-the-household] Budget and transaction calls act on **one** household: * the one named by the `X-SpendRock-Household` header, if you send it, or * your **default** household otherwise. ```bash # your default household curl -s "$SR_API/months/2026-12" -H "Authorization: Bearer $SR_TOKEN" # a specific household curl -s "$SR_API/months/2026-12" \ -H "Authorization: Bearer $SR_TOKEN" \ -H "X-SpendRock-Household: 01J8Z3N4Q5R6S7T8V9W0X1Y2Z3" ``` `GET /me` tells you which household the request acted on (`household`), your default (`default_household_id`), and every household you belong to (`households`). If you aren't a current member of the household you name, the request fails with `403` and the code `not_a_member`. Membership is checked on every request, so someone removed from a household loses access immediately. Your default household can change (you can pick a different one in the app). A script that should always touch the same budget should send `X-SpendRock-Household` explicitly. ## Finding household IDs [#finding-household-ids] ```bash curl -s "$SR_API/households" -H "Authorization: Bearer $SR_TOKEN" ``` This lists your households, default first, with your `role` (`owner` or `member`) and whether each is your `personal` household. Archived households are left out unless you pass `include_archived=true`. (Needs `households:read`.) ## Managing households [#managing-households] With `households:manage` a token can do what you can do under **Settings → Households**: create households, rename them (owner only), set your default, archive and unarchive, leave, transfer ownership (owner only), remove members (owner only), and send, revoke, accept, or decline invites. The rules are the app's rules. For example: * you can't leave or delete your **only** household (`last_household`) * your **personal** household can't be deleted (`personal_household`) * an owner must transfer ownership before leaving (`owner_must_transfer`) * only the owner can do owner things (`not_owner`) See [Errors](/api/errors/) for every code. --- # Idempotency > Transaction IDs are generated by the client and double as idempotency keys, so retries never duplicate. Source: https://docs.spendrock.com/api/idempotency/ Networks fail. If a request to add a transaction times out, you don't know whether it was saved. SpendRock makes retrying safe: **the client picks the transaction's ID**, and that ID is the idempotency key. ## How it works [#how-it-works] When you create a transaction with `POST /transactions`, you send an `id`: a new [ULID](https://github.com/ulid/spec) you generate. | You send | You get | | ----------------------------------------- | --------------------------------------------------------------- | | A new `id` | `201 Created` with the transaction and the recomputed month(s). | | The same `id` and the **same** body again | `200 OK` with the **stored** result. Nothing is created twice. | | The same `id` with a **different** body | `409 Conflict` with the code `id_conflict`. | So the rule for clients is simple: **generate the ID once, before the first attempt, and reuse it for every retry** of that same transaction. Never generate a new ID for a retry. ```js import { ulid } from 'ulid'; const txn = { id: ulid(), // once, up front type: 'expense', amount: 5420, date: '2026-12-03', merchant: 'Corner Grocery', budget_month: '2026-12', splits: [{ item_id: groceriesId, amount: 5420 }], }; // Safe to call as many times as needed: at most one transaction is created. async function save() { const res = await fetch(`${SR_API}/transactions`, { method: 'POST', headers: { Authorization: `Bearer ${SR_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify(txn), }); if (res.status === 201 || res.status === 200) return res.json(); throw new Error(`${res.status}: ${(await res.json()).code}`); } ``` This is exactly how the mobile app saves expenses you add while offline: they're stored on the device with their ID and uploaded when the connection returns, and nothing is lost or duplicated. ## Other requests [#other-requests] * **Updates** (`PATCH`) set fields to the values you send, so repeating one is harmless. * **Deleting** a transaction is a soft delete: nothing is lost, and `POST /transactions/{id}/restore` brings it back. * **Other creates** (groups, items, households, invites) use server-generated IDs and are **not** idempotent. If one times out, list or read before retrying. Names of groups and items are unique within a month, so a duplicate create usually fails with `name_taken` rather than creating a copy. --- # API overview > A JSON API over HTTPS for everything SpendRock does, authenticated with personal access tokens. Source: https://docs.spendrock.com/api/ The SpendRock API is the same API the web and mobile apps use: every budget, transaction, and household operation is available to your own scripts. Requests and responses are JSON over HTTPS. **Base URL:** `https://app.spendrock.com/api/v1` Create a token and make your first request in two minutes. Personal access tokens, what each scope allows, and what tokens can't do. Every endpoint, generated from the OpenAPI spec, with a playground to try it with your token. The error format and every error code. ## Conventions at a glance [#conventions-at-a-glance] * **Money** is an integer number of **cents** (`1234` is $12.34). See [Money & dates](/api/money-and-dates/). * **Months** are `YYYY-MM`, **dates** are `YYYY-MM-DD`, and **IDs** are [ULIDs](https://github.com/ulid/spec). * **Households:** budget calls act on your default household unless you send `X-SpendRock-Household`. See [Households](/api/households/). * **Transactions have client-generated IDs**, which double as idempotency keys. See [Idempotency](/api/idempotency/). * **Mutations return the recomputed month**, so you rarely need a second request to see the effect. * **Errors** are `{ "code", "message", "details"? }` with a stable `code`. See [Errors](/api/errors/). * **`/v1` only changes additively.** Ignore fields you don't recognize. See [Versioning & changelog](/api/versioning/). ## The OpenAPI spec [#the-openapi-spec] The reference in these docs is generated from the API's OpenAPI 3.0 spec. The live spec is public, with no authentication, at `GET /openapi.json` and `GET /openapi.yaml` under the base URL. Use it to generate a client in your language of choice. Every page here is available as Markdown (add `.md` to the URL), and [/llms.txt](/llms.txt) / [/llms-full.txt](/llms-full.txt) index the whole site. Use **Copy Markdown** or **Open in Claude / ChatGPT** at the top of any page. --- # Money & dates > Amounts are integer cents; months are YYYY-MM; dates are YYYY-MM-DD. Source: https://docs.spendrock.com/api/money-and-dates/ ## Money is integer cents [#money-is-integer-cents] Every amount in the API is an **integer number of US cents** (a 64-bit integer): | Dollars | In the API | | --------- | ---------- | | $12.34 | `1234` | | $1,500.00 | `150000` | | $0.05 | `5` | Never send floats or strings. Convert at the edges of your program, for example `Math.round(dollars * 100)`, and format for display by dividing by 100. * **Transaction amounts are always positive.** Whether money comes in or goes out is the transaction's `type`: `expense` or `income`. * **Split amounts must add up** to the transaction's amount (`splits_do_not_sum` otherwise), and each must be positive (`split_not_positive`). * **Computed figures can be negative.** `left_to_budget` is negative when you've planned more than your income, and an item's `remaining` is negative when it's overspent. Fund balances can go negative too. ## Months are `YYYY-MM` [#months-are-yyyy-mm] A budget covers one calendar month and is addressed by `YYYY-MM`, like `2026-12`: ``` GET /months/2026-12 ``` A month either has a budget or doesn't yet. Reading one that doesn't gives `404` with the code `month_not_created`. Create it with `POST /months/{month}`, which copies the most recent earlier budget (or a default template). ## Dates are `YYYY-MM-DD` [#dates-are-yyyy-mm-dd] A transaction's `date` is a plain calendar date with no time or time zone, like `2026-12-03`. Timestamps like `created_at` are RFC 3339 date-times in UTC. ### Budget month vs. date [#budget-month-vs-date] A transaction also has a `budget_month`: the month whose budget it counts toward. It's usually the month of its date, but it doesn't have to be. A rent payment made on November 30th can count toward December. `budget_month` is required whenever the transaction has splits, and each split's item must exist in that month (`item_not_in_month` otherwise). ## IDs are ULIDs [#ids-are-ulids] IDs are [ULIDs](https://github.com/ulid/spec): 26 characters of Crockford base32, like `01J8Z3N4Q5R6S7T8V9W0X1Y2Z3`. They sort by creation time. Most are generated by the server; the exception is **transaction IDs**, which you generate (see [Idempotency](/api/idempotency/)). --- # Pagination > List transactions page by page with an opaque cursor. Source: https://docs.spendrock.com/api/pagination/ Lists that can grow without bound are paginated with a **cursor**. Today that's `GET /transactions`. Other lists (households, members, invites, a month's groups and items) are small and come back whole. ## Parameters [#parameters] | Parameter | Meaning | | --------- | ----------------------------------------------------------------------------------------------- | | `limit` | Page size, 1 to 200. Default 100. | | `cursor` | Where to continue. Omit it for the first page; then pass the previous response's `next_cursor`. | The response has the page's items plus `next_cursor`. When `next_cursor` is `null` (or missing), you've reached the end. Treat the cursor as an opaque string: don't parse or build it, and don't reuse it with different filters. ## Example [#example] ```bash # first page curl -s "$SR_API/transactions?view=tracked&month=2026-12&limit=50" \ -H "Authorization: Bearer $SR_TOKEN" ``` ```json { "transactions": [ … 50 transactions, newest first … ], "next_cursor": "eyJkIjoiMjAyNi0xMi0wMyIsImkiOiIwMUpDWEs2UThXNk01VDdWM04yQjlSNFkxWiJ9", "untracked_count": 2 } ``` ```bash # next page: same filters, plus the cursor curl -s "$SR_API/transactions?view=tracked&month=2026-12&limit=50&cursor=eyJkIjoi…" \ -H "Authorization: Bearer $SR_TOKEN" ``` A loop that reads everything: ```js let cursor; const all = []; do { const url = new URL(`${SR_API}/transactions`); url.search = new URLSearchParams({ view: 'tracked', month: '2026-12', limit: '200' }); if (cursor) url.searchParams.set('cursor', cursor); const res = await fetch(url, { headers: { Authorization: `Bearer ${SR_TOKEN}` } }); const page = await res.json(); all.push(...page.transactions); cursor = page.next_cursor; } while (cursor); ``` ## Filters [#filters] `GET /transactions` needs a `view`: `untracked`, `tracked`, or `deleted` (the three tabs of the app's Transactions panel). `month` limits `tracked` to one budget month, and `q` searches by merchant (a case-insensitive substring) or by an exact amount such as `54.20`. Results are newest first by date. --- # Quickstart > Create a personal access token and make your first API request. Source: https://docs.spendrock.com/api/quickstart/ ## 1. Create a personal access token [#1-create-a-personal-access-token] In SpendRock, open **Account → API tokens** ([open it now](https://app.spendrock.com/account/api-tokens)) and choose **Create token**: * **Name:** something that tells you where it's used, like `budget-export script`. * **Expiry:** 30, 90, or 365 days, or never. * **Scopes:** **Read only** for scripts that only look, **Full access** for scripts that change things, or pick exactly the ones you need. See [Authentication & scopes](/api/authentication/). The token (it starts with `srp_`) is shown **once**. Copy it somewhere safe, like a password manager or your script's secret store. If you lose it, revoke it and create a new one. A token acts as you. Don't commit it to a repository or paste it into shared documents. ## 2. Make your first request [#2-make-your-first-request] Set two shell variables. `SR_API` is the base URL, `https://app.spendrock.com/api/v1`. ```bash export SR_API="" export SR_TOKEN="srp_…" # your token ``` Ask who you are: ```bash curl -s "$SR_API/me" -H "Authorization: Bearer $SR_TOKEN" ``` You get your user, the household you're acting on, and all your households (abbreviated): ```json { "user": { "id": "…", "name": "Jacob Barrieault", "email": "jacob@example.com" }, "household": { "id": "01J8Z3N4Q5R6S7T8V9W0X1Y2Z3", "name": "Jacob's Budget", "role": "owner" }, "default_household_id": "01J8Z3N4Q5R6S7T8V9W0X1Y2Z3", "households": [ … ] } ``` That needs the `account:read` scope. With a **Read only** or **Full access** token it just works. ## 3. Read this month's budget [#3-read-this-months-budget] ```bash curl -s "$SR_API/months/2026-12" -H "Authorization: Bearer $SR_TOKEN" ``` The response is the whole month in one call: `left_to_budget`, every group and item with its planned, spent, and remaining amounts (all in **cents**), and more. If the month has no budget yet, you get `404` with the code `month_not_created`. ## 4. Add an expense [#4-add-an-expense] Transactions need an ID that **you** generate: a [ULID](https://github.com/ulid/spec). Sending the same request twice (say, after a timeout) never creates a duplicate. See [Idempotency](/api/idempotency/). ```bash curl -s -X POST "$SR_API/transactions" \ -H "Authorization: Bearer $SR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "id": "01JCXK6Q8W6M5T7V3N2B9R4Y1Z", "type": "expense", "amount": 5420, "date": "2026-12-03", "merchant": "Corner Grocery", "budget_month": "2026-12", "splits": [{ "item_id": "", "amount": 5420 }] }' ``` That needs `transactions:write`. The response contains the new transaction and the recomputed month, so you can see the item's new remaining amount right away. ## Next steps [#next-steps] * Try any endpoint from the browser: every [reference](/api/reference/months/getMonth/) page has a playground. Paste your token into its **Authorization** field. * Read about [errors](/api/errors/) and [rate limits](/api/rate-limits/) before you automate anything. --- # Rate limits > 120 requests per minute per token, with bursts, and headers that tell you where you stand. Source: https://docs.spendrock.com/api/rate-limits/ Each personal access token may make **120 requests per minute**. The limit is a token bucket: a token starts with a full bucket of 120 requests, so short bursts are fine, and the bucket refills steadily at 2 requests per second. ## Headers [#headers] Every response to a request made with a personal access token carries: | Header | Meaning | | ----------------------- | --------------------------------------- | | `X-RateLimit-Limit` | Requests allowed per minute (`120`). | | `X-RateLimit-Remaining` | Requests left in the bucket right now. | | `X-RateLimit-Reset` | Seconds until the bucket is full again. | ## Over the limit [#over-the-limit] When the bucket is empty, the API answers `429 Too Many Requests` with the code `rate_limited` and a `Retry-After` header: the number of seconds to wait before trying again. ```http HTTP/2 429 retry-after: 1 x-ratelimit-limit: 120 x-ratelimit-remaining: 0 x-ratelimit-reset: 60 { "code": "rate_limited", "message": "Too many requests. Try again in a moment." } ``` A well-behaved client: * waits `Retry-After` seconds before retrying a `429` (retrying [transaction creates](/api/idempotency/) is always safe) * slows down when `X-RateLimit-Remaining` gets low, instead of running into the wall ```js async function call(url, init) { for (;;) { const res = await fetch(url, init); if (res.status !== 429) return res; const wait = Number(res.headers.get('Retry-After') ?? '1'); await new Promise((r) => setTimeout(r, wait * 1000)); } } ``` Limits are tracked in memory on each server instance, not shared between instances. Under load you may occasionally get a little more than 120 requests per minute through, and a limit can reset early. Don't rely on the exact numbers; do respect `429` and `Retry-After`. ## Who is limited [#who-is-limited] Only **personal access tokens** are limited this way. Browser and mobile-app sessions aren't (signing in and password requests have their own protections against abuse). --- # Versioning & changelog > /v1 only changes additively. Here's what that promises, and what changed. Source: https://docs.spendrock.com/api/versioning/ ## The policy [#the-policy] The API is versioned in the path: `/api/v1`. * **`/v1` only changes additively.** New endpoints, new optional request fields, new response fields, new error codes, and new enum values for things that are clearly open-ended can appear at any time. * **Breaking changes mean `/v2`.** Removing or renaming a field, changing a type or its meaning, or making an optional request field required would all go into a new version, and `/v1` would keep working alongside it. ## What that means for your client [#what-that-means-for-your-client] * **Ignore fields you don't recognize.** Don't fail on unknown properties in responses. * **Handle unknown error codes** by their HTTP status (see [Errors](/api/errors/)). * **Don't depend on field order** or on the exact wording of `message`. * **Treat cursors and IDs as opaque strings.** ## Changelog [#changelog] Newest first. ### 2026-09 [#2026-09] * **Public API launch.** Personal access tokens with fine-grained scopes, per-token rate limits (with `X-RateLimit-*` headers), CORS for the API playground, and the public spec at `/openapi.json` and `/openapi.yaml`. * These docs: guides, a generated reference with a playground, and Markdown / `llms.txt` versions of every page. --- # Exchange a Google ID token for a SpendRock session (AUTH-1, AUTH-2) Source: https://docs.spendrock.com/api/reference/auth/signInWithGoogle/ ## POST /auth/google `POST https://app.spendrock.com/api/v1/auth/google` Operation ID: `signInWithGoogle` Scope: `public` Exchange a Google ID token for a SpendRock session (AUTH-1, AUTH-2) No authentication needed. ### Request body (required) ```yaml type: object required: - id_token - client properties: id_token: type: string client: type: string enum: - web - mobile description: '`web` sets the `sr_session` cookie; `mobile` returns a bearer token.' ``` ### Responses #### 200 Signed in. ```yaml type: object required: - me properties: token: type: string description: Bearer session token (mobile only). Store it in the Keychain. me: type: object required: - user - household - default_household_id - households properties: user: type: object required: - id - email - name properties: id: type: string description: >- Account id: a ULID, or the Google subject for accounts created before email + password sign-in. email: type: string name: type: string household: type: object description: The household this request acts on (X-SpendRock-Household, else the default). required: - id - name - role properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string role: type: string enum: - owner - member default_household_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ households: type: array description: Every household I belong to, archived ones included, default first. items: type: object required: - id - name - role - personal - default - archived - member_count - joined_at properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string role: type: string enum: - owner - member personal: type: boolean description: Created at its owner's first sign-in; can't be deleted (HH-18). default: type: boolean description: My default (HH-10). archived: type: boolean description: Archived by me (per user HH-19).: null member_count: type: integer joined_at: type: string format: date-time ``` #### 401 Error. #### 403 Valid Google account, but not on the allowlist (`not_invited`). --- # Sign Out Source: https://docs.spendrock.com/api/reference/auth/signOut/ ## POST /auth/logout `POST https://app.spendrock.com/api/v1/auth/logout` Operation ID: `signOut` Scope: `any` Any credential; no scope needed. ### Responses #### 204 Session revoked (and the cookie cleared). #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Start an email + password sign-up (ACCT-1) Source: https://docs.spendrock.com/api/reference/auth/registerWithPassword/ ## POST /auth/password/register `POST https://app.spendrock.com/api/v1/auth/password/register` Operation ID: `registerWithPassword` Scope: `public` Start an email + password sign-up (ACCT-1) Emails a verification link (valid 24h, single use). If the email already belongs to an account, that inbox gets a "you already have an account" note instead. The response is the same either way. No authentication needed. ### Request body (required) ```yaml type: object required: - email - password - name properties: email: type: string maxLength: 254 password: type: string minLength: 10 maxLength: 128 name: type: string maxLength: 80 ``` ### Responses #### 202 Accepted. The same answer whether or not the email has an account. ```yaml type: object required: - message properties: message: type: string ``` #### 404 Error. #### 422 Error. #### 429 Error. --- # Redeem an email verification link and sign in (ACCT-2) Source: https://docs.spendrock.com/api/reference/auth/verifyEmail/ ## POST /auth/password/verify `POST https://app.spendrock.com/api/v1/auth/password/verify` Operation ID: `verifyEmail` Scope: `public` Redeem an email verification link and sign in (ACCT-2) The token is the `token` query parameter of the emailed link. A first verification creates the account and its personal household (HH-1) if the email is admitted (allowlist, pending invite, or existing member; HH-7, ACCT-4); otherwise the email is verified but the answer is 403 `not_invited`. 410 `invalid_link` for expired or used links. No authentication needed. ### Request body (required) ```yaml type: object required: - token - client properties: token: type: string client: type: string enum: - web - mobile description: '`web` sets the `sr_session` cookie; `mobile` returns a bearer token.' ``` ### Responses #### 200 Signed in. ```yaml type: object required: - me properties: token: type: string description: Bearer session token (mobile only). Store it in the Keychain. me: type: object required: - user - household - default_household_id - households properties: user: type: object required: - id - email - name properties: id: type: string description: >- Account id: a ULID, or the Google subject for accounts created before email + password sign-in. email: type: string name: type: string household: type: object description: The household this request acts on (X-SpendRock-Household, else the default). required: - id - name - role properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string role: type: string enum: - owner - member default_household_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ households: type: array description: Every household I belong to, archived ones included, default first. items: type: object required: - id - name - role - personal - default - archived - member_count - joined_at properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string role: type: string enum: - owner - member personal: type: boolean description: Created at its owner's first sign-in; can't be deleted (HH-18). default: type: boolean description: My default (HH-10). archived: type: boolean description: Archived by me (per user HH-19).: null member_count: type: integer joined_at: type: string format: date-time ``` #### 403 Error. #### 404 Error. #### 410 Error. #### 429 Error. --- # Email a new verification link to an unverified sign-up Source: https://docs.spendrock.com/api/reference/auth/resendVerification/ ## POST /auth/password/resend `POST https://app.spendrock.com/api/v1/auth/password/resend` Operation ID: `resendVerification` Scope: `public` Email a new verification link to an unverified sign-up No authentication needed. ### Request body (required) ```yaml type: object required: - email properties: email: type: string ``` ### Responses #### 202 Accepted. The same answer whether or not the email has an account. ```yaml type: object required: - message properties: message: type: string ``` #### 404 Error. #### 422 Error. #### 429 Error. --- # Sign in with email + password (ACCT-1) Source: https://docs.spendrock.com/api/reference/auth/signInWithPassword/ ## POST /auth/password/login `POST https://app.spendrock.com/api/v1/auth/password/login` Operation ID: `signInWithPassword` Scope: `public` Sign in with email + password (ACCT-1) 401 `bad_credentials` ("Email or password is incorrect.") for any wrong email or password; 403 `email_unverified` when the email still needs verifying; 403 `not_invited`. No authentication needed. ### Request body (required) ```yaml type: object required: - email - password - client properties: email: type: string password: type: string client: type: string enum: - web - mobile description: '`web` sets the `sr_session` cookie; `mobile` returns a bearer token.' ``` ### Responses #### 200 Signed in. ```yaml type: object required: - me properties: token: type: string description: Bearer session token (mobile only). Store it in the Keychain. me: type: object required: - user - household - default_household_id - households properties: user: type: object required: - id - email - name properties: id: type: string description: >- Account id: a ULID, or the Google subject for accounts created before email + password sign-in. email: type: string name: type: string household: type: object description: The household this request acts on (X-SpendRock-Household, else the default). required: - id - name - role properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string role: type: string enum: - owner - member default_household_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ households: type: array description: Every household I belong to, archived ones included, default first. items: type: object required: - id - name - role - personal - default - archived - member_count - joined_at properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string role: type: string enum: - owner - member personal: type: boolean description: Created at its owner's first sign-in; can't be deleted (HH-18). default: type: boolean description: My default (HH-10). archived: type: boolean description: Archived by me (per user HH-19).: null member_count: type: integer joined_at: type: string format: date-time ``` #### 401 Error. #### 403 Error. #### 404 Error. #### 429 Error. --- # Email a password reset link (ACCT-3) Source: https://docs.spendrock.com/api/reference/auth/requestPasswordReset/ ## POST /auth/password/forgot `POST https://app.spendrock.com/api/v1/auth/password/forgot` Operation ID: `requestPasswordReset` Scope: `public` Email a password reset link (ACCT-3) Sends a link (valid 1h, single use) if the email has an account. Same answer either way. No authentication needed. ### Request body (required) ```yaml type: object required: - email properties: email: type: string ``` ### Responses #### 202 Accepted. The same answer whether or not the email has an account. ```yaml type: object required: - message properties: message: type: string ``` #### 404 Error. #### 422 Error. #### 429 Error. --- # Set a new password from a reset link (ACCT-3) Source: https://docs.spendrock.com/api/reference/auth/resetPassword/ ## POST /auth/password/reset `POST https://app.spendrock.com/api/v1/auth/password/reset` Operation ID: `resetPassword` Scope: `public` Set a new password from a reset link (ACCT-3) Also marks the email verified (the link proves it) and signs out every session of the account. It doesn't sign in; sign in with the new password. No authentication needed. ### Request body (required) ```yaml type: object required: - token - password properties: token: type: string password: type: string minLength: 10 maxLength: 128 ``` ### Responses #### 204 Password changed; every session signed out. #### 404 Error. #### 410 Error. #### 422 Error. #### 429 Error. --- # The caller's sign-in methods (ACCT-2, Settings → Sign-in methods) Source: https://docs.spendrock.com/api/reference/auth/getSignInMethods/ ## GET /me/sign-in-methods `GET https://app.spendrock.com/api/v1/me/sign-in-methods` Operation ID: `getSignInMethods` Scope: `account:read` The caller's sign-in methods (ACCT-2, Settings → Sign-in methods) Required scope: `account:read` (personal access tokens; sessions have every scope). ### Responses #### 200 Sign-in methods. ```yaml type: object required: - email - email_verified - password - password_pending - google properties: email: type: string email_verified: type: boolean password: type: boolean description: The account has a password. password_pending: type: boolean description: The password works once the email is verified. google: type: array items: type: object required: - email - linked_at properties: email: type: string linked_at: type: string format: date-time ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 404 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Add or change the caller's password (ACCT-2) Source: https://docs.spendrock.com/api/reference/auth/setPassword/ ## PUT /me/password `PUT https://app.spendrock.com/api/v1/me/password` Operation ID: `setPassword` Scope: `session` Add or change the caller's password (ACCT-2) Changing an existing password needs `current_password` (401 `bad_credentials`). If the account's email isn't verified yet, a verification link is emailed and the password works once it's verified (`verification_sent`). Needs a signed-in session: personal access tokens get 403 `session_required`. ### Request body (required) ```yaml type: object required: - password properties: password: type: string minLength: 10 maxLength: 128 current_password: type: string ``` ### Responses #### 200 Password saved. ```yaml type: object required: - verification_sent properties: verification_sent: type: boolean ``` #### 401 Error. #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 404 Error. #### 422 Error. #### 429 Error. --- # Get Me Source: https://docs.spendrock.com/api/reference/auth/getMe/ ## GET /me `GET https://app.spendrock.com/api/v1/me` Operation ID: `getMe` Scope: `account:read` Required scope: `account:read` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | ### Responses #### 200 Current user and household. ```yaml type: object required: - user - household - default_household_id - households properties: user: type: object required: - id - email - name properties: id: type: string description: >- Account id: a ULID, or the Google subject for accounts created before email + password sign-in. email: type: string name: type: string household: type: object description: The household this request acts on (X-SpendRock-Household, else the default). required: - id - name - role properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string role: type: string enum: - owner - member default_household_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ households: type: array description: Every household I belong to, archived ones included, default first. items: type: object required: - id - name - role - personal - default - archived - member_count - joined_at properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string role: type: string enum: - owner - member personal: type: boolean description: Created at its owner's first sign-in; can't be deleted (HH-18). default: type: boolean description: My default (HH-10). archived: type: boolean description: Archived by me (per user HH-19).: null member_count: type: integer joined_at: type: string format: date-time ``` #### 401 Error. #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Personal access tokens (AUTH-5) Source: https://docs.spendrock.com/api/reference/auth/listTokens/ ## GET /tokens `GET https://app.spendrock.com/api/v1/tokens` Operation ID: `listTokens` Scope: `session` Personal access tokens (AUTH-5) Needs a signed-in session: personal access tokens get 403 `session_required`. ### Responses #### 200 Tokens (secrets are never returned after creation). ```yaml type: object required: - tokens properties: tokens: type: array items: type: object required: - id - name - scopes - created_at properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string scopes: type: array items: type: string description: > A personal access token scope (DEV-2). A `*:write` / `households:manage` scope includes the matching read scope. Browser and mobile sessions have every scope. enum: - budget:read - budget:write - transactions:read - transactions:write - households:read - households:manage - account:read - account:write created_at: type: string format: date-time last_used_at: type: - string - 'null' format: date-time description: Updated at most about once a minute. expires_at: type: - string - 'null' format: date-time description: 'Null: never expires.' ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Create a personal access token (DEV-1, DEV-2) Source: https://docs.spendrock.com/api/reference/auth/createToken/ ## POST /tokens `POST https://app.spendrock.com/api/v1/tokens` Operation ID: `createToken` Scope: `session` Create a personal access token (DEV-1, DEV-2) `scopes` defaults to every scope (full access) when omitted. `expires_in_days` omitted means the token never expires; an expired token gets 401 `unauthenticated`. Needs a signed-in session: personal access tokens get 403 `session_required`. ### Request body (required) ```yaml type: object required: - name properties: name: type: string minLength: 1 maxLength: 100 scopes: type: array minItems: 1 items: type: string description: | A personal access token scope (DEV-2). A `*:write` / `households:manage` scope includes the matching read scope. Browser and mobile sessions have every scope. enum: - budget:read - budget:write - transactions:read - transactions:write - households:read - households:manage - account:read - account:write expires_in_days: type: integer minimum: 1 maximum: 365 ``` ### Responses #### 201 Created. `secret` is shown only once. ```yaml allOf: - type: object required: - id - name - scopes - created_at properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string scopes: type: array items: type: string description: | A personal access token scope (DEV-2). A `*:write` / `households:manage` scope includes the matching read scope. Browser and mobile sessions have every scope. enum: - budget:read - budget:write - transactions:read - transactions:write - households:read - households:manage - account:read - account:write created_at: type: string format: date-time last_used_at: type: - string - 'null' format: date-time description: Updated at most about once a minute. expires_at: type: - string - 'null' format: date-time description: 'Null: never expires.' - type: object required: - secret properties: secret: type: string ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Delete Token Source: https://docs.spendrock.com/api/reference/auth/deleteToken/ ## DELETE /tokens/{tokenId} `DELETE https://app.spendrock.com/api/v1/tokens/{tokenId}` Operation ID: `deleteToken` Scope: `session` Needs a signed-in session: personal access tokens get 403 `session_required`. ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `tokenId` | path | yes | string | ULID. | ### Responses #### 204 Revoked. #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 404 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Which months have budgets (for the month picker, NAV-2) Source: https://docs.spendrock.com/api/reference/months/listMonths/ ## GET /months `GET https://app.spendrock.com/api/v1/months` Operation ID: `listMonths` Scope: `budget:read` Which months have budgets (for the month picker, NAV-2) Required scope: `budget:read` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | | `from` | query | yes | string | | | `to` | query | yes | string | | ### Responses #### 200 One entry per month in [from, to] (max 36 months). ```yaml type: object required: - months properties: months: type: array items: type: object required: - month - exists properties: month: type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ exists: type: boolean ``` #### 400 Error. #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # The full budget for a month (one call renders the whole budget screen, PERF-4) Source: https://docs.spendrock.com/api/reference/months/getMonth/ ## GET /months/{month} `GET https://app.spendrock.com/api/v1/months/{month}` Operation ID: `getMonth` Scope: `budget:read` The full budget for a month (one call renders the whole budget screen, PERF-4) Required scope: `budget:read` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | | `month` | path | yes | string | | ### Responses #### 200 The month's budget. ```yaml type: object required: - month - left_to_budget - groups - favorites - center - untracked_count - copied_from - created_at properties: month: type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ left_to_budget: allOf: - type: integer format: int64 description: US cents. description: CALC-1. Positive = left to budget; 0 = "It's a SpendRock budget!"; negative = over budget. groups: type: array description: Income first, then user order. items: allOf: - type: object required: - id - name - color - is_income properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string color: type: string pattern: ^#[0-9a-fA-F]{6}$ is_income: type: boolean - type: object required: - items - totals properties: items: type: array items: description: >- An item as it appears in one month, with computed figures (PLAN §4, spec/calc-fixtures). allOf: - type: object description: Month-independent item attributes (shared by every month). required: - id - name - kind - is_fund - favorite properties: id: $ref: Id name: type: string kind: $ref: ItemKind is_fund: type: boolean favorite: type: boolean fund_target: $ref: NullableCents - type: object required: - group_id - planned - spent - received - remaining - note properties: group_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ planned: type: integer format: int64 description: US cents. spent: allOf: - type: integer format: int64 description: US cents. description: Expense items; net of refunds. received: allOf: - type: integer format: int64 description: US cents. description: Income items. remaining: allOf: - type: integer format: int64 description: US cents. description: >- Row value. Funds: the running balance. Income: planned − received (negative = over-received, shown as +$X). note: type: string fund: type: - object - 'null' description: Present when is_fund. required: - carry_in - adjustments - ending properties: carry_in: type: integer format: int64 description: US cents. adjustments: type: array description: '"Balance Updated" entries this month (FUND-5).' items: type: object required: - amount - at properties: amount: type: integer format: int64 description: US cents. at: type: string format: date-time by: type: string ending: type: integer format: int64 description: US cents. totals: type: object required: - planned - spent - received - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. received: type: integer format: int64 description: US cents. remaining: allOf: - type: integer format: int64 description: US cents. description: >- Month-only (planned − spent; funds exclude carry-in). Income: planned − received. favorites: type: array description: Favorited item ids present in this month, in Favorites order (FAV-1). items: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ center: type: object description: Summary doughnut center (SUM-2). required: - planned - spent - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. remaining: type: integer format: int64 description: US cents. untracked_count: type: integer description: Household-wide (TXN-2a badge). copied_from: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' created_at: type: string format: date-time ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 404 No budget for this month yet (`month_not_created`). Shows the CREATE-1 empty state. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Create the month by copying the most recent earlier budget, or the default template (CREATE-2, CREATE-4, CREATE-5) Source: https://docs.spendrock.com/api/reference/months/createMonth/ ## POST /months/{month} `POST https://app.spendrock.com/api/v1/months/{month}` Operation ID: `createMonth` Scope: `budget:write` Create the month by copying the most recent earlier budget, or the default template (CREATE-2, CREATE-4, CREATE-5) Required scope: `budget:write` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | | `month` | path | yes | string | | ### Responses #### 201 Created. ```yaml type: object required: - month - left_to_budget - groups - favorites - center - untracked_count - copied_from - created_at properties: month: type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ left_to_budget: allOf: - type: integer format: int64 description: US cents. description: CALC-1. Positive = left to budget; 0 = "It's a SpendRock budget!"; negative = over budget. groups: type: array description: Income first, then user order. items: allOf: - type: object required: - id - name - color - is_income properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string color: type: string pattern: ^#[0-9a-fA-F]{6}$ is_income: type: boolean - type: object required: - items - totals properties: items: type: array items: description: >- An item as it appears in one month, with computed figures (PLAN §4, spec/calc-fixtures). allOf: - type: object description: Month-independent item attributes (shared by every month). required: - id - name - kind - is_fund - favorite properties: id: $ref: Id name: type: string kind: $ref: ItemKind is_fund: type: boolean favorite: type: boolean fund_target: $ref: NullableCents - type: object required: - group_id - planned - spent - received - remaining - note properties: group_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ planned: type: integer format: int64 description: US cents. spent: allOf: - type: integer format: int64 description: US cents. description: Expense items; net of refunds. received: allOf: - type: integer format: int64 description: US cents. description: Income items. remaining: allOf: - type: integer format: int64 description: US cents. description: >- Row value. Funds: the running balance. Income: planned − received (negative = over-received, shown as +$X). note: type: string fund: type: - object - 'null' description: Present when is_fund. required: - carry_in - adjustments - ending properties: carry_in: type: integer format: int64 description: US cents. adjustments: type: array description: '"Balance Updated" entries this month (FUND-5).' items: type: object required: - amount - at properties: amount: type: integer format: int64 description: US cents. at: type: string format: date-time by: type: string ending: type: integer format: int64 description: US cents. totals: type: object required: - planned - spent - received - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. received: type: integer format: int64 description: US cents. remaining: allOf: - type: integer format: int64 description: US cents. description: >- Month-only (planned − spent; funds exclude carry-in). Income: planned − received. favorites: type: array description: Favorited item ids present in this month, in Favorites order (FAV-1). items: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ center: type: object description: Summary doughnut center (SUM-2). required: - planned - spent - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. remaining: type: integer format: int64 description: US cents. untracked_count: type: integer description: Household-wide (TXN-2a badge). copied_from: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' created_at: type: string format: date-time ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 409 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Reset Budget (CREATE-6). `replace` untracks every transaction in the month. Source: https://docs.spendrock.com/api/reference/months/resetMonth/ ## POST /months/{month}/reset `POST https://app.spendrock.com/api/v1/months/{month}/reset` Operation ID: `resetMonth` Scope: `budget:write` Reset Budget (CREATE-6). `replace` untracks every transaction in the month. Required scope: `budget:write` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | | `month` | path | yes | string | | ### Request body (required) ```yaml type: object required: - mode properties: mode: type: string enum: - zero - replace ``` ### Responses #### 200 Reset. ```yaml type: object required: - month - left_to_budget - groups - favorites - center - untracked_count - copied_from - created_at properties: month: type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ left_to_budget: allOf: - type: integer format: int64 description: US cents. description: CALC-1. Positive = left to budget; 0 = "It's a SpendRock budget!"; negative = over budget. groups: type: array description: Income first, then user order. items: allOf: - type: object required: - id - name - color - is_income properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string color: type: string pattern: ^#[0-9a-fA-F]{6}$ is_income: type: boolean - type: object required: - items - totals properties: items: type: array items: description: >- An item as it appears in one month, with computed figures (PLAN §4, spec/calc-fixtures). allOf: - type: object description: Month-independent item attributes (shared by every month). required: - id - name - kind - is_fund - favorite properties: id: $ref: Id name: type: string kind: $ref: ItemKind is_fund: type: boolean favorite: type: boolean fund_target: $ref: NullableCents - type: object required: - group_id - planned - spent - received - remaining - note properties: group_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ planned: type: integer format: int64 description: US cents. spent: allOf: - type: integer format: int64 description: US cents. description: Expense items; net of refunds. received: allOf: - type: integer format: int64 description: US cents. description: Income items. remaining: allOf: - type: integer format: int64 description: US cents. description: >- Row value. Funds: the running balance. Income: planned − received (negative = over-received, shown as +$X). note: type: string fund: type: - object - 'null' description: Present when is_fund. required: - carry_in - adjustments - ending properties: carry_in: type: integer format: int64 description: US cents. adjustments: type: array description: '"Balance Updated" entries this month (FUND-5).' items: type: object required: - amount - at properties: amount: type: integer format: int64 description: US cents. at: type: string format: date-time by: type: string ending: type: integer format: int64 description: US cents. totals: type: object required: - planned - spent - received - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. received: type: integer format: int64 description: US cents. remaining: allOf: - type: integer format: int64 description: US cents. description: >- Month-only (planned − spent; funds exclude carry-in). Income: planned − received. favorites: type: array description: Favorited item ids present in this month, in Favorites order (FAV-1). items: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ center: type: object description: Summary doughnut center (SUM-2). required: - planned - spent - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. remaining: type: integer format: int64 description: US cents. untracked_count: type: integer description: Household-wide (TXN-2a badge). copied_from: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' created_at: type: string format: date-time ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 404 Error. #### 409 `replace` with no earlier month to copy (`no_previous_month`). #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Add a group to this month (GROUP-1) Source: https://docs.spendrock.com/api/reference/groups/createGroup/ ## POST /months/{month}/groups `POST https://app.spendrock.com/api/v1/months/{month}/groups` Operation ID: `createGroup` Scope: `budget:write` Add a group to this month (GROUP-1) Required scope: `budget:write` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | | `month` | path | yes | string | | ### Request body (required) ```yaml type: object required: - name properties: name: type: string minLength: 1 maxLength: 80 ``` ### Responses #### 201 The recomputed month. ```yaml type: object required: - month - left_to_budget - groups - favorites - center - untracked_count - copied_from - created_at properties: month: type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ left_to_budget: allOf: - type: integer format: int64 description: US cents. description: CALC-1. Positive = left to budget; 0 = "It's a SpendRock budget!"; negative = over budget. groups: type: array description: Income first, then user order. items: allOf: - type: object required: - id - name - color - is_income properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string color: type: string pattern: ^#[0-9a-fA-F]{6}$ is_income: type: boolean - type: object required: - items - totals properties: items: type: array items: description: >- An item as it appears in one month, with computed figures (PLAN §4, spec/calc-fixtures). allOf: - type: object description: Month-independent item attributes (shared by every month). required: - id - name - kind - is_fund - favorite properties: id: $ref: Id name: type: string kind: $ref: ItemKind is_fund: type: boolean favorite: type: boolean fund_target: $ref: NullableCents - type: object required: - group_id - planned - spent - received - remaining - note properties: group_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ planned: type: integer format: int64 description: US cents. spent: allOf: - type: integer format: int64 description: US cents. description: Expense items; net of refunds. received: allOf: - type: integer format: int64 description: US cents. description: Income items. remaining: allOf: - type: integer format: int64 description: US cents. description: >- Row value. Funds: the running balance. Income: planned − received (negative = over-received, shown as +$X). note: type: string fund: type: - object - 'null' description: Present when is_fund. required: - carry_in - adjustments - ending properties: carry_in: type: integer format: int64 description: US cents. adjustments: type: array description: '"Balance Updated" entries this month (FUND-5).' items: type: object required: - amount - at properties: amount: type: integer format: int64 description: US cents. at: type: string format: date-time by: type: string ending: type: integer format: int64 description: US cents. totals: type: object required: - planned - spent - received - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. received: type: integer format: int64 description: US cents. remaining: allOf: - type: integer format: int64 description: US cents. description: >- Month-only (planned − spent; funds exclude carry-in). Income: planned − received. favorites: type: array description: Favorited item ids present in this month, in Favorites order (FAV-1). items: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ center: type: object description: Summary doughnut center (SUM-2). required: - planned - spent - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. remaining: type: integer format: int64 description: US cents. untracked_count: type: integer description: Household-wide (TXN-2a badge). copied_from: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' created_at: type: string format: date-time ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 409 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Reorder this month's groups (GROUP-4). Income is always first; list non-income groups only. Source: https://docs.spendrock.com/api/reference/groups/setGroupOrder/ ## PUT /months/{month}/group-order `PUT https://app.spendrock.com/api/v1/months/{month}/group-order` Operation ID: `setGroupOrder` Scope: `budget:write` Reorder this month's groups (GROUP-4). Income is always first; list non-income groups only. Required scope: `budget:write` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | | `month` | path | yes | string | | ### Request body (required) ```yaml type: object required: - group_ids properties: group_ids: type: array items: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ ``` ### Responses #### 200 The recomputed month. ```yaml type: object required: - month - left_to_budget - groups - favorites - center - untracked_count - copied_from - created_at properties: month: type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ left_to_budget: allOf: - type: integer format: int64 description: US cents. description: CALC-1. Positive = left to budget; 0 = "It's a SpendRock budget!"; negative = over budget. groups: type: array description: Income first, then user order. items: allOf: - type: object required: - id - name - color - is_income properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string color: type: string pattern: ^#[0-9a-fA-F]{6}$ is_income: type: boolean - type: object required: - items - totals properties: items: type: array items: description: >- An item as it appears in one month, with computed figures (PLAN §4, spec/calc-fixtures). allOf: - type: object description: Month-independent item attributes (shared by every month). required: - id - name - kind - is_fund - favorite properties: id: $ref: Id name: type: string kind: $ref: ItemKind is_fund: type: boolean favorite: type: boolean fund_target: $ref: NullableCents - type: object required: - group_id - planned - spent - received - remaining - note properties: group_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ planned: type: integer format: int64 description: US cents. spent: allOf: - type: integer format: int64 description: US cents. description: Expense items; net of refunds. received: allOf: - type: integer format: int64 description: US cents. description: Income items. remaining: allOf: - type: integer format: int64 description: US cents. description: >- Row value. Funds: the running balance. Income: planned − received (negative = over-received, shown as +$X). note: type: string fund: type: - object - 'null' description: Present when is_fund. required: - carry_in - adjustments - ending properties: carry_in: type: integer format: int64 description: US cents. adjustments: type: array description: '"Balance Updated" entries this month (FUND-5).' items: type: object required: - amount - at properties: amount: type: integer format: int64 description: US cents. at: type: string format: date-time by: type: string ending: type: integer format: int64 description: US cents. totals: type: object required: - planned - spent - received - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. received: type: integer format: int64 description: US cents. remaining: allOf: - type: integer format: int64 description: US cents. description: >- Month-only (planned − spent; funds exclude carry-in). Income: planned − received. favorites: type: array description: Favorited item ids present in this month, in Favorites order (FAV-1). items: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ center: type: object description: Summary doughnut center (SUM-2). required: - planned - spent - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. remaining: type: integer format: int64 description: US cents. untracked_count: type: integer description: Household-wide (TXN-2a badge). copied_from: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' created_at: type: string format: date-time ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 422 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Delete a group and its items from THIS month only; their transactions become untracked (GROUP-3) Source: https://docs.spendrock.com/api/reference/groups/deleteGroupFromMonth/ ## DELETE /months/{month}/groups/{groupId} `DELETE https://app.spendrock.com/api/v1/months/{month}/groups/{groupId}` Operation ID: `deleteGroupFromMonth` Scope: `budget:write` Delete a group and its items from THIS month only; their transactions become untracked (GROUP-3) Required scope: `budget:write` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | | `month` | path | yes | string | | | `groupId` | path | yes | string | ULID. | ### Responses #### 200 The recomputed month. ```yaml type: object required: - month - left_to_budget - groups - favorites - center - untracked_count - copied_from - created_at properties: month: type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ left_to_budget: allOf: - type: integer format: int64 description: US cents. description: CALC-1. Positive = left to budget; 0 = "It's a SpendRock budget!"; negative = over budget. groups: type: array description: Income first, then user order. items: allOf: - type: object required: - id - name - color - is_income properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string color: type: string pattern: ^#[0-9a-fA-F]{6}$ is_income: type: boolean - type: object required: - items - totals properties: items: type: array items: description: >- An item as it appears in one month, with computed figures (PLAN §4, spec/calc-fixtures). allOf: - type: object description: Month-independent item attributes (shared by every month). required: - id - name - kind - is_fund - favorite properties: id: $ref: Id name: type: string kind: $ref: ItemKind is_fund: type: boolean favorite: type: boolean fund_target: $ref: NullableCents - type: object required: - group_id - planned - spent - received - remaining - note properties: group_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ planned: type: integer format: int64 description: US cents. spent: allOf: - type: integer format: int64 description: US cents. description: Expense items; net of refunds. received: allOf: - type: integer format: int64 description: US cents. description: Income items. remaining: allOf: - type: integer format: int64 description: US cents. description: >- Row value. Funds: the running balance. Income: planned − received (negative = over-received, shown as +$X). note: type: string fund: type: - object - 'null' description: Present when is_fund. required: - carry_in - adjustments - ending properties: carry_in: type: integer format: int64 description: US cents. adjustments: type: array description: '"Balance Updated" entries this month (FUND-5).' items: type: object required: - amount - at properties: amount: type: integer format: int64 description: US cents. at: type: string format: date-time by: type: string ending: type: integer format: int64 description: US cents. totals: type: object required: - planned - spent - received - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. received: type: integer format: int64 description: US cents. remaining: allOf: - type: integer format: int64 description: US cents. description: >- Month-only (planned − spent; funds exclude carry-in). Income: planned − received. favorites: type: array description: Favorited item ids present in this month, in Favorites order (FAV-1). items: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ center: type: object description: Summary doughnut center (SUM-2). required: - planned - spent - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. remaining: type: integer format: int64 description: US cents. untracked_count: type: integer description: Household-wide (TXN-2a badge). copied_from: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' created_at: type: string format: date-time ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 404 Error. #### 422 The Income group can't be deleted (`income_group_protected`). #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Rename or recolor a group. Applies to EVERY month (linked identity, GROUP-2). Source: https://docs.spendrock.com/api/reference/groups/updateGroup/ ## PATCH /groups/{groupId} `PATCH https://app.spendrock.com/api/v1/groups/{groupId}` Operation ID: `updateGroup` Scope: `budget:write` Rename or recolor a group. Applies to EVERY month (linked identity, GROUP-2). Required scope: `budget:write` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | | `groupId` | path | yes | string | ULID. | ### Request body (required) ```yaml type: object properties: name: type: string minLength: 1 maxLength: 80 color: type: string pattern: ^#[0-9a-fA-F]{6}$ ``` ### Responses #### 200 Updated. ```yaml type: object required: - id - name - color - is_income properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string color: type: string pattern: ^#[0-9a-fA-F]{6}$ is_income: type: boolean ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 409 Name already used (`name_taken`, GROUP-5). #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Set the order of items within a group; an item listed here that belongs to another group is moved into this one (ITEM-5) Source: https://docs.spendrock.com/api/reference/items/setItemOrder/ ## PUT /months/{month}/groups/{groupId}/item-order `PUT https://app.spendrock.com/api/v1/months/{month}/groups/{groupId}/item-order` Operation ID: `setItemOrder` Scope: `budget:write` Set the order of items within a group; an item listed here that belongs to another group is moved into this one (ITEM-5) Required scope: `budget:write` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | | `month` | path | yes | string | | | `groupId` | path | yes | string | ULID. | ### Request body (required) ```yaml type: object required: - item_ids properties: item_ids: type: array items: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ ``` ### Responses #### 200 The recomputed month. ```yaml type: object required: - month - left_to_budget - groups - favorites - center - untracked_count - copied_from - created_at properties: month: type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ left_to_budget: allOf: - type: integer format: int64 description: US cents. description: CALC-1. Positive = left to budget; 0 = "It's a SpendRock budget!"; negative = over budget. groups: type: array description: Income first, then user order. items: allOf: - type: object required: - id - name - color - is_income properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string color: type: string pattern: ^#[0-9a-fA-F]{6}$ is_income: type: boolean - type: object required: - items - totals properties: items: type: array items: description: >- An item as it appears in one month, with computed figures (PLAN §4, spec/calc-fixtures). allOf: - type: object description: Month-independent item attributes (shared by every month). required: - id - name - kind - is_fund - favorite properties: id: $ref: Id name: type: string kind: $ref: ItemKind is_fund: type: boolean favorite: type: boolean fund_target: $ref: NullableCents - type: object required: - group_id - planned - spent - received - remaining - note properties: group_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ planned: type: integer format: int64 description: US cents. spent: allOf: - type: integer format: int64 description: US cents. description: Expense items; net of refunds. received: allOf: - type: integer format: int64 description: US cents. description: Income items. remaining: allOf: - type: integer format: int64 description: US cents. description: >- Row value. Funds: the running balance. Income: planned − received (negative = over-received, shown as +$X). note: type: string fund: type: - object - 'null' description: Present when is_fund. required: - carry_in - adjustments - ending properties: carry_in: type: integer format: int64 description: US cents. adjustments: type: array description: '"Balance Updated" entries this month (FUND-5).' items: type: object required: - amount - at properties: amount: type: integer format: int64 description: US cents. at: type: string format: date-time by: type: string ending: type: integer format: int64 description: US cents. totals: type: object required: - planned - spent - received - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. received: type: integer format: int64 description: US cents. remaining: allOf: - type: integer format: int64 description: US cents. description: >- Month-only (planned − spent; funds exclude carry-in). Income: planned − received. favorites: type: array description: Favorited item ids present in this month, in Favorites order (FAV-1). items: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ center: type: object description: Summary doughnut center (SUM-2). required: - planned - spent - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. remaining: type: integer format: int64 description: US cents. untracked_count: type: integer description: Household-wide (TXN-2a badge). copied_from: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' created_at: type: string format: date-time ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 422 e.g. moving an expense item into Income (`kind_mismatch`). #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Add an item to a group in this month (ITEM-1). Kind follows the group (income vs expense). Source: https://docs.spendrock.com/api/reference/items/createItem/ ## POST /months/{month}/items `POST https://app.spendrock.com/api/v1/months/{month}/items` Operation ID: `createItem` Scope: `budget:write` Add an item to a group in this month (ITEM-1). Kind follows the group (income vs expense). Required scope: `budget:write` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | | `month` | path | yes | string | | ### Request body (required) ```yaml type: object required: - group_id - name properties: group_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string minLength: 1 maxLength: 80 planned: type: integer format: int64 description: US cents. ``` ### Responses #### 201 The recomputed month. ```yaml type: object required: - month - left_to_budget - groups - favorites - center - untracked_count - copied_from - created_at properties: month: type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ left_to_budget: allOf: - type: integer format: int64 description: US cents. description: CALC-1. Positive = left to budget; 0 = "It's a SpendRock budget!"; negative = over budget. groups: type: array description: Income first, then user order. items: allOf: - type: object required: - id - name - color - is_income properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string color: type: string pattern: ^#[0-9a-fA-F]{6}$ is_income: type: boolean - type: object required: - items - totals properties: items: type: array items: description: >- An item as it appears in one month, with computed figures (PLAN §4, spec/calc-fixtures). allOf: - type: object description: Month-independent item attributes (shared by every month). required: - id - name - kind - is_fund - favorite properties: id: $ref: Id name: type: string kind: $ref: ItemKind is_fund: type: boolean favorite: type: boolean fund_target: $ref: NullableCents - type: object required: - group_id - planned - spent - received - remaining - note properties: group_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ planned: type: integer format: int64 description: US cents. spent: allOf: - type: integer format: int64 description: US cents. description: Expense items; net of refunds. received: allOf: - type: integer format: int64 description: US cents. description: Income items. remaining: allOf: - type: integer format: int64 description: US cents. description: >- Row value. Funds: the running balance. Income: planned − received (negative = over-received, shown as +$X). note: type: string fund: type: - object - 'null' description: Present when is_fund. required: - carry_in - adjustments - ending properties: carry_in: type: integer format: int64 description: US cents. adjustments: type: array description: '"Balance Updated" entries this month (FUND-5).' items: type: object required: - amount - at properties: amount: type: integer format: int64 description: US cents. at: type: string format: date-time by: type: string ending: type: integer format: int64 description: US cents. totals: type: object required: - planned - spent - received - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. received: type: integer format: int64 description: US cents. remaining: allOf: - type: integer format: int64 description: US cents. description: >- Month-only (planned − spent; funds exclude carry-in). Income: planned − received. favorites: type: array description: Favorited item ids present in this month, in Favorites order (FAV-1). items: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ center: type: object description: Summary doughnut center (SUM-2). required: - planned - spent - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. remaining: type: integer format: int64 description: US cents. untracked_count: type: integer description: Household-wide (TXN-2a badge). copied_from: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' created_at: type: string format: date-time ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 409 `name_taken` (ITEM-6). #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Change this month's planned amount or note (ITEM-2, ITEM-8) Source: https://docs.spendrock.com/api/reference/items/updateMonthItem/ ## PATCH /months/{month}/items/{itemId} `PATCH https://app.spendrock.com/api/v1/months/{month}/items/{itemId}` Operation ID: `updateMonthItem` Scope: `budget:write` Change this month's planned amount or note (ITEM-2, ITEM-8) Required scope: `budget:write` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | | `month` | path | yes | string | | | `itemId` | path | yes | string | ULID. | ### Request body (required) ```yaml type: object properties: planned: type: integer format: int64 description: US cents. note: type: string maxLength: 2000 ``` ### Responses #### 200 The recomputed month. ```yaml type: object required: - month - left_to_budget - groups - favorites - center - untracked_count - copied_from - created_at properties: month: type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ left_to_budget: allOf: - type: integer format: int64 description: US cents. description: CALC-1. Positive = left to budget; 0 = "It's a SpendRock budget!"; negative = over budget. groups: type: array description: Income first, then user order. items: allOf: - type: object required: - id - name - color - is_income properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string color: type: string pattern: ^#[0-9a-fA-F]{6}$ is_income: type: boolean - type: object required: - items - totals properties: items: type: array items: description: >- An item as it appears in one month, with computed figures (PLAN §4, spec/calc-fixtures). allOf: - type: object description: Month-independent item attributes (shared by every month). required: - id - name - kind - is_fund - favorite properties: id: $ref: Id name: type: string kind: $ref: ItemKind is_fund: type: boolean favorite: type: boolean fund_target: $ref: NullableCents - type: object required: - group_id - planned - spent - received - remaining - note properties: group_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ planned: type: integer format: int64 description: US cents. spent: allOf: - type: integer format: int64 description: US cents. description: Expense items; net of refunds. received: allOf: - type: integer format: int64 description: US cents. description: Income items. remaining: allOf: - type: integer format: int64 description: US cents. description: >- Row value. Funds: the running balance. Income: planned − received (negative = over-received, shown as +$X). note: type: string fund: type: - object - 'null' description: Present when is_fund. required: - carry_in - adjustments - ending properties: carry_in: type: integer format: int64 description: US cents. adjustments: type: array description: '"Balance Updated" entries this month (FUND-5).' items: type: object required: - amount - at properties: amount: type: integer format: int64 description: US cents. at: type: string format: date-time by: type: string ending: type: integer format: int64 description: US cents. totals: type: object required: - planned - spent - received - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. received: type: integer format: int64 description: US cents. remaining: allOf: - type: integer format: int64 description: US cents. description: >- Month-only (planned − spent; funds exclude carry-in). Income: planned − received. favorites: type: array description: Favorited item ids present in this month, in Favorites order (FAV-1). items: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ center: type: object description: Summary doughnut center (SUM-2). required: - planned - spent - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. remaining: type: integer format: int64 description: US cents. untracked_count: type: integer description: Household-wide (TXN-2a badge). copied_from: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' created_at: type: string format: date-time ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 404 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Delete the item from THIS month only; its transactions in this month become untracked (ITEM-4) Source: https://docs.spendrock.com/api/reference/items/deleteItemFromMonth/ ## DELETE /months/{month}/items/{itemId} `DELETE https://app.spendrock.com/api/v1/months/{month}/items/{itemId}` Operation ID: `deleteItemFromMonth` Scope: `budget:write` Delete the item from THIS month only; its transactions in this month become untracked (ITEM-4) Required scope: `budget:write` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | | `month` | path | yes | string | | | `itemId` | path | yes | string | ULID. | ### Responses #### 200 The recomputed month. ```yaml type: object required: - month - left_to_budget - groups - favorites - center - untracked_count - copied_from - created_at properties: month: type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ left_to_budget: allOf: - type: integer format: int64 description: US cents. description: CALC-1. Positive = left to budget; 0 = "It's a SpendRock budget!"; negative = over budget. groups: type: array description: Income first, then user order. items: allOf: - type: object required: - id - name - color - is_income properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string color: type: string pattern: ^#[0-9a-fA-F]{6}$ is_income: type: boolean - type: object required: - items - totals properties: items: type: array items: description: >- An item as it appears in one month, with computed figures (PLAN §4, spec/calc-fixtures). allOf: - type: object description: Month-independent item attributes (shared by every month). required: - id - name - kind - is_fund - favorite properties: id: $ref: Id name: type: string kind: $ref: ItemKind is_fund: type: boolean favorite: type: boolean fund_target: $ref: NullableCents - type: object required: - group_id - planned - spent - received - remaining - note properties: group_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ planned: type: integer format: int64 description: US cents. spent: allOf: - type: integer format: int64 description: US cents. description: Expense items; net of refunds. received: allOf: - type: integer format: int64 description: US cents. description: Income items. remaining: allOf: - type: integer format: int64 description: US cents. description: >- Row value. Funds: the running balance. Income: planned − received (negative = over-received, shown as +$X). note: type: string fund: type: - object - 'null' description: Present when is_fund. required: - carry_in - adjustments - ending properties: carry_in: type: integer format: int64 description: US cents. adjustments: type: array description: '"Balance Updated" entries this month (FUND-5).' items: type: object required: - amount - at properties: amount: type: integer format: int64 description: US cents. at: type: string format: date-time by: type: string ending: type: integer format: int64 description: US cents. totals: type: object required: - planned - spent - received - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. received: type: integer format: int64 description: US cents. remaining: allOf: - type: integer format: int64 description: US cents. description: >- Month-only (planned − spent; funds exclude carry-in). Income: planned − received. favorites: type: array description: Favorited item ids present in this month, in Favorites order (FAV-1). items: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ center: type: object description: Summary doughnut center (SUM-2). required: - planned - spent - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. remaining: type: integer format: int64 description: US cents. untracked_count: type: integer description: Household-wide (TXN-2a badge). copied_from: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' created_at: type: string format: date-time ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 404 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # "Make This a Fund" (FUND-1). current_balance becomes the carry-in for this month. Source: https://docs.spendrock.com/api/reference/items/makeFund/ ## POST /months/{month}/items/{itemId}/fund `POST https://app.spendrock.com/api/v1/months/{month}/items/{itemId}/fund` Operation ID: `makeFund` Scope: `budget:write` "Make This a Fund" (FUND-1). current_balance becomes the carry-in for this month. Required scope: `budget:write` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | | `month` | path | yes | string | | | `itemId` | path | yes | string | ULID. | ### Request body (required) ```yaml type: object properties: current_balance: type: integer format: int64 description: US cents. target: type: - integer - 'null' format: int64 ``` ### Responses #### 200 The recomputed month. ```yaml type: object required: - month - left_to_budget - groups - favorites - center - untracked_count - copied_from - created_at properties: month: type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ left_to_budget: allOf: - type: integer format: int64 description: US cents. description: CALC-1. Positive = left to budget; 0 = "It's a SpendRock budget!"; negative = over budget. groups: type: array description: Income first, then user order. items: allOf: - type: object required: - id - name - color - is_income properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string color: type: string pattern: ^#[0-9a-fA-F]{6}$ is_income: type: boolean - type: object required: - items - totals properties: items: type: array items: description: >- An item as it appears in one month, with computed figures (PLAN §4, spec/calc-fixtures). allOf: - type: object description: Month-independent item attributes (shared by every month). required: - id - name - kind - is_fund - favorite properties: id: $ref: Id name: type: string kind: $ref: ItemKind is_fund: type: boolean favorite: type: boolean fund_target: $ref: NullableCents - type: object required: - group_id - planned - spent - received - remaining - note properties: group_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ planned: type: integer format: int64 description: US cents. spent: allOf: - type: integer format: int64 description: US cents. description: Expense items; net of refunds. received: allOf: - type: integer format: int64 description: US cents. description: Income items. remaining: allOf: - type: integer format: int64 description: US cents. description: >- Row value. Funds: the running balance. Income: planned − received (negative = over-received, shown as +$X). note: type: string fund: type: - object - 'null' description: Present when is_fund. required: - carry_in - adjustments - ending properties: carry_in: type: integer format: int64 description: US cents. adjustments: type: array description: '"Balance Updated" entries this month (FUND-5).' items: type: object required: - amount - at properties: amount: type: integer format: int64 description: US cents. at: type: string format: date-time by: type: string ending: type: integer format: int64 description: US cents. totals: type: object required: - planned - spent - received - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. received: type: integer format: int64 description: US cents. remaining: allOf: - type: integer format: int64 description: US cents. description: >- Month-only (planned − spent; funds exclude carry-in). Income: planned − received. favorites: type: array description: Favorited item ids present in this month, in Favorites order (FAV-1). items: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ center: type: object description: Summary doughnut center (SUM-2). required: - planned - spent - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. remaining: type: integer format: int64 description: US cents. untracked_count: type: integer description: Household-wide (TXN-2a badge). copied_from: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' created_at: type: string format: date-time ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 422 Income items can't be funds (`kind_mismatch`); already a fund (`already_fund`). #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Set the fund's current balance; records a "Balance Updated" adjustment for the difference (FUND-5). Never counts as Spent. Source: https://docs.spendrock.com/api/reference/items/setFundBalance/ ## POST /months/{month}/items/{itemId}/fund-balance `POST https://app.spendrock.com/api/v1/months/{month}/items/{itemId}/fund-balance` Operation ID: `setFundBalance` Scope: `budget:write` Set the fund's current balance; records a "Balance Updated" adjustment for the difference (FUND-5). Never counts as Spent. Required scope: `budget:write` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | | `month` | path | yes | string | | | `itemId` | path | yes | string | ULID. | ### Request body (required) ```yaml type: object required: - current_balance properties: current_balance: type: integer format: int64 description: US cents. ``` ### Responses #### 200 The recomputed month. ```yaml type: object required: - month - left_to_budget - groups - favorites - center - untracked_count - copied_from - created_at properties: month: type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ left_to_budget: allOf: - type: integer format: int64 description: US cents. description: CALC-1. Positive = left to budget; 0 = "It's a SpendRock budget!"; negative = over budget. groups: type: array description: Income first, then user order. items: allOf: - type: object required: - id - name - color - is_income properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string color: type: string pattern: ^#[0-9a-fA-F]{6}$ is_income: type: boolean - type: object required: - items - totals properties: items: type: array items: description: >- An item as it appears in one month, with computed figures (PLAN §4, spec/calc-fixtures). allOf: - type: object description: Month-independent item attributes (shared by every month). required: - id - name - kind - is_fund - favorite properties: id: $ref: Id name: type: string kind: $ref: ItemKind is_fund: type: boolean favorite: type: boolean fund_target: $ref: NullableCents - type: object required: - group_id - planned - spent - received - remaining - note properties: group_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ planned: type: integer format: int64 description: US cents. spent: allOf: - type: integer format: int64 description: US cents. description: Expense items; net of refunds. received: allOf: - type: integer format: int64 description: US cents. description: Income items. remaining: allOf: - type: integer format: int64 description: US cents. description: >- Row value. Funds: the running balance. Income: planned − received (negative = over-received, shown as +$X). note: type: string fund: type: - object - 'null' description: Present when is_fund. required: - carry_in - adjustments - ending properties: carry_in: type: integer format: int64 description: US cents. adjustments: type: array description: '"Balance Updated" entries this month (FUND-5).' items: type: object required: - amount - at properties: amount: type: integer format: int64 description: US cents. at: type: string format: date-time by: type: string ending: type: integer format: int64 description: US cents. totals: type: object required: - planned - spent - received - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. received: type: integer format: int64 description: US cents. remaining: allOf: - type: integer format: int64 description: US cents. description: >- Month-only (planned − spent; funds exclude carry-in). Income: planned − received. favorites: type: array description: Favorited item ids present in this month, in Favorites order (FAV-1). items: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ center: type: object description: Summary doughnut center (SUM-2). required: - planned - spent - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. remaining: type: integer format: int64 description: US cents. untracked_count: type: integer description: Household-wide (TXN-2a badge). copied_from: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' created_at: type: string format: date-time ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 422 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # "Harvest $X" (HARV-1..4): set this month's Planned = Spent so the remainder goes back to Left to Budget. Source: https://docs.spendrock.com/api/reference/items/harvestRemainder/ ## POST /months/{month}/items/{itemId}/harvest `POST https://app.spendrock.com/api/v1/months/{month}/items/{itemId}/harvest` Operation ID: `harvestRemainder` Scope: `budget:write` "Harvest $X" (HARV-1..4): set this month's Planned = Spent so the remainder goes back to Left to Budget. Only for regular expense items whose Remaining is above $0. `harvested` is the amount moved back to Left to Budget. Undo by sending `previous_planned` to `PATCH /months/{month}/items/{itemId}`. Required scope: `budget:write` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | | `month` | path | yes | string | | | `itemId` | path | yes | string | ULID. | ### Responses #### 200 The recomputed month, with what was harvested. ```yaml type: object required: - month - harvested - previous_planned properties: month: type: object required: - month - left_to_budget - groups - favorites - center - untracked_count - copied_from - created_at properties: month: type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ left_to_budget: allOf: - type: integer format: int64 description: US cents. description: CALC-1. Positive = left to budget; 0 = "It's a SpendRock budget!"; negative = over budget. groups: type: array description: Income first, then user order. items: allOf: - type: object required: - id - name - color - is_income properties: id: $ref: Id name: type: string color: $ref: Color is_income: type: boolean - type: object required: - items - totals properties: items: type: array items: description: >- An item as it appears in one month, with computed figures (PLAN §4, spec/calc-fixtures). allOf: - $ref: ItemIdentity - type: object required: - group_id - planned - spent - received - remaining - note properties: group_id: $ref: Id planned: $ref: Cents spent: allOf: - $ref: Cents description: Expense items; net of refunds. received: allOf: - $ref: Cents description: Income items. remaining: allOf: - $ref: Cents description: >- Row value. Funds: the running balance. Income: planned − received (negative = over-received, shown as +$X). note: type: string fund: type: - object - 'null' description: Present when is_fund. required: - carry_in - adjustments - ending properties: carry_in: $ref: Cents adjustments: type: array description: '"Balance Updated" entries this month (FUND-5).' items: type: object required: - amount - at properties: amount: $ref: Cents at: type: string format: date-time by: type: string ending: $ref: Cents totals: type: object required: - planned - spent - received - remaining properties: planned: $ref: Cents spent: $ref: Cents received: $ref: Cents remaining: allOf: - $ref: Cents description: >- Month-only (planned − spent; funds exclude carry-in). Income: planned − received. favorites: type: array description: Favorited item ids present in this month, in Favorites order (FAV-1). items: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ center: type: object description: Summary doughnut center (SUM-2). required: - planned - spent - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. remaining: type: integer format: int64 description: US cents. untracked_count: type: integer description: Household-wide (TXN-2a badge). copied_from: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' created_at: type: string format: date-time harvested: allOf: - type: integer format: int64 description: US cents. description: The amount moved back to Left to Budget (HARV-2). previous_planned: allOf: - type: integer format: int64 description: US cents. description: The item's planned amount before harvesting, for Undo (HARV-3). ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 404 Error. #### 422 Nothing remains (`nothing_to_harvest`); funds and income items can't be harvested (`kind_mismatch`); `month_not_created`. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Identity-level changes that apply to EVERY month (ITEM-3, ITEM-7, FUND-6) Source: https://docs.spendrock.com/api/reference/items/updateItem/ ## PATCH /items/{itemId} `PATCH https://app.spendrock.com/api/v1/items/{itemId}` Operation ID: `updateItem` Scope: `budget:write` Identity-level changes that apply to EVERY month (ITEM-3, ITEM-7, FUND-6) Required scope: `budget:write` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | | `itemId` | path | yes | string | ULID. | ### Request body (required) ```yaml type: object properties: name: type: string minLength: 1 maxLength: 80 favorite: type: boolean description: Income items can't be favorited. fund_target: type: - integer - 'null' format: int64 ``` ### Responses #### 200 Updated. ```yaml type: object description: Month-independent item attributes (shared by every month). required: - id - name - kind - is_fund - favorite properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string kind: type: string enum: - income - expense is_fund: type: boolean favorite: type: boolean fund_target: type: - integer - 'null' format: int64 ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 409 `name_taken`. #### 422 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Reorder the Favorites section (FAV-1). Independent of each item's order within its group. Source: https://docs.spendrock.com/api/reference/items/setFavoritesOrder/ ## PUT /favorites/order `PUT https://app.spendrock.com/api/v1/favorites/order` Operation ID: `setFavoritesOrder` Scope: `budget:write` Reorder the Favorites section (FAV-1). Independent of each item's order within its group. `item_ids` may list all favorites or only some (e.g. the favorites in the month being viewed). The listed favorites are placed, in the given order, into the positions they held between them in the household's current Favorites order; favorites that are not listed keep their positions. For example, with Favorites `[A, B, C, D]`, sending `[D, B]` gives `[A, D, C, B]`. Every id must be a current favorite, listed once; otherwise the request fails with `invalid`. Required scope: `budget:write` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | ### Request body (required) ```yaml type: object required: - item_ids properties: item_ids: type: array items: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ ``` ### Responses #### 204 Saved. #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 422 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # The Transactions panel tabs (TXN-8, TXN-2a, TXN-4a) Source: https://docs.spendrock.com/api/reference/transactions/listTransactions/ ## GET /transactions `GET https://app.spendrock.com/api/v1/transactions` Operation ID: `listTransactions` Scope: `transactions:read` The Transactions panel tabs (TXN-8, TXN-2a, TXN-4a) Required scope: `transactions:read` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | | `view` | query | yes | untracked \\| tracked \\| deleted | | | `month` | query | no | string | For `tracked`: limit to this budget month. | | `q` | query | no | string | Search by merchant (case-insensitive substring) or exact amount (e.g. `54.20`). | | `cursor` | query | no | string | | | `limit` | query | no | integer | | ### Responses #### 200 Newest first by date. ```yaml type: object required: - transactions - untracked_count properties: transactions: type: array items: type: object required: - id - type - amount - date - merchant - note - budget_month - splits - status - created_at - updated_at - created_by - created_by_name - updated_by - updated_by_name properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ type: type: string enum: - expense - income amount: type: integer format: int64 description: US cents. date: type: string format: date merchant: type: string note: type: string budget_month: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' splits: type: array items: allOf: - type: object required: - item_id - amount properties: item_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ amount: type: integer format: int64 description: US cents. - type: object required: - item_name properties: item_name: type: string status: type: string enum: - tracked - untracked - deleted deleted_at: type: - string - 'null' format: date-time location: anyOf: - type: object required: - lat - lng properties: lat: type: number format: double minimum: -90 maximum: 90 lng: type: number format: double minimum: -180 maximum: 180 accuracy_m: type: number format: double minimum: 0 - type: 'null' created_by: type: string description: User id (Google subject) of whoever added it (HH-13). created_by_name: type: string description: Their first name. Show it only when the household has more than one member (HH-13). updated_by: type: - string - 'null' description: Who last edited it; null if never edited. updated_by_name: type: - string - 'null' created_at: type: string format: date-time updated_at: type: string format: date-time next_cursor: type: - string - 'null' untracked_count: type: integer ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Add an expense or income (TXN-1). Idempotent on the client-generated id (SYNC-1). Source: https://docs.spendrock.com/api/reference/transactions/createTransaction/ ## POST /transactions `POST https://app.spendrock.com/api/v1/transactions` Operation ID: `createTransaction` Scope: `transactions:write` Add an expense or income (TXN-1). Idempotent on the client-generated id (SYNC-1). Required scope: `transactions:write` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | ### Request body (required) ```yaml type: object required: - id - type - amount - date - merchant - splits properties: id: allOf: - type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ description: Client-generated ULID; also the idempotency key. type: type: string enum: - expense - income amount: allOf: - type: integer format: int64 description: US cents. description: Always positive; direction comes from `type`. date: type: string format: date merchant: type: string maxLength: 120 note: type: string maxLength: 2000 budget_month: description: Required when splits are non-empty. Defaults to the viewed month in clients (TXN-5). anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' splits: type: array description: Empty = untracked (TXN-2). Otherwise they must sum to amount. items: type: object required: - item_id - amount properties: item_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ amount: type: integer format: int64 description: US cents. location: description: Mobile only, and only if opted in (TXN-13). anyOf: - type: object required: - lat - lng properties: lat: type: number format: double minimum: -90 maximum: 90 lng: type: number format: double minimum: -180 maximum: 180 accuracy_m: type: number format: double minimum: 0 - type: 'null' ``` ### Responses #### 200 Replay of an identical earlier create (same id + same body). Returns the stored result. ```yaml type: object required: - transaction - months - untracked_count properties: transaction: type: object required: - id - type - amount - date - merchant - note - budget_month - splits - status - created_at - updated_at - created_by - created_by_name - updated_by - updated_by_name properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ type: type: string enum: - expense - income amount: type: integer format: int64 description: US cents. date: type: string format: date merchant: type: string note: type: string budget_month: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' splits: type: array items: allOf: - type: object required: - item_id - amount properties: item_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ amount: type: integer format: int64 description: US cents. - type: object required: - item_name properties: item_name: type: string status: type: string enum: - tracked - untracked - deleted deleted_at: type: - string - 'null' format: date-time location: anyOf: - type: object required: - lat - lng properties: lat: type: number format: double minimum: -90 maximum: 90 lng: type: number format: double minimum: -180 maximum: 180 accuracy_m: type: number format: double minimum: 0 - type: 'null' created_by: type: string description: User id (Google subject) of whoever added it (HH-13). created_by_name: type: string description: Their first name. Show it only when the household has more than one member (HH-13). updated_by: type: - string - 'null' description: Who last edited it; null if never edited. updated_by_name: type: - string - 'null' created_at: type: string format: date-time updated_at: type: string format: date-time months: type: array description: Every month whose figures changed (0–2), recomputed. items: type: object required: - month - left_to_budget - groups - favorites - center - untracked_count - copied_from - created_at properties: month: type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ left_to_budget: allOf: - type: integer format: int64 description: US cents. description: >- CALC-1. Positive = left to budget; 0 = "It's a SpendRock budget!"; negative = over budget. groups: type: array description: Income first, then user order. items: allOf: - type: object required: - id - name - color - is_income properties: id: $ref: Id name: type: string color: $ref: Color is_income: type: boolean - type: object required: - items - totals properties: items: type: array items: description: >- An item as it appears in one month, with computed figures (PLAN §4, spec/calc-fixtures). allOf: - $ref: ItemIdentity - type: object required: - group_id - planned - spent - received - remaining - note properties: group_id: $ref: Id planned: $ref: Cents spent: allOf: - $ref: Cents description: Expense items; net of refunds. received: allOf: - $ref: Cents description: Income items. remaining: allOf: - $ref: Cents description: >- Row value. Funds: the running balance. Income: planned − received (negative = over-received, shown as +$X). note: type: string fund: type: - object - 'null' description: Present when is_fund. required: - carry_in - adjustments - ending properties: carry_in: $ref: Cents adjustments: type: array description: '"Balance Updated" entries this month (FUND-5).' items: type: object required: - amount - at properties: amount: $ref: Cents at: type: string format: date-time by: type: string ending: $ref: Cents totals: type: object required: - planned - spent - received - remaining properties: planned: $ref: Cents spent: $ref: Cents received: $ref: Cents remaining: allOf: - $ref: Cents description: >- Month-only (planned − spent; funds exclude carry-in). Income: planned − received. favorites: type: array description: Favorited item ids present in this month, in Favorites order (FAV-1). items: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ center: type: object description: Summary doughnut center (SUM-2). required: - planned - spent - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. remaining: type: integer format: int64 description: US cents. untracked_count: type: integer description: Household-wide (TXN-2a badge). copied_from: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' created_at: type: string format: date-time untracked_count: type: integer ``` #### 201 The transaction and every month it affected, recomputed. ```yaml type: object required: - transaction - months - untracked_count properties: transaction: type: object required: - id - type - amount - date - merchant - note - budget_month - splits - status - created_at - updated_at - created_by - created_by_name - updated_by - updated_by_name properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ type: type: string enum: - expense - income amount: type: integer format: int64 description: US cents. date: type: string format: date merchant: type: string note: type: string budget_month: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' splits: type: array items: allOf: - type: object required: - item_id - amount properties: item_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ amount: type: integer format: int64 description: US cents. - type: object required: - item_name properties: item_name: type: string status: type: string enum: - tracked - untracked - deleted deleted_at: type: - string - 'null' format: date-time location: anyOf: - type: object required: - lat - lng properties: lat: type: number format: double minimum: -90 maximum: 90 lng: type: number format: double minimum: -180 maximum: 180 accuracy_m: type: number format: double minimum: 0 - type: 'null' created_by: type: string description: User id (Google subject) of whoever added it (HH-13). created_by_name: type: string description: Their first name. Show it only when the household has more than one member (HH-13). updated_by: type: - string - 'null' description: Who last edited it; null if never edited. updated_by_name: type: - string - 'null' created_at: type: string format: date-time updated_at: type: string format: date-time months: type: array description: Every month whose figures changed (0–2), recomputed. items: type: object required: - month - left_to_budget - groups - favorites - center - untracked_count - copied_from - created_at properties: month: type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ left_to_budget: allOf: - type: integer format: int64 description: US cents. description: >- CALC-1. Positive = left to budget; 0 = "It's a SpendRock budget!"; negative = over budget. groups: type: array description: Income first, then user order. items: allOf: - type: object required: - id - name - color - is_income properties: id: $ref: Id name: type: string color: $ref: Color is_income: type: boolean - type: object required: - items - totals properties: items: type: array items: description: >- An item as it appears in one month, with computed figures (PLAN §4, spec/calc-fixtures). allOf: - $ref: ItemIdentity - type: object required: - group_id - planned - spent - received - remaining - note properties: group_id: $ref: Id planned: $ref: Cents spent: allOf: - $ref: Cents description: Expense items; net of refunds. received: allOf: - $ref: Cents description: Income items. remaining: allOf: - $ref: Cents description: >- Row value. Funds: the running balance. Income: planned − received (negative = over-received, shown as +$X). note: type: string fund: type: - object - 'null' description: Present when is_fund. required: - carry_in - adjustments - ending properties: carry_in: $ref: Cents adjustments: type: array description: '"Balance Updated" entries this month (FUND-5).' items: type: object required: - amount - at properties: amount: $ref: Cents at: type: string format: date-time by: type: string ending: $ref: Cents totals: type: object required: - planned - spent - received - remaining properties: planned: $ref: Cents spent: $ref: Cents received: $ref: Cents remaining: allOf: - $ref: Cents description: >- Month-only (planned − spent; funds exclude carry-in). Income: planned − received. favorites: type: array description: Favorited item ids present in this month, in Favorites order (FAV-1). items: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ center: type: object description: Summary doughnut center (SUM-2). required: - planned - spent - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. remaining: type: integer format: int64 description: US cents. untracked_count: type: integer description: Household-wide (TXN-2a badge). copied_from: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' created_at: type: string format: date-time untracked_count: type: integer ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 409 Same id, different body (`id_conflict`). #### 422 Validation: `amount_not_positive`, `splits_do_not_sum`, `split_not_positive`, `duplicate_split_item`, `item_not_in_month` (SYNC-4), `month_not_created`, `kind_mismatch`. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Get Transaction Source: https://docs.spendrock.com/api/reference/transactions/getTransaction/ ## GET /transactions/{transactionId} `GET https://app.spendrock.com/api/v1/transactions/{transactionId}` Operation ID: `getTransaction` Scope: `transactions:read` Required scope: `transactions:read` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | | `transactionId` | path | yes | string | ULID. | ### Responses #### 200 The transaction. ```yaml type: object required: - id - type - amount - date - merchant - note - budget_month - splits - status - created_at - updated_at - created_by - created_by_name - updated_by - updated_by_name properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ type: type: string enum: - expense - income amount: type: integer format: int64 description: US cents. date: type: string format: date merchant: type: string note: type: string budget_month: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' splits: type: array items: allOf: - type: object required: - item_id - amount properties: item_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ amount: type: integer format: int64 description: US cents. - type: object required: - item_name properties: item_name: type: string status: type: string enum: - tracked - untracked - deleted deleted_at: type: - string - 'null' format: date-time location: anyOf: - type: object required: - lat - lng properties: lat: type: number format: double minimum: -90 maximum: 90 lng: type: number format: double minimum: -180 maximum: 180 accuracy_m: type: number format: double minimum: 0 - type: 'null' created_by: type: string description: User id (Google subject) of whoever added it (HH-13). created_by_name: type: string description: Their first name. Show it only when the household has more than one member (HH-13). updated_by: type: - string - 'null' description: Who last edited it; null if never edited. updated_by_name: type: - string - 'null' created_at: type: string format: date-time updated_at: type: string format: date-time ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 404 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Edit amount/date/merchant/note/type/splits/budget month, untrack (empty splits), or remove the location (TXN-3, TXN-4, TXN-5) Source: https://docs.spendrock.com/api/reference/transactions/updateTransaction/ ## PATCH /transactions/{transactionId} `PATCH https://app.spendrock.com/api/v1/transactions/{transactionId}` Operation ID: `updateTransaction` Scope: `transactions:write` Edit amount/date/merchant/note/type/splits/budget month, untrack (empty splits), or remove the location (TXN-3, TXN-4, TXN-5) Required scope: `transactions:write` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | | `transactionId` | path | yes | string | ULID. | ### Request body (required) ```yaml type: object description: 'Only the fields present are changed. `splits: []` untracks. `location: null` removes the location.' properties: type: type: string enum: - expense - income amount: type: integer format: int64 description: US cents. date: type: string format: date merchant: type: string maxLength: 120 note: type: string maxLength: 2000 budget_month: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' splits: type: array items: type: object required: - item_id - amount properties: item_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ amount: type: integer format: int64 description: US cents. location: anyOf: - type: object required: - lat - lng properties: lat: type: number format: double minimum: -90 maximum: 90 lng: type: number format: double minimum: -180 maximum: 180 accuracy_m: type: number format: double minimum: 0 - type: 'null' ``` ### Responses #### 200 The transaction and every month it affected, recomputed. ```yaml type: object required: - transaction - months - untracked_count properties: transaction: type: object required: - id - type - amount - date - merchant - note - budget_month - splits - status - created_at - updated_at - created_by - created_by_name - updated_by - updated_by_name properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ type: type: string enum: - expense - income amount: type: integer format: int64 description: US cents. date: type: string format: date merchant: type: string note: type: string budget_month: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' splits: type: array items: allOf: - type: object required: - item_id - amount properties: item_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ amount: type: integer format: int64 description: US cents. - type: object required: - item_name properties: item_name: type: string status: type: string enum: - tracked - untracked - deleted deleted_at: type: - string - 'null' format: date-time location: anyOf: - type: object required: - lat - lng properties: lat: type: number format: double minimum: -90 maximum: 90 lng: type: number format: double minimum: -180 maximum: 180 accuracy_m: type: number format: double minimum: 0 - type: 'null' created_by: type: string description: User id (Google subject) of whoever added it (HH-13). created_by_name: type: string description: Their first name. Show it only when the household has more than one member (HH-13). updated_by: type: - string - 'null' description: Who last edited it; null if never edited. updated_by_name: type: - string - 'null' created_at: type: string format: date-time updated_at: type: string format: date-time months: type: array description: Every month whose figures changed (0–2), recomputed. items: type: object required: - month - left_to_budget - groups - favorites - center - untracked_count - copied_from - created_at properties: month: type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ left_to_budget: allOf: - type: integer format: int64 description: US cents. description: >- CALC-1. Positive = left to budget; 0 = "It's a SpendRock budget!"; negative = over budget. groups: type: array description: Income first, then user order. items: allOf: - type: object required: - id - name - color - is_income properties: id: $ref: Id name: type: string color: $ref: Color is_income: type: boolean - type: object required: - items - totals properties: items: type: array items: description: >- An item as it appears in one month, with computed figures (PLAN §4, spec/calc-fixtures). allOf: - $ref: ItemIdentity - type: object required: - group_id - planned - spent - received - remaining - note properties: group_id: $ref: Id planned: $ref: Cents spent: allOf: - $ref: Cents description: Expense items; net of refunds. received: allOf: - $ref: Cents description: Income items. remaining: allOf: - $ref: Cents description: >- Row value. Funds: the running balance. Income: planned − received (negative = over-received, shown as +$X). note: type: string fund: type: - object - 'null' description: Present when is_fund. required: - carry_in - adjustments - ending properties: carry_in: $ref: Cents adjustments: type: array description: '"Balance Updated" entries this month (FUND-5).' items: type: object required: - amount - at properties: amount: $ref: Cents at: type: string format: date-time by: type: string ending: $ref: Cents totals: type: object required: - planned - spent - received - remaining properties: planned: $ref: Cents spent: $ref: Cents received: $ref: Cents remaining: allOf: - $ref: Cents description: >- Month-only (planned − spent; funds exclude carry-in). Income: planned − received. favorites: type: array description: Favorited item ids present in this month, in Favorites order (FAV-1). items: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ center: type: object description: Summary doughnut center (SUM-2). required: - planned - spent - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. remaining: type: integer format: int64 description: US cents. untracked_count: type: integer description: Household-wide (TXN-2a badge). copied_from: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' created_at: type: string format: date-time untracked_count: type: integer ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 404 Error. #### 422 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Soft delete, no confirmation; the client offers Undo (TXN-4) Source: https://docs.spendrock.com/api/reference/transactions/deleteTransaction/ ## DELETE /transactions/{transactionId} `DELETE https://app.spendrock.com/api/v1/transactions/{transactionId}` Operation ID: `deleteTransaction` Scope: `transactions:write` Soft delete, no confirmation; the client offers Undo (TXN-4) Required scope: `transactions:write` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | | `transactionId` | path | yes | string | ULID. | ### Responses #### 200 The transaction and every month it affected, recomputed. ```yaml type: object required: - transaction - months - untracked_count properties: transaction: type: object required: - id - type - amount - date - merchant - note - budget_month - splits - status - created_at - updated_at - created_by - created_by_name - updated_by - updated_by_name properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ type: type: string enum: - expense - income amount: type: integer format: int64 description: US cents. date: type: string format: date merchant: type: string note: type: string budget_month: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' splits: type: array items: allOf: - type: object required: - item_id - amount properties: item_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ amount: type: integer format: int64 description: US cents. - type: object required: - item_name properties: item_name: type: string status: type: string enum: - tracked - untracked - deleted deleted_at: type: - string - 'null' format: date-time location: anyOf: - type: object required: - lat - lng properties: lat: type: number format: double minimum: -90 maximum: 90 lng: type: number format: double minimum: -180 maximum: 180 accuracy_m: type: number format: double minimum: 0 - type: 'null' created_by: type: string description: User id (Google subject) of whoever added it (HH-13). created_by_name: type: string description: Their first name. Show it only when the household has more than one member (HH-13). updated_by: type: - string - 'null' description: Who last edited it; null if never edited. updated_by_name: type: - string - 'null' created_at: type: string format: date-time updated_at: type: string format: date-time months: type: array description: Every month whose figures changed (0–2), recomputed. items: type: object required: - month - left_to_budget - groups - favorites - center - untracked_count - copied_from - created_at properties: month: type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ left_to_budget: allOf: - type: integer format: int64 description: US cents. description: >- CALC-1. Positive = left to budget; 0 = "It's a SpendRock budget!"; negative = over budget. groups: type: array description: Income first, then user order. items: allOf: - type: object required: - id - name - color - is_income properties: id: $ref: Id name: type: string color: $ref: Color is_income: type: boolean - type: object required: - items - totals properties: items: type: array items: description: >- An item as it appears in one month, with computed figures (PLAN §4, spec/calc-fixtures). allOf: - $ref: ItemIdentity - type: object required: - group_id - planned - spent - received - remaining - note properties: group_id: $ref: Id planned: $ref: Cents spent: allOf: - $ref: Cents description: Expense items; net of refunds. received: allOf: - $ref: Cents description: Income items. remaining: allOf: - $ref: Cents description: >- Row value. Funds: the running balance. Income: planned − received (negative = over-received, shown as +$X). note: type: string fund: type: - object - 'null' description: Present when is_fund. required: - carry_in - adjustments - ending properties: carry_in: $ref: Cents adjustments: type: array description: '"Balance Updated" entries this month (FUND-5).' items: type: object required: - amount - at properties: amount: $ref: Cents at: type: string format: date-time by: type: string ending: $ref: Cents totals: type: object required: - planned - spent - received - remaining properties: planned: $ref: Cents spent: $ref: Cents received: $ref: Cents remaining: allOf: - $ref: Cents description: >- Month-only (planned − spent; funds exclude carry-in). Income: planned − received. favorites: type: array description: Favorited item ids present in this month, in Favorites order (FAV-1). items: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ center: type: object description: Summary doughnut center (SUM-2). required: - planned - spent - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. remaining: type: integer format: int64 description: US cents. untracked_count: type: integer description: Household-wide (TXN-2a badge). copied_from: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' created_at: type: string format: date-time untracked_count: type: integer ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 404 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Restore with original tracking; splits whose item no longer exists in that month are dropped (TXN-4a) Source: https://docs.spendrock.com/api/reference/transactions/restoreTransaction/ ## POST /transactions/{transactionId}/restore `POST https://app.spendrock.com/api/v1/transactions/{transactionId}/restore` Operation ID: `restoreTransaction` Scope: `transactions:write` Restore with original tracking; splits whose item no longer exists in that month are dropped (TXN-4a) Required scope: `transactions:write` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | | `transactionId` | path | yes | string | ULID. | ### Responses #### 200 The transaction and every month it affected, recomputed. ```yaml type: object required: - transaction - months - untracked_count properties: transaction: type: object required: - id - type - amount - date - merchant - note - budget_month - splits - status - created_at - updated_at - created_by - created_by_name - updated_by - updated_by_name properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ type: type: string enum: - expense - income amount: type: integer format: int64 description: US cents. date: type: string format: date merchant: type: string note: type: string budget_month: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' splits: type: array items: allOf: - type: object required: - item_id - amount properties: item_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ amount: type: integer format: int64 description: US cents. - type: object required: - item_name properties: item_name: type: string status: type: string enum: - tracked - untracked - deleted deleted_at: type: - string - 'null' format: date-time location: anyOf: - type: object required: - lat - lng properties: lat: type: number format: double minimum: -90 maximum: 90 lng: type: number format: double minimum: -180 maximum: 180 accuracy_m: type: number format: double minimum: 0 - type: 'null' created_by: type: string description: User id (Google subject) of whoever added it (HH-13). created_by_name: type: string description: Their first name. Show it only when the household has more than one member (HH-13). updated_by: type: - string - 'null' description: Who last edited it; null if never edited. updated_by_name: type: - string - 'null' created_at: type: string format: date-time updated_at: type: string format: date-time months: type: array description: Every month whose figures changed (0–2), recomputed. items: type: object required: - month - left_to_budget - groups - favorites - center - untracked_count - copied_from - created_at properties: month: type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ left_to_budget: allOf: - type: integer format: int64 description: US cents. description: >- CALC-1. Positive = left to budget; 0 = "It's a SpendRock budget!"; negative = over budget. groups: type: array description: Income first, then user order. items: allOf: - type: object required: - id - name - color - is_income properties: id: $ref: Id name: type: string color: $ref: Color is_income: type: boolean - type: object required: - items - totals properties: items: type: array items: description: >- An item as it appears in one month, with computed figures (PLAN §4, spec/calc-fixtures). allOf: - $ref: ItemIdentity - type: object required: - group_id - planned - spent - received - remaining - note properties: group_id: $ref: Id planned: $ref: Cents spent: allOf: - $ref: Cents description: Expense items; net of refunds. received: allOf: - $ref: Cents description: Income items. remaining: allOf: - $ref: Cents description: >- Row value. Funds: the running balance. Income: planned − received (negative = over-received, shown as +$X). note: type: string fund: type: - object - 'null' description: Present when is_fund. required: - carry_in - adjustments - ending properties: carry_in: $ref: Cents adjustments: type: array description: '"Balance Updated" entries this month (FUND-5).' items: type: object required: - amount - at properties: amount: $ref: Cents at: type: string format: date-time by: type: string ending: $ref: Cents totals: type: object required: - planned - spent - received - remaining properties: planned: $ref: Cents spent: $ref: Cents received: $ref: Cents remaining: allOf: - $ref: Cents description: >- Month-only (planned − spent; funds exclude carry-in). Income: planned − received. favorites: type: array description: Favorited item ids present in this month, in Favorites order (FAV-1). items: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ center: type: object description: Summary doughnut center (SUM-2). required: - planned - spent - remaining properties: planned: type: integer format: int64 description: US cents. spent: type: integer format: int64 description: US cents. remaining: type: integer format: int64 description: US cents. untracked_count: type: integer description: Household-wide (TXN-2a badge). copied_from: anyOf: - type: string pattern: ^[0-9]{4}-(0[1-9]|1[0-2])$ - type: 'null' created_at: type: string format: date-time untracked_count: type: integer ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 404 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # The full merchant → last-used item map, cached on clients (TXN-9, PERF-5) Source: https://docs.spendrock.com/api/reference/suggestions/listMerchantHints/ ## GET /suggestions/merchants `GET https://app.spendrock.com/api/v1/suggestions/merchants` Operation ID: `listMerchantHints` Scope: `transactions:read` The full merchant → last-used item map, cached on clients (TXN-9, PERF-5) Required scope: `transactions:read` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | ### Responses #### 200 All hints for the household. ```yaml type: object required: - hints properties: hints: type: array items: type: object required: - merchant - type - item_id properties: merchant: type: string description: Display form of the most recent use. type: type: string enum: - expense - income item_id: allOf: - type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ description: >- The largest split's item on the most recent tracked transaction with this merchant (TXN-9). ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Get Settings Source: https://docs.spendrock.com/api/reference/settings/getSettings/ ## GET /settings `GET https://app.spendrock.com/api/v1/settings` Operation ID: `getSettings` Scope: `account:read` Required scope: `account:read` (personal access tokens; sessions have every scope). ### Responses #### 200 Current user's settings. ```yaml type: object required: - column_view - location_opt_in - theme properties: column_view: type: string enum: - remaining - spent description: LAYOUT-9 global toggle. `spent` shows Spent on expense groups and Received on Income. location_opt_in: type: boolean description: TXN-13; off by default. theme: type: string enum: - light - dark - system description: UI-20 web color scheme. `system` follows the browser's prefers-color-scheme. ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Update Settings Source: https://docs.spendrock.com/api/reference/settings/updateSettings/ ## PATCH /settings `PATCH https://app.spendrock.com/api/v1/settings` Operation ID: `updateSettings` Scope: `account:write` Required scope: `account:write` (personal access tokens; sessions have every scope). ### Request body (required) ```yaml type: object properties: column_view: type: string enum: - remaining - spent description: LAYOUT-9 global toggle. `spent` shows Spent on expense groups and Received on Income. location_opt_in: type: boolean theme: type: string enum: - light - dark - system description: UI-20 web color scheme. `system` follows the browser's prefers-color-scheme. ``` ### Responses #### 200 Updated. ```yaml type: object required: - column_view - location_opt_in - theme properties: column_view: type: string enum: - remaining - spent description: LAYOUT-9 global toggle. `spent` shows Spent on expense groups and Received on Income. location_opt_in: type: boolean description: TXN-13; off by default. theme: type: string enum: - light - dark - system description: UI-20 web color scheme. `system` follows the browser's prefers-color-scheme. ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # "Delete all saved locations" (TXN-13) Source: https://docs.spendrock.com/api/reference/settings/deleteAllLocations/ ## DELETE /settings/locations `DELETE https://app.spendrock.com/api/v1/settings/locations` Operation ID: `deleteAllLocations` Scope: `account:write` "Delete all saved locations" (TXN-13) Required scope: `account:write` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `X-SpendRock-Household` | header | no | string | The household to act on (HH-12). Defaults to the user's default household. 403 `not_a_member` if the caller isn't a current member (HH-16). | ### Responses #### 204 All transaction locations removed. #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # My households, default first (HH-11). Archived ones only with include_archived (HH-19). Source: https://docs.spendrock.com/api/reference/households/listHouseholds/ ## GET /households `GET https://app.spendrock.com/api/v1/households` Operation ID: `listHouseholds` Scope: `households:read` My households, default first (HH-11). Archived ones only with include_archived (HH-19). Required scope: `households:read` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `include_archived` | query | no | boolean | | ### Responses #### 200 Households. ```yaml type: object required: - households properties: households: type: array items: type: object required: - id - name - role - personal - default - archived - member_count - joined_at properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string role: type: string enum: - owner - member personal: type: boolean description: Created at its owner's first sign-in; can't be deleted (HH-18). default: type: boolean description: My default (HH-10). archived: type: boolean description: Archived by me (per user HH-19).: null member_count: type: integer joined_at: type: string format: date-time ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Create an additional household that I own (HH-11) Source: https://docs.spendrock.com/api/reference/households/createHousehold/ ## POST /households `POST https://app.spendrock.com/api/v1/households` Operation ID: `createHousehold` Scope: `households:manage` Create an additional household that I own (HH-11) Required scope: `households:manage` (personal access tokens; sessions have every scope). ### Request body (required) ```yaml type: object required: - name properties: name: type: string minLength: 1 maxLength: 80 ``` ### Responses #### 201 Created. ```yaml type: object required: - id - name - role - personal - default - archived - member_count - joined_at properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string role: type: string enum: - owner - member personal: type: boolean description: Created at its owner's first sign-in; can't be deleted (HH-18). default: type: boolean description: My default (HH-10). archived: type: boolean description: Archived by me (per user HH-19).: null member_count: type: integer joined_at: type: string format: date-time ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 422 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Get Household Source: https://docs.spendrock.com/api/reference/households/getHousehold/ ## GET /households/{householdId} `GET https://app.spendrock.com/api/v1/households/{householdId}` Operation ID: `getHousehold` Scope: `households:read` Required scope: `households:read` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `householdId` | path | yes | string | ULID. | ### Responses #### 200 The household, from my point of view. ```yaml type: object required: - id - name - role - personal - default - archived - member_count - joined_at properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string role: type: string enum: - owner - member personal: type: boolean description: Created at its owner's first sign-in; can't be deleted (HH-18). default: type: boolean description: My default (HH-10). archived: type: boolean description: Archived by me (per user HH-19).: null member_count: type: integer joined_at: type: string format: date-time ``` #### 403 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Rename (owner only, HH-2) Source: https://docs.spendrock.com/api/reference/households/renameHousehold/ ## PATCH /households/{householdId} `PATCH https://app.spendrock.com/api/v1/households/{householdId}` Operation ID: `renameHousehold` Scope: `households:manage` Rename (owner only, HH-2) Required scope: `households:manage` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `householdId` | path | yes | string | ULID. | ### Request body (required) ```yaml type: object required: - name properties: name: type: string minLength: 1 maxLength: 80 ``` ### Responses #### 200 Renamed. ```yaml type: object required: - id - name - role - personal - default - archived - member_count - joined_at properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string role: type: string enum: - owner - member personal: type: boolean description: Created at its owner's first sign-in; can't be deleted (HH-18). default: type: boolean description: My default (HH-10). archived: type: boolean description: Archived by me (per user HH-19).: null member_count: type: integer joined_at: type: string format: date-time ``` #### 403 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Delete for all members (owner only; never a personal household). A soft delete; members whose default it was fall back to their personal household (HH-18). Source: https://docs.spendrock.com/api/reference/households/deleteHousehold/ ## DELETE /households/{householdId} `DELETE https://app.spendrock.com/api/v1/households/{householdId}` Operation ID: `deleteHousehold` Scope: `households:manage` Delete for all members (owner only; never a personal household). A soft delete; members whose default it was fall back to their personal household (HH-18). Required scope: `households:manage` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `householdId` | path | yes | string | ULID. | ### Responses #### 204 Deleted. #### 403 Error. #### 422 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Make it my default (HH-10). Not an archived household (`household_archived`). Source: https://docs.spendrock.com/api/reference/households/setDefaultHousehold/ ## POST /households/{householdId}/default `POST https://app.spendrock.com/api/v1/households/{householdId}/default` Operation ID: `setDefaultHousehold` Scope: `households:manage` Make it my default (HH-10). Not an archived household (`household_archived`). Required scope: `households:manage` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `householdId` | path | yes | string | ULID. | ### Responses #### 204 Default changed. #### 403 Error. #### 422 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Hide it from my lists (per user; never deletes anything; HH-19). Not my default (`default_household`), not my last non-archived one (`last_household`). Source: https://docs.spendrock.com/api/reference/households/archiveHousehold/ ## POST /households/{householdId}/archive `POST https://app.spendrock.com/api/v1/households/{householdId}/archive` Operation ID: `archiveHousehold` Scope: `households:manage` Hide it from my lists (per user; never deletes anything; HH-19). Not my default (`default_household`), not my last non-archived one (`last_household`). Required scope: `households:manage` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `householdId` | path | yes | string | ULID. | ### Responses #### 204 Archived. #### 403 Error. #### 422 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Show it in my lists again (HH-19) Source: https://docs.spendrock.com/api/reference/households/unarchiveHousehold/ ## POST /households/{householdId}/unarchive `POST https://app.spendrock.com/api/v1/households/{householdId}/unarchive` Operation ID: `unarchiveHousehold` Scope: `households:manage` Show it in my lists again (HH-19) Required scope: `households:manage` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `householdId` | path | yes | string | ULID. | ### Responses #### 204 Unarchived. #### 403 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Leave (HH-2, HH-3). Owners must transfer first unless alone (a sole owner leaving deletes it); a personal household's owner can't leave it. Source: https://docs.spendrock.com/api/reference/households/leaveHousehold/ ## POST /households/{householdId}/leave `POST https://app.spendrock.com/api/v1/households/{householdId}/leave` Operation ID: `leaveHousehold` Scope: `households:manage` Leave (HH-2, HH-3). Owners must transfer first unless alone (a sole owner leaving deletes it); a personal household's owner can't leave it. Required scope: `households:manage` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `householdId` | path | yes | string | ULID. | ### Request body ```yaml type: object properties: new_default_household_id: allOf: - type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ description: >- Required (`choose_default`) when leaving my default and more than one other household could replace it. ``` ### Responses #### 204 Left. #### 403 Error. #### 422 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Make another member the owner (owner only, HH-2). Not for personal households. Source: https://docs.spendrock.com/api/reference/households/transferOwnership/ ## POST /households/{householdId}/transfer `POST https://app.spendrock.com/api/v1/households/{householdId}/transfer` Operation ID: `transferOwnership` Scope: `households:manage` Make another member the owner (owner only, HH-2). Not for personal households. Required scope: `households:manage` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `householdId` | path | yes | string | ULID. | ### Request body (required) ```yaml type: object required: - user_id properties: user_id: type: string ``` ### Responses #### 204 Transferred. #### 403 Error. #### 404 Error. #### 422 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # List Members Source: https://docs.spendrock.com/api/reference/households/listMembers/ ## GET /households/{householdId}/members `GET https://app.spendrock.com/api/v1/households/{householdId}/members` Operation ID: `listMembers` Scope: `households:read` Required scope: `households:read` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `householdId` | path | yes | string | ULID. | ### Responses #### 200 Current members, oldest first. ```yaml type: object required: - members properties: members: type: array items: type: object required: - user_id - name - first_name - email - role - joined_at properties: user_id: type: string name: type: string first_name: type: string description: Shown on transactions (HH-13). email: type: string role: type: string enum: - owner - member joined_at: type: string format: date-time ``` #### 403 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Remove a member (owner only, HH-2). They lose access on their next request (HH-16). Source: https://docs.spendrock.com/api/reference/households/removeMember/ ## DELETE /households/{householdId}/members/{userId} `DELETE https://app.spendrock.com/api/v1/households/{householdId}/members/{userId}` Operation ID: `removeMember` Scope: `households:manage` Remove a member (owner only, HH-2). They lose access on their next request (HH-16). Required scope: `households:manage` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `householdId` | path | yes | string | ULID. | | `userId` | path | yes | string | The member's user id (Google subject). | ### Responses #### 204 Removed. #### 403 Error. #### 404 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Pending, unexpired invites with their links (HH-9) Source: https://docs.spendrock.com/api/reference/invites/listHouseholdInvites/ ## GET /households/{householdId}/invites `GET https://app.spendrock.com/api/v1/households/{householdId}/invites` Operation ID: `listHouseholdInvites` Scope: `households:read` Pending, unexpired invites with their links (HH-9) Required scope: `households:read` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `householdId` | path | yes | string | ULID. | ### Responses #### 200 Invites. ```yaml type: object required: - invites properties: invites: type: array items: type: object required: - id - household_id - household_name - email - inviter_id - inviter_name - created_at - expires_at - status - member_names properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ household_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ household_name: type: string email: type: string description: Normalized (lower case). inviter_id: type: string inviter_name: type: string description: First name. created_at: type: string format: date-time expires_at: type: string format: date-time status: type: string enum: - pending - accepted - declined - revoked - expired token: type: string description: The link secret. Only for the household's members and the invitee. path: type: string description: '`/invite/{token}`, relative to the web origin. Present with `token`.' member_names: type: array description: Current members' first names (for the HH-8 explanation). items: type: string ``` #### 403 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Invite by email (any member, HH-4). Expires in 7 days; re-inviting the same email replaces the pending invite (HH-9). Source: https://docs.spendrock.com/api/reference/invites/createInvite/ ## POST /households/{householdId}/invites `POST https://app.spendrock.com/api/v1/households/{householdId}/invites` Operation ID: `createInvite` Scope: `households:manage` Invite by email (any member, HH-4). Expires in 7 days; re-inviting the same email replaces the pending invite (HH-9). Required scope: `households:manage` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `householdId` | path | yes | string | ULID. | ### Request body (required) ```yaml type: object required: - email properties: email: type: string maxLength: 254 ``` ### Responses #### 201 Created. `token`/`path` form the copyable link. ```yaml type: object required: - id - household_id - household_name - email - inviter_id - inviter_name - created_at - expires_at - status - member_names properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ household_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ household_name: type: string email: type: string description: Normalized (lower case). inviter_id: type: string inviter_name: type: string description: First name. created_at: type: string format: date-time expires_at: type: string format: date-time status: type: string enum: - pending - accepted - declined - revoked - expired token: type: string description: The link secret. Only for the household's members and the invitee. path: type: string description: '`/invite/{token}`, relative to the web origin. Present with `token`.' member_names: type: array description: Current members' first names (for the HH-8 explanation). items: type: string ``` #### 403 Error. #### 409 Error. #### 422 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Revoke (the owner, or whoever sent it; HH-2) Source: https://docs.spendrock.com/api/reference/invites/revokeInvite/ ## DELETE /households/{householdId}/invites/{inviteId} `DELETE https://app.spendrock.com/api/v1/households/{householdId}/invites/{inviteId}` Operation ID: `revokeInvite` Scope: `households:manage` Revoke (the owner, or whoever sent it; HH-2) Required scope: `households:manage` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `householdId` | path | yes | string | ULID. | | `inviteId` | path | yes | string | ULID. | ### Responses #### 204 Revoked. #### 403 Error. #### 404 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Pending invites addressed to my email (the HH-5 banner) Source: https://docs.spendrock.com/api/reference/invites/listMyInvites/ ## GET /invites `GET https://app.spendrock.com/api/v1/invites` Operation ID: `listMyInvites` Scope: `households:read` Pending invites addressed to my email (the HH-5 banner) Required scope: `households:read` (personal access tokens; sessions have every scope). ### Responses #### 200 Invites. ```yaml type: object required: - invites properties: invites: type: array items: type: object required: - id - household_id - household_name - email - inviter_id - inviter_name - created_at - expires_at - status - member_names properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ household_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ household_name: type: string email: type: string description: Normalized (lower case). inviter_id: type: string inviter_name: type: string description: First name. created_at: type: string format: date-time expires_at: type: string format: date-time status: type: string enum: - pending - accepted - declined - revoked - expired token: type: string description: The link secret. Only for the household's members and the invitee. path: type: string description: '`/invite/{token}`, relative to the web origin. Present with `token`.' member_names: type: array description: Current members' first names (for the HH-8 explanation). items: type: string ``` #### 403 The caller can't do this: e.g. `insufficient_scope` (a personal access token without the scope in `details.required_scope`), `session_required`, `not_a_member`. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Look up an invite link in any status (HH-6). No sign-in needed; the token isn't echoed. Source: https://docs.spendrock.com/api/reference/invites/getInvite/ ## GET /invites/{token} `GET https://app.spendrock.com/api/v1/invites/{token}` Operation ID: `getInvite` Scope: `public` Look up an invite link in any status (HH-6). No sign-in needed; the token isn't echoed. No authentication needed. ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `token` | path | yes | string | The secret from the invite link (`/invite/{token}`). | ### Responses #### 200 The invite. ```yaml type: object required: - id - household_id - household_name - email - inviter_id - inviter_name - created_at - expires_at - status - member_names properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ household_id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ household_name: type: string email: type: string description: Normalized (lower case). inviter_id: type: string inviter_name: type: string description: First name. created_at: type: string format: date-time expires_at: type: string format: date-time status: type: string enum: - pending - accepted - declined - revoked - expired token: type: string description: The link secret. Only for the household's members and the invitee. path: type: string description: '`/invite/{token}`, relative to the web origin. Present with `token`.' member_names: type: array description: Current members' first names (for the HH-8 explanation). items: type: string ``` #### 404 Error. --- # Join (HH-8). My email must match the invite (`invite_email_mismatch`). Source: https://docs.spendrock.com/api/reference/invites/acceptInvite/ ## POST /invites/{token}/accept `POST https://app.spendrock.com/api/v1/invites/{token}/accept` Operation ID: `acceptInvite` Scope: `households:manage` Join (HH-8). My email must match the invite (`invite_email_mismatch`). Required scope: `households:manage` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `token` | path | yes | string | The secret from the invite link (`/invite/{token}`). | ### Request body ```yaml type: object properties: make_default: type: boolean default: false description: Make it my default household. archive_current: type: boolean default: false description: Archive my current default. Implies make_default. ``` ### Responses #### 200 Joined. ```yaml type: object required: - id - name - role - personal - default - archived - member_count - joined_at properties: id: type: string description: ULID. pattern: ^[0-9A-HJKMNP-TV-Z]{26}$ name: type: string role: type: string enum: - owner - member personal: type: boolean description: Created at its owner's first sign-in; can't be deleted (HH-18). default: type: boolean description: My default (HH-10). archived: type: boolean description: Archived by me (per user HH-19).: null member_count: type: integer joined_at: type: string format: date-time ``` #### 403 Error. #### 404 Error. #### 410 Expired, revoked, declined, or already accepted (`invite_unavailable`, `details.status`). #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds. --- # Decline Invite Source: https://docs.spendrock.com/api/reference/invites/declineInvite/ ## POST /invites/{token}/decline `POST https://app.spendrock.com/api/v1/invites/{token}/decline` Operation ID: `declineInvite` Scope: `households:manage` Required scope: `households:manage` (personal access tokens; sessions have every scope). ### Parameters | Name | In | Required | Schema | Description | |---|---|---|---|---| | `token` | path | yes | string | The secret from the invite link (`/invite/{token}`). | ### Responses #### 204 Declined. #### 403 Error. #### 404 Error. #### 410 Error. #### 429 Too many requests (`rate_limited`). Retry after `Retry-After` seconds.