Budgets API
Every Budgets action (6) and data model (2): endpoints, scopes, input, preconditions, events and examples.
A spending plan per expense category, with alerts when you go over.
Module code budgets · 6 actions · 2 data models · 5 events. A key or token never does more here than the person behind it can; row-level security applies exactly as in the app. Scopes: budgets (everything), budgets:read (read only) or budgets:<capability>.
Resources
| Resource | Table | List and read | POST | PATCH | DELETE |
|---|---|---|---|---|---|
budget_lines | budget_line | /api/v1/budget_lines | |||
budgets | budget | /api/v1/budgets | budgets.create | budgets.update |
Lists take limit (up to 200), cursor, order (e.g. -created_at), fields and q; every other query parameter is a column filter: ?status=paid, ?total[gte]=100, ?id[in]=a,b, ?x=null.
Actions
| Action | Needs | What it does |
|---|---|---|
budgets.check_alerts | budgets:write | Check budget alerts |
budgets.close | budgets:manage | Close a budget |
budgets.create | budgets:write | Plan a budget |
budgets.remove_line | budgets:write | Remove a category |
budgets.set_line | budgets:write | Plan a category |
budgets.update | budgets:write | Edit a budget |
budgets.check_alerts
Check budget alerts — Compares actual spending with the plan and raises each threshold once.
Needs budgets:write · changes data
https://app.getera.io/api/v1/budget_lines/actions/check_alerts- Canonical address:
POST /api/v1/actions/budgets.check_alerts
Input
| Field | Type | Required | Notes |
|---|---|---|---|
expenseId | uuid | Expense · id of expense | |
budgetId | uuid | Budget · id of budget |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
expense_not_found | A named expense must exist. |
Events it publishes: budgets.threshold_reached, budgets.exceeded
What it writes: budget_line (update) — Stamps the alert level
curl -X POST https://app.getera.io/api/v1/budget_lines/actions/check_alerts \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"expenseId":"EXPENSE_ID"}'{
"ok": true,
"result": "…",
"events": [
"budgets.threshold_reached",
"budgets.exceeded"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.budgetLines.checkAlerts({
expenseId: "EXPENSE_ID"
})era budget_lines check_alerts \
--expense-id "EXPENSE_ID"{
"name": "era__budgets__check_alerts",
"arguments": {
"expenseId": "EXPENSE_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold budgets:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 expense_not_found · 429 slow down
budgets.close
Close a budget — Freezes the plan; it stops raising alerts.
Needs budgets:manage · reaches outside (sends, publishes or charges) · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/budgets/{id}/actions/close- Canonical address:
POST /api/v1/actions/budgets.close
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Budget · id of budget |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
budget_not_found | The budget must exist. |
budget_closed | A budget is closed once. |
Events it publishes: budgets.closed
What it writes: budget (update) — Closes it
curl -X POST https://app.getera.io/api/v1/budgets/BUDGET_ID/actions/close \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"budgets.closed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.budgets.close("BUDGET_ID")era budgets close BUDGET_ID{
"name": "era__budgets__close",
"arguments": {
"id": "BUDGET_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold budgets:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 budget_not_found, budget_closed · 429 slow down
budgets.create
Plan a budget — A period, a currency and a planned amount per expense category.
Needs budgets:write · changes data
https://app.getera.io/api/v1/budgets/actions/create- REST alias:
POST /api/v1/budgets - Canonical address:
POST /api/v1/actions/budgets.create
Input
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Name · max 255 |
periodStart | date | yes | From |
periodEnd | date | yes | To |
currency | string | Currency · max 3 | |
alertThresholdPercent | number | Warn at (%) · min 1 · max 100 | |
notes | string | Notes · max 4000 | |
lines | object / array | Lines |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
budget_period_invalid | A period cannot end before it starts. |
invalid_currency | The currency must be an ISO-4217 code. |
invalid_budget_lines | Every planned amount is zero or more. |
expense_category_not_available | Each line names an active expense category. |
Events it publishes: budgets.created
What it writes: budget (insert) — Plans the budget; budget_line (insert) — Writes its lines
curl -X POST https://app.getera.io/api/v1/budgets/actions/create \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Name","periodStart":"2026-11-02","periodEnd":"2026-11-02"}'{
"ok": true,
"result": "…",
"events": [
"budgets.created"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.budgets.create({
name: "Name",
periodStart: "2026-11-02",
periodEnd: "2026-11-02"
})era budgets create \
--name "Name" \
--period-start "2026-11-02" \
--period-end "2026-11-02"{
"name": "era__budgets__create",
"arguments": {
"name": "Name",
"periodStart": "2026-11-02",
"periodEnd": "2026-11-02"
}
}Errors: 401 no or expired credential · 403 the credential does not hold budgets:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 budget_period_invalid, invalid_currency, invalid_budget_lines, expense_category_not_available · 429 slow down
budgets.remove_line
Remove a category — Takes a category out of the plan.
Needs budgets:write · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/budget_lines/{id}/actions/remove_line- Canonical address:
POST /api/v1/actions/budgets.remove_line
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Line · id of budget_line |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
budget_line_not_found | The line must exist. |
budget_closed | A closed budget is not edited. |
Events it publishes: budgets.updated
What it writes: budget_line (delete) — Removes the line
curl -X POST https://app.getera.io/api/v1/budget_lines/BUDGET_LINE_ID/actions/remove_line \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"budgets.updated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.budgetLines.removeLine("BUDGET_LINE_ID")era budget_lines remove_line BUDGET_LINE_ID{
"name": "era__budgets__remove_line",
"arguments": {
"id": "BUDGET_LINE_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold budgets:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 budget_line_not_found, budget_closed · 429 slow down
budgets.set_line
Plan a category — Sets the planned amount for one expense category.
Needs budgets:write · changes data
https://app.getera.io/api/v1/budget_lines/actions/set_line- Canonical address:
POST /api/v1/actions/budgets.set_line
Input
| Field | Type | Required | Notes |
|---|---|---|---|
budgetId | uuid | yes | Budget · id of budget |
categoryId | uuid | yes | Category · id of expense_category |
plannedAmount | number (money) | yes | Planned · min 0 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
budget_not_found | The budget must exist. |
budget_closed | A closed budget is not edited. |
invalid_budget_lines | A planned amount is zero or more. |
expense_category_not_available | The category must be active. |
Events it publishes: budgets.updated
What it writes: budget_line (upsert) — Writes the line
curl -X POST https://app.getera.io/api/v1/budget_lines/actions/set_line \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"budgetId":"BUDGET_ID","categoryId":"EXPENSE_CATEGORY_ID","plannedAmount":1}'{
"ok": true,
"result": "…",
"events": [
"budgets.updated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.budgetLines.setLine({
budgetId: "BUDGET_ID",
categoryId: "EXPENSE_CATEGORY_ID",
plannedAmount: 1
})era budget_lines set_line \
--budget-id "BUDGET_ID" \
--category-id "EXPENSE_CATEGORY_ID" \
--planned-amount 1{
"name": "era__budgets__set_line",
"arguments": {
"budgetId": "BUDGET_ID",
"categoryId": "EXPENSE_CATEGORY_ID",
"plannedAmount": 1
}
}Errors: 401 no or expired credential · 403 the credential does not hold budgets:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 budget_not_found, budget_closed, invalid_budget_lines, expense_category_not_available · 429 slow down
budgets.update
Edit a budget — Renames it, moves its warning threshold, or switches it between draft and active.
Needs budgets:write · changes data
https://app.getera.io/api/v1/budgets/{id}/actions/update- REST alias:
PATCH /api/v1/budgets/{id} - Canonical address:
POST /api/v1/actions/budgets.update
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Budget · id of budget |
name | string | Name · max 255 | |
alertThresholdPercent | number | Warn at (%) · min 1 · max 100 | |
notes | string | Notes · max 4000 | |
status | string (enum) | Status · Allowed values: draft, active |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
budget_not_found | The budget must exist. |
budget_closed | A closed budget is not edited. |
Events it publishes: budgets.updated
What it writes: budget (update) — Updates the budget; budget_line (update) — Clears alerts when the threshold moves
curl -X POST https://app.getera.io/api/v1/budgets/BUDGET_ID/actions/update \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"name":"Name"}'{
"ok": true,
"result": "…",
"events": [
"budgets.updated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.budgets.update("BUDGET_ID", {
name: "Name"
})era budgets update BUDGET_ID \
--name "Name"{
"name": "era__budgets__update",
"arguments": {
"id": "BUDGET_ID",
"name": "Name"
}
}Errors: 401 no or expired credential · 403 the credential does not hold budgets:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 budget_not_found, budget_closed · 429 slow down
Data models
BudgetLine
Table budget_line · Resource budget_lines
GET /api/v1/budget_linesGET /api/v1/budget_lines/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
budget_id | string (uuid) · uuid | budget | ||
expense_category_id | string (uuid) · uuid | expense_category | ||
planned_amount | string (int64) · int8 | |||
alert_level | string · text | yes | ||
alerted_at | string (date-time) · timestamptz | yes | ||
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz |
Primary key: id
Unique: budget_id, expense_category_id
Checks:
CHECK (((alert_level IS NULL) OR (alert_level = ANY (ARRAY['warning'::text, 'exceeded'::text]))))CHECK ((planned_amount >= 0))
Budget
Table budget · Resource budgets
GET /api/v1/budgetsGET /api/v1/budgets/{id}POST /api/v1/budgets→budgets.createPATCH /api/v1/budgets/{id}→budgets.update
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
name | string · text | |||
period_start | string (date) · date | |||
period_end | string (date) · date | |||
currency | string · bpchar | max 3 | ||
status | string · text | draft, active, closed | ||
alert_threshold_percent | string (decimal) · numeric | |||
notes | string · text | yes | ||
created_by_user_id | string (uuid) · uuid | yes | app_user | |
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz |
Primary key: id
Checks:
CHECK (((alert_threshold_percent > (0)::numeric) AND (alert_threshold_percent <= (100)::numeric)))CHECK ((currency ~ '^[A-Z]{3}$'::text))CHECK ((length(TRIM(BOTH FROM name)) > 0))CHECK ((period_end >= period_start))
Events
Workflows and webhooks react to these events.
| Event | What happens | Payload |
|---|---|---|
budgets.closed | A budget was closed. | id |
budgets.created | A budget was planned. | id |
budgets.exceeded | A category went over budget. | id, lineId, category, plannedAmount, actualAmount, currency |
budgets.threshold_reached | A category crossed its warning threshold. | id, lineId, category, plannedAmount, actualAmount, currency |
budgets.updated | A budget changed. | id |