Whisper Money

Search the documentation

Search pages and sections.

Budgets

A budget is a spending limit per period over some of your categories and labels. Budgets cover your whole account, not a space, and only ever track your own categories and labels. Amounts are in the minor units of your account's currency, given as currency. A budget's period length, start day, rollover and what it tracks are fixed once it is created: to change them, delete the budget and create it again.

The example requests read your personal token from $WHISPER_MONEY_TOKEN: run export WHISPER_MONEY_TOKEN=wm_api_your_token_here before pasting them.

List budgets

GET /api/v1/budgets

Your budgets, newest first, each with the period in progress. Archived budgets are left out; retrieve one by its id to read it.

Scope: read. A personal token needs api:read; an OAuth token needs api:use.

Parameters

Name In Type Required Description
limit query integer No How many objects to return, between 1 and 100. Defaults to 25.
starting_after query string (uuid) No An object id: return the page that follows it. Pass the last id of the page you have.
ending_before query string (uuid) No An object id: return the page that precedes it. Pass the first id of the page you have. Cannot be combined with starting_after.

Response

200

Field Type Description
data array of Budget
data[].id string (uuid) The budget's id.
data[].name string The budget's name.
data[].period_type "monthly" | "weekly" | "biweekly" | "yearly" | "quarterly" How often the budget starts over: monthly, weekly, biweekly, quarterly or yearly. Fixed once created.
data[].period_start_day integer or null The day a period starts: the day of the month (1–31) for a monthly budget, the day of the week (0 is Sunday) for a weekly or biweekly one. Quarterly and yearly budgets follow the calendar. null when not set. Fixed once created.
data[].rollover_type "carry_over" | "reset" What happens to what is left at the end of a period: carry_over adds it to the next period, reset starts every period at the limit. Fixed once created.
data[].expense_type "fixed" | "variable" fixed for spending that repeats at the same amount every period (rent, subscriptions), variable for spending you can adjust (groceries, leisure). It decides how the cashflow analytics split your expenses. A catch-all budget is always variable.
data[].is_catch_all boolean Whether the budget tracks every expense category no other budget tracks. An account has at most one. Fixed once created.
data[].categories array of object The categories the budget tracks; tracking a parent category also tracks its children. Fixed once created.
data[].categories[].id string
data[].categories[].name string
data[].labels array of object The labels the budget tracks. Fixed once created.
data[].labels[].id string
data[].labels[].name string
data[].currency string The currency of the budget's amounts: your account's currency, as an ISO 4217 code.
data[].current_period BudgetPeriod or null The period that covers today, or null when no period does.
data[].current_period.id string (uuid) The period's id.
data[].current_period.start_date string (date) The period's first day.
data[].current_period.end_date string (date) The period's last day, included.
data[].current_period.allocated_amount integer The limit for this period.
data[].current_period.carried_over_amount integer What the previous period left over, carried into this one when the budget's rollover_type is carry_over.
data[].current_period.spent_amount integer What the tracked transactions of the period add up to, as a positive number; a refund lowers it. Provisional while processing_historical is true.
data[].current_period.remaining_amount integer allocated_amount minus spent_amount, the figure the app shows. It leaves out carried_over_amount, and it is negative once the period goes over its limit.
data[].current_period.processing_historical boolean true while the transactions that were already in the period when the budget was created are still being added to it, in the background. Until then spent_amount and remaining_amount are provisional: read the budget again in a few seconds.
data[].archived_at string (date-time) or null When the budget was archived in the app, or null. An archived budget keeps its history but can no longer change; it can still be deleted.
data[].created_at string (date-time) or null When the budget was created.
data[].updated_at string (date-time) or null When the budget was last changed.
has_more boolean Whether more objects exist past this page, in the direction being walked.

Example request

curl https://whisper.money/api/v1/budgets \
  -H "Authorization: Bearer $WHISPER_MONEY_TOKEN"

Example response

{
    "data": [
        {
            "id": "0199a3f8-4c5d-7e6f-8a9b-0c1d2e3f4a5b",
            "name": "Groceries",
            "period_type": "monthly",
            "period_start_day": 1,
            "rollover_type": "reset",
            "expense_type": "variable",
            "is_catch_all": true,
            "categories": [
                {
                    "id": "0199a3f5-2b3c-7d4e-8f5a-6b7c8d9e0f12",
                    "name": "Groceries"
                }
            ],
            "labels": [
                {
                    "id": "0199a3f6-3c4d-7e5f-9a6b-7c8d9e0f1a23",
                    "name": "Essentials"
                }
            ],
            "currency": "EUR",
            "current_period": {
                "id": "0199a3fa-1b2c-7d3e-8f4a-5b6c7d8e9f01",
                "start_date": "2026-10-01",
                "end_date": "2026-10-31",
                "allocated_amount": 50000,
                "carried_over_amount": 0,
                "spent_amount": 32050,
                "remaining_amount": 17950,
                "processing_historical": true
            },
            "archived_at": null,
            "created_at": "2026-01-15T09:30:00+00:00",
            "updated_at": "2026-01-20T18:05:00+00:00"
        }
    ],
    "has_more": true
}

Errors

Status When
400 A malformed request: an unknown pagination cursor (invalid_cursor) or an invalid Idempotency-Key (invalid_idempotency_key).
401 No bearer token, or one that is unknown, expired or revoked.
403 The token lacks the scope (insufficient_scope), the account has no Pro plan (plan_required, with a subscribe_url) or it is a shared demo account (shared_account).
422 The request failed validation.
429 The rate limit is exhausted. Wait the seconds in Retry-After.

Create a budget

POST /api/v1/budgets

The budget needs at least one category or label to track, or is_catch_all: true. It starts with the period in progress and the one before it. The transactions already in them are added in the background, so current_period.spent_amount is provisional while current_period.processing_historical is true: read the budget again a few seconds later.

Scope: write. A personal token needs api:write (a read & write token); an OAuth token needs api:use.

Parameters

Name In Type Required Description
Idempotency-Key header string No Any unique string up to 255 characters. Retrying with the same key within 24 hours returns the first response instead of repeating the request.

Request body

Field Type Required Description
name string Yes The budget's name.
allocated_amount integer Yes The limit for each period, in the minor units of your account's currency.
period_type BudgetPeriodType Yes How often the budget starts over.
rollover_type RolloverType Yes carry_over adds what is left at the end of a period to the next one, reset starts every period at the limit.
period_start_day integer or null No The day a period starts: the day of the month (1–31) for a monthly budget, the day of the week (0 is Sunday, 6 Saturday) for a weekly or biweekly one. Quarterly and yearly budgets follow the calendar and ignore it.
expense_type BudgetExpenseType No fixed for spending that repeats at the same amount every period (rent, subscriptions), variable (the default) for spending you can adjust. A catch-all budget is always variable.
is_catch_all boolean No Track every expense category no other budget tracks. A catch-all budget takes no category_ids or label_ids, and an account has at most one.
category_ids array of string (uuid) No The ids of the categories to track, each one of your own, 100 at most. Tracking a parent also tracks its children. A budget needs at least one category or label, unless it is the catch-all.
label_ids array of string (uuid) No The ids of the labels to track, each one of your own, 100 at most.

Response

201

Field Type Description
data Budget
data.id string (uuid) The budget's id.
data.name string The budget's name.
data.period_type "monthly" | "weekly" | "biweekly" | "yearly" | "quarterly" How often the budget starts over: monthly, weekly, biweekly, quarterly or yearly. Fixed once created.
data.period_start_day integer or null The day a period starts: the day of the month (1–31) for a monthly budget, the day of the week (0 is Sunday) for a weekly or biweekly one. Quarterly and yearly budgets follow the calendar. null when not set. Fixed once created.
data.rollover_type "carry_over" | "reset" What happens to what is left at the end of a period: carry_over adds it to the next period, reset starts every period at the limit. Fixed once created.
data.expense_type "fixed" | "variable" fixed for spending that repeats at the same amount every period (rent, subscriptions), variable for spending you can adjust (groceries, leisure). It decides how the cashflow analytics split your expenses. A catch-all budget is always variable.
data.is_catch_all boolean Whether the budget tracks every expense category no other budget tracks. An account has at most one. Fixed once created.
data.categories array of object The categories the budget tracks; tracking a parent category also tracks its children. Fixed once created.
data.categories[].id string
data.categories[].name string
data.labels array of object The labels the budget tracks. Fixed once created.
data.labels[].id string
data.labels[].name string
data.currency string The currency of the budget's amounts: your account's currency, as an ISO 4217 code.
data.current_period BudgetPeriod or null The period that covers today, or null when no period does.
data.current_period.id string (uuid) The period's id.
data.current_period.start_date string (date) The period's first day.
data.current_period.end_date string (date) The period's last day, included.
data.current_period.allocated_amount integer The limit for this period.
data.current_period.carried_over_amount integer What the previous period left over, carried into this one when the budget's rollover_type is carry_over.
data.current_period.spent_amount integer What the tracked transactions of the period add up to, as a positive number; a refund lowers it. Provisional while processing_historical is true.
data.current_period.remaining_amount integer allocated_amount minus spent_amount, the figure the app shows. It leaves out carried_over_amount, and it is negative once the period goes over its limit.
data.current_period.processing_historical boolean true while the transactions that were already in the period when the budget was created are still being added to it, in the background. Until then spent_amount and remaining_amount are provisional: read the budget again in a few seconds.
data.archived_at string (date-time) or null When the budget was archived in the app, or null. An archived budget keeps its history but can no longer change; it can still be deleted.
data.created_at string (date-time) or null When the budget was created.
data.updated_at string (date-time) or null When the budget was last changed.

Example request

curl -X POST https://whisper.money/api/v1/budgets \
  -H "Authorization: Bearer $WHISPER_MONEY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Groceries",
    "allocated_amount": 50000,
    "period_type": "monthly",
    "rollover_type": "carry_over",
    "period_start_day": 1,
    "is_catch_all": false,
    "category_ids": [
        "0199a3f5-2b3c-7d4e-8f5a-6b7c8d9e0f12"
    ],
    "label_ids": [
        "0199a3f6-3c4d-7e5f-9a6b-7c8d9e0f1a23"
    ]
}'

Example response

{
    "data": {
        "id": "0199a3f8-4c5d-7e6f-8a9b-0c1d2e3f4a5b",
        "name": "Groceries",
        "period_type": "monthly",
        "period_start_day": 1,
        "rollover_type": "reset",
        "expense_type": "variable",
        "is_catch_all": true,
        "categories": [
            {
                "id": "0199a3f5-2b3c-7d4e-8f5a-6b7c8d9e0f12",
                "name": "Groceries"
            }
        ],
        "labels": [
            {
                "id": "0199a3f6-3c4d-7e5f-9a6b-7c8d9e0f1a23",
                "name": "Essentials"
            }
        ],
        "currency": "EUR",
        "current_period": {
            "id": "0199a3fa-1b2c-7d3e-8f4a-5b6c7d8e9f01",
            "start_date": "2026-10-01",
            "end_date": "2026-10-31",
            "allocated_amount": 50000,
            "carried_over_amount": 0,
            "spent_amount": 32050,
            "remaining_amount": 17950,
            "processing_historical": true
        },
        "archived_at": null,
        "created_at": "2026-01-15T09:30:00+00:00",
        "updated_at": "2026-01-20T18:05:00+00:00"
    }
}

Errors

Status When
400 A malformed request: an unknown pagination cursor (invalid_cursor) or an invalid Idempotency-Key (invalid_idempotency_key).
401 No bearer token, or one that is unknown, expired or revoked.
403 The token lacks the scope (insufficient_scope), the account has no Pro plan (plan_required, with a subscribe_url) or it is a shared demo account (shared_account).
409 A request with the same Idempotency-Key is still being processed.
422 The request failed validation.
429 The rate limit is exhausted. Wait the seconds in Retry-After.

Retrieve a budget

GET /api/v1/budgets/{budget}

Archived budgets included.

Scope: read. A personal token needs api:read; an OAuth token needs api:use.

Parameters

Name In Type Required Description
budget path string (uuid) Yes The budget's id.

Response

200

Field Type Description
data Budget
data.id string (uuid) The budget's id.
data.name string The budget's name.
data.period_type "monthly" | "weekly" | "biweekly" | "yearly" | "quarterly" How often the budget starts over: monthly, weekly, biweekly, quarterly or yearly. Fixed once created.
data.period_start_day integer or null The day a period starts: the day of the month (1–31) for a monthly budget, the day of the week (0 is Sunday) for a weekly or biweekly one. Quarterly and yearly budgets follow the calendar. null when not set. Fixed once created.
data.rollover_type "carry_over" | "reset" What happens to what is left at the end of a period: carry_over adds it to the next period, reset starts every period at the limit. Fixed once created.
data.expense_type "fixed" | "variable" fixed for spending that repeats at the same amount every period (rent, subscriptions), variable for spending you can adjust (groceries, leisure). It decides how the cashflow analytics split your expenses. A catch-all budget is always variable.
data.is_catch_all boolean Whether the budget tracks every expense category no other budget tracks. An account has at most one. Fixed once created.
data.categories array of object The categories the budget tracks; tracking a parent category also tracks its children. Fixed once created.
data.categories[].id string
data.categories[].name string
data.labels array of object The labels the budget tracks. Fixed once created.
data.labels[].id string
data.labels[].name string
data.currency string The currency of the budget's amounts: your account's currency, as an ISO 4217 code.
data.current_period BudgetPeriod or null The period that covers today, or null when no period does.
data.current_period.id string (uuid) The period's id.
data.current_period.start_date string (date) The period's first day.
data.current_period.end_date string (date) The period's last day, included.
data.current_period.allocated_amount integer The limit for this period.
data.current_period.carried_over_amount integer What the previous period left over, carried into this one when the budget's rollover_type is carry_over.
data.current_period.spent_amount integer What the tracked transactions of the period add up to, as a positive number; a refund lowers it. Provisional while processing_historical is true.
data.current_period.remaining_amount integer allocated_amount minus spent_amount, the figure the app shows. It leaves out carried_over_amount, and it is negative once the period goes over its limit.
data.current_period.processing_historical boolean true while the transactions that were already in the period when the budget was created are still being added to it, in the background. Until then spent_amount and remaining_amount are provisional: read the budget again in a few seconds.
data.archived_at string (date-time) or null When the budget was archived in the app, or null. An archived budget keeps its history but can no longer change; it can still be deleted.
data.created_at string (date-time) or null When the budget was created.
data.updated_at string (date-time) or null When the budget was last changed.

Example request

curl https://whisper.money/api/v1/budgets/0199a3f2-6c1e-7b4d-9a8e-2f5c1d3b4a60 \
  -H "Authorization: Bearer $WHISPER_MONEY_TOKEN"

Example response

{
    "data": {
        "id": "0199a3f8-4c5d-7e6f-8a9b-0c1d2e3f4a5b",
        "name": "Groceries",
        "period_type": "monthly",
        "period_start_day": 1,
        "rollover_type": "reset",
        "expense_type": "variable",
        "is_catch_all": true,
        "categories": [
            {
                "id": "0199a3f5-2b3c-7d4e-8f5a-6b7c8d9e0f12",
                "name": "Groceries"
            }
        ],
        "labels": [
            {
                "id": "0199a3f6-3c4d-7e5f-9a6b-7c8d9e0f1a23",
                "name": "Essentials"
            }
        ],
        "currency": "EUR",
        "current_period": {
            "id": "0199a3fa-1b2c-7d3e-8f4a-5b6c7d8e9f01",
            "start_date": "2026-10-01",
            "end_date": "2026-10-31",
            "allocated_amount": 50000,
            "carried_over_amount": 0,
            "spent_amount": 32050,
            "remaining_amount": 17950,
            "processing_historical": true
        },
        "archived_at": null,
        "created_at": "2026-01-15T09:30:00+00:00",
        "updated_at": "2026-01-20T18:05:00+00:00"
    }
}

Errors

Status When
401 No bearer token, or one that is unknown, expired or revoked.
403 The token lacks the scope (insufficient_scope), the account has no Pro plan (plan_required, with a subscribe_url) or it is a shared demo account (shared_account).
404 No object with that id exists, or it belongs to someone else.
429 The rate limit is exhausted. Wait the seconds in Retry-After.

Update a budget

PATCH /api/v1/budgets/{budget}

Changes only the fields you send: the name, the limit and the expense type. A new allocated_amount applies to the period in progress and every later one; past periods keep theirs. period_type, period_start_day, rollover_type, is_catch_all, category_ids and label_ids are fixed, since the budget's periods are calculated over its history: sending any of them is a 422 under that field, and the way to change them is to delete the budget and create it again. An archived budget can no longer change: 422 under budget.

Scope: write. A personal token needs api:write (a read & write token); an OAuth token needs api:use.

Parameters

Name In Type Required Description
budget path string (uuid) Yes The budget's id.

Request body

Field Type Required Description
name string No The budget's new name.
allocated_amount integer No The new limit, in the minor units of your account's currency. It applies to the period in progress and every later one; past periods keep theirs.
expense_type BudgetExpenseType No fixed or variable. A catch-all budget stays variable whatever you send.

Response

200

Field Type Description
data Budget
data.id string (uuid) The budget's id.
data.name string The budget's name.
data.period_type "monthly" | "weekly" | "biweekly" | "yearly" | "quarterly" How often the budget starts over: monthly, weekly, biweekly, quarterly or yearly. Fixed once created.
data.period_start_day integer or null The day a period starts: the day of the month (1–31) for a monthly budget, the day of the week (0 is Sunday) for a weekly or biweekly one. Quarterly and yearly budgets follow the calendar. null when not set. Fixed once created.
data.rollover_type "carry_over" | "reset" What happens to what is left at the end of a period: carry_over adds it to the next period, reset starts every period at the limit. Fixed once created.
data.expense_type "fixed" | "variable" fixed for spending that repeats at the same amount every period (rent, subscriptions), variable for spending you can adjust (groceries, leisure). It decides how the cashflow analytics split your expenses. A catch-all budget is always variable.
data.is_catch_all boolean Whether the budget tracks every expense category no other budget tracks. An account has at most one. Fixed once created.
data.categories array of object The categories the budget tracks; tracking a parent category also tracks its children. Fixed once created.
data.categories[].id string
data.categories[].name string
data.labels array of object The labels the budget tracks. Fixed once created.
data.labels[].id string
data.labels[].name string
data.currency string The currency of the budget's amounts: your account's currency, as an ISO 4217 code.
data.current_period BudgetPeriod or null The period that covers today, or null when no period does.
data.current_period.id string (uuid) The period's id.
data.current_period.start_date string (date) The period's first day.
data.current_period.end_date string (date) The period's last day, included.
data.current_period.allocated_amount integer The limit for this period.
data.current_period.carried_over_amount integer What the previous period left over, carried into this one when the budget's rollover_type is carry_over.
data.current_period.spent_amount integer What the tracked transactions of the period add up to, as a positive number; a refund lowers it. Provisional while processing_historical is true.
data.current_period.remaining_amount integer allocated_amount minus spent_amount, the figure the app shows. It leaves out carried_over_amount, and it is negative once the period goes over its limit.
data.current_period.processing_historical boolean true while the transactions that were already in the period when the budget was created are still being added to it, in the background. Until then spent_amount and remaining_amount are provisional: read the budget again in a few seconds.
data.archived_at string (date-time) or null When the budget was archived in the app, or null. An archived budget keeps its history but can no longer change; it can still be deleted.
data.created_at string (date-time) or null When the budget was created.
data.updated_at string (date-time) or null When the budget was last changed.

Example request

curl -X PATCH https://whisper.money/api/v1/budgets/0199a3f2-6c1e-7b4d-9a8e-2f5c1d3b4a60 \
  -H "Authorization: Bearer $WHISPER_MONEY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Food",
    "allocated_amount": 60000
}'

Example response

{
    "data": {
        "id": "0199a3f8-4c5d-7e6f-8a9b-0c1d2e3f4a5b",
        "name": "Groceries",
        "period_type": "monthly",
        "period_start_day": 1,
        "rollover_type": "reset",
        "expense_type": "variable",
        "is_catch_all": true,
        "categories": [
            {
                "id": "0199a3f5-2b3c-7d4e-8f5a-6b7c8d9e0f12",
                "name": "Groceries"
            }
        ],
        "labels": [
            {
                "id": "0199a3f6-3c4d-7e5f-9a6b-7c8d9e0f1a23",
                "name": "Essentials"
            }
        ],
        "currency": "EUR",
        "current_period": {
            "id": "0199a3fa-1b2c-7d3e-8f4a-5b6c7d8e9f01",
            "start_date": "2026-10-01",
            "end_date": "2026-10-31",
            "allocated_amount": 50000,
            "carried_over_amount": 0,
            "spent_amount": 32050,
            "remaining_amount": 17950,
            "processing_historical": true
        },
        "archived_at": null,
        "created_at": "2026-01-15T09:30:00+00:00",
        "updated_at": "2026-01-20T18:05:00+00:00"
    }
}

Errors

Status When
401 No bearer token, or one that is unknown, expired or revoked.
403 The token lacks the scope (insufficient_scope), the account has no Pro plan (plan_required, with a subscribe_url) or it is a shared demo account (shared_account).
404 No object with that id exists, or it belongs to someone else.
422 Validation error
429 The rate limit is exhausted. Wait the seconds in Retry-After.

Delete a budget

DELETE /api/v1/budgets/{budget}

Deletes the budget with its periods and their spending history, archived budgets included. The transactions it tracked are untouched.

Scope: write. A personal token needs api:write (a read & write token); an OAuth token needs api:use.

Parameters

Name In Type Required Description
budget path string (uuid) Yes The budget's id.

Response

200

Field Type Description
id string (uuid) The deleted budget's id.
deleted boolean Always true.

Example request

curl -X DELETE https://whisper.money/api/v1/budgets/0199a3f2-6c1e-7b4d-9a8e-2f5c1d3b4a60 \
  -H "Authorization: Bearer $WHISPER_MONEY_TOKEN"

Example response

{
    "id": "0199a3f2-6c1e-7b4d-9a8e-2f5c1d3b4a60",
    "deleted": true
}

Errors

Status When
401 No bearer token, or one that is unknown, expired or revoked.
403 The token lacks the scope (insufficient_scope), the account has no Pro plan (plan_required, with a subscribe_url) or it is a shared demo account (shared_account).
404 No object with that id exists, or it belongs to someone else.
422 Validation error
429 The rate limit is exhausted. Wait the seconds in Retry-After.