---
title: "Developer API"
description: "Figa external API authentication, permissions, endpoint contract and full parameter reference."
canonical_url: "https://getfiga.com/help/developers/api/"
---

# Developer API

External integrations authenticate via `x-api-key`. API keys are workspace-scoped with granular permission levels per resource. For an agent connection with OAuth, use the [MCP guide](https://getfiga.com/help/developers/mcp). Both work with the same workspace records.

## Authentication

Every request must include the `x-api-key` header with a valid key. Workspace owners and admins generate keys in workspace settings. Keys are scoped to a single workspace. Every request also checks the issuer's current membership, role and active account/workspace status. Revoked, disabled and expired keys cannot authenticate. Store the key securely: it is shown only once.

```
curl -X GET "https://api.figa.cc/api/v1/expenses?year=2026&month=2" \
  -H "x-api-key: et_live_abc123..."
```

**Using Raycast?**

Follow the [Raycast Extension guide](https://getfiga.com/help/developers/integrations/raycast) for API key setup, command capability behavior, manual QA, and Store release readiness.

## Plan limits and downgrade

API keys and MCP workspace bindings share integration slots: Free includes 1 per organization, Pro includes 10 per workspace, and Enterprise has no slot limit. Each key remains scoped to one workspace. An MCP connection uses a slot in each selected workspace. Downgrade retains the oldest eligible integrations; excess connections return plan restrictions. Revocation or expiry releases a slot.

Free exposes the oldest 20 logical expenses in each expense month across the organization. Selection happens before workspace filters, search, sorting and pagination. Skip and pause retain places. Deleting an accessible expense reveals the next retained expense. Upgrade restores the retained pool. Details, payments, history, attachments, statistics and notifications follow the same rule; direct access to a blocked expense returns `403 EXPENSE_PLAN_RESTRICTED`.

Lists and aggregates expose `planLimit` or `planLimits` metadata with `current`, `accessible`, `blocked`, `limit` and the month. The quota `scope` remains `organization`, while `countScope` identifies counts for this `workspace` (or `accessible_workspaces` in session-only cross-workspace statistics). Counts never describe inaccessible workspaces. `canCreate` reports whether the shared monthly quota permits another expense, without exposing its usage. Quota errors omit organization-wide usage counts. Totals describe the accessible subset. Whole-series operations require access to every affected occurrence. Creates and schedule/date changes that exceed the monthly limit return `409 EXPENSE_LIMIT_REACHED`. An old idempotent financial response may return `409 IDEMPOTENCY_RESPONSE_RESTRICTED` after downgrade; the committed operation is not repeated.

Organization request budgets are 100 per minute on Free and 1000 on paid plans, shared across API keys, MCP business tools and workspaces. A single key can use the entire plan budget, including existing keys. The HTTP protection also caps each verified key at 1000 requests per minute. Organization admission returns `X-Organization-RateLimit-Limit`; shared remaining usage is not disclosed to workspace keys. A rejected request returns `429` and `Retry-After`. Rotating keys does not reset the organization budget.

The signed-in account export remains available for the user's complete retained data. API keys cannot access account exports, key management, billing, member administration, internal session routes or the retired Figa AI routes.

## Permission model

Each API key has per-resource permissions. Available actions per resource:

| Resource | Actions |
| --- | --- |
| `expenses` | read, write, delete, payments |
| `categories` | read, write, delete |
| `recipients` | read, write, delete |
| `tags` | read, write, delete |
| `workspaces` | read |

-   Workspace settings expose `read` and `write` key presets.
-   `write` permission implies `read` access.
-   `write` preset grants mutation routes, including `expenses.delete` and `expenses.payments`.
-   Older write keys without an explicit `expenses.payments` entry are still accepted for payment routes for compatibility.
-   Keys created before tag support need replacement to gain tags permissions; existing keys are not silently expanded.
-   Non-allowlisted routes return `403 FORBIDDEN`.

## Categories

Workspace connections can create and edit local categories; shared organization categories are readable and assignable only. Categories have at most two levels and names are unique among siblings. Creation uses `scope: "local"`; updates can change parent, with `parentId: null` making a root. Deletion blocks children and active references, but can permanently clean related archived records. Inspect the affected category before deletion.

GET `/api/v1/categories`

List categories (workspace + global).

POST `/api/v1/categories`

Create category. Requires categories.write.

PUT `/api/v1/categories/:id`

Update category name/description/color.

DELETE `/api/v1/categories/:id`

Delete category. Requires categories.delete.

## Recipients

GET `/api/v1/recipients`

List recipients (workspace + global).

POST `/api/v1/recipients`

Create recipient. Requires recipients.write.

PUT `/api/v1/recipients/:id`

Update recipient name/description.

DELETE `/api/v1/recipients/:id`

Delete recipient. Requires recipients.delete.

## Tags

Assign with `tagIds` or `tagInputs` on expense create/update; never send both. Each replaces the entire set, `[]` clears it, and omission preserves it. Draft inputs can create/reuse tags. For series edits, ALL\_INSTANCES updates defaults and inherited stored history; FUTURE\_INSTANCES starts at the selected occurrence. Explicit overrides and locked tag sets remain.

Names are trimmed and contain 1–100 characters. Tags always belong to the key's workspace.

GET `/api/v1/tags`

List this workspace's tags. Requires tags.read.

POST `/api/v1/tags`

Create a tag with { name }. Requires tags.write; supports Idempotency-Key.

PUT `/api/v1/tags/:tagId`

Rename a tag with { name }. Requires tags.write.

DELETE `/api/v1/tags/:tagId`

Delete a local tag. Requires tags.delete.

## Expenses

GET `/api/v1/expenses?year=YYYY&month=MM`

List expenses for month. year/month query required.

POST `/api/v1/expenses`

Create expense. Add schedule to create recurring. amount is integer cents (14250 = 142.50).

GET `/api/v1/expenses/:expenseId`

Get expense by ID. Add ?occurrence=unixSeconds for recurring occurrence view.

PUT `/api/v1/expenses/:expenseId`

Update expense fields. Use editScope for recurring series.

DELETE `/api/v1/expenses/:expenseId`

Delete expense. Use ?editScope for recurring. Requires expenses.delete.

POST `/api/v1/expenses/:expenseId/skip/toggle`

Toggle skip for recurring occurrence (scheduledDate: unix seconds or YYYY-MM-DD).

GET `/api/v1/expenses/:expenseId/history`

Expense audit history.

GET `/api/v1/expenses/monthly-totals`

Aggregated monthly totals.

POST `/api/v1/expenses/bulk`

Bulk actions (e.g. markAsPaid). Returns HTTP 207.

## Series, bills and service calculations

Lifecycle actions and the dedicated fixed-to-variable conversion use a preview token. Read current revisions before a write; on a stale revision (409), reread and review before retrying. Save amount preserves status. Optional markPaid confirms the amount and sets Paid atomically in that same request.

GET `/api/v1/expenses/series`

List recurring series with cursor, limit, search and status filters. Requires expenses.read.

GET `/api/v1/expenses/:expenseId/series`

Read series state, revision and history. Requires expenses.read.

POST `/api/v1/expenses/:expenseId/series/preview`

Preview a lifecycle action with { revision, action }. Requires expenses.write; does not commit the action.

POST `/api/v1/expenses/:expenseId/series`

Commit with revision, action, previewToken, excludePlanIds and requestId. Requires expenses.write; supports Idempotency-Key.

GET `/api/v1/expenses/:expenseId/bill`

Read bill state and forecast; optional start/end comparison period. Requires expenses.read.

PUT `/api/v1/expenses/:expenseId/bill`

Confirm an amount and period with expectedRevision, expectedSettingsRevision and requestId. Service bills also require expectedActivityRevision. Requires expenses.write; supports Idempotency-Key.

POST `/api/v1/expenses/:expenseId/variable-bills/preview`

Preview switching a fixed monthly series with { from, startAmount }. Requires expenses.read.

POST `/api/v1/expenses/:expenseId/variable-bills`

Enable variable bills using the preview token and reviewed resolutions. Requires expenses.write; supports Idempotency-Key.

PUT `/api/v1/expenses/:expenseId/bill-forecast`

Set series-wide { strategy?, startAmount, expectedRevision }; use rootId as expenseId. Requires expenses.write; supports Idempotency-Key.

GET `/api/v1/expenses/:expenseId/service-calculation?month=YYYY-MM`

Read a service calculation. Requires expenses.read.

POST `/api/v1/expenses/:expenseId/service-calculation/preview`

Preview { unitPrice, weekdays, fromMonth, expectedRevision }. Requires expenses.read.

PUT `/api/v1/expenses/:expenseId/service-calculation`

Save future service rates/weekdays with the settings revision. Requires expenses.write; supports Idempotency-Key.

PUT `/api/v1/expenses/:expenseId/service-calculation/exception`

Set { date, excluded, expectedRevision }. Requires expenses.write; supports Idempotency-Key.

### Variable bill forecasts

Ordinary monthly bills support `last_confirmed` (default), `recent_average`, `seasonal` and `manual`. See [how the methods, comparable periods and fallbacks work](https://getfiga.com/help/bill-forecasts). Service calculations use separate price/schedule settings.

Create with `amountMode: "variable"`, a monthly `schedule` and optional `forecastStrategy`. The required `amount` field is the starting estimate; send `null` for no estimate. Manual forecasts require a positive amount. For example, this body creates a seasonal electricity series (replace the category ID):

```
{
  "name": "Electricity",
  "amount": 12000,
  "currency": "PLN",
  "categoryInput": "CATEGORY_ID",
  "amountMode": "variable",
  "forecastStrategy": "seasonal",
  "schedule": { "frequency": "monthly", "startDate": "2026-11-15", "dayOfMonth": 15 }
}
```

Read an occurrence with `GET /api/v1/expenses/:expenseId/bill`. Its context provides `rootId`, `scheduledDate` and `settingsRevision`; use these returned values for settings edits. The nested `bill` is also available on expense reads:

-   `state`: `confirmed`, `estimated` or `unknown`. Unknown is not zero. `forecastPeriod` is the target billing period.
-   `forecast.amount` and `currency`: the estimate in that currency’s minor units. `forecast` is `null` when no estimate is available.
-   `forecast.source`: the basis actually used, one of `invoice`, `recent_average`, `seasonal`, `start_amount` or `manual`; service bills use `service_schedule`.
-   `forecast.assessment`: selected `strategy`, `reliability` (`low`, `medium`, `high`, `manual`), `reasons`, `sampleCount`, `historyCount`, `seasonalSampleCount` and nullable `annualTrend` (a multiplier). Reliability is a data-quality assessment, not a probability.
-   `assessment.fallbackReason`: `null`, `no_seasonal_history`, `insufficient_recent_history` or `non_monthly_period`. The selected strategy can differ from the applied source.

Read `bill.state` before treating a value as owed. Confirmed amounts stay separate from the current forecast; `firstConfirmationForecast` retains the estimate at first confirmation. Older saved snapshots can omit `assessment`.

To change forecasts across all variable periods, PUT `/api/v1/expenses/:rootId/bill-forecast` with the body below. `startAmount` is required but nullable, except for `manual`. Omit `strategy` to preserve existing methods. Replace revision `3` with the context’s current `settingsRevision`. This PUT supports `Idempotency-Key`.

```
{ "strategy": "seasonal", "startAmount": 12000, "expectedRevision": 3 }
```

For one save with other expense fields, use ordinary expense PUT with this object nested under `forecastSettings` and `editScope: "ALL_INSTANCES"`. The whole update uses that scope; confirmed invoice amounts stay unchanged.

### Change the monthly billing method from an occurrence

PUT `/api/v1/expenses/:expenseId` with `editScope: "FUTURE_INSTANCES"` and `recurringSettings`. Use the selected occurrence ID and its canonical `scheduledDate` as `from` (Unix seconds, not a moved due date), or use `rootId` with that same `from`. Replace the example timestamp and revision with the current values:

```
{
  "editScope": "FUTURE_INSTANCES",
  "recurringSettings": {
    "from": 1794697200,
    "expectedRevision": 3,
    "amountMode": "variable",
    "strategy": "seasonal",
    "startAmount": 12000
  }
}
```

For variable → fixed, use `amountMode: "fixed"` and `amount: 12000` inside `recurringSettings`, omitting `strategy` and `startAmount`. Both directions preserve earlier settings, confirmed invoices and Paid expenses; future planned method changes are replaced from `from`.

Other editable fields can share this atomic save. Do not combine it with top-level `amount`, `schedule`, `expenseDate`, `occurrenceDate` or `forecastSettings`. Only ordinary monthly series support this operation. Schedule edits are unavailable once billing-method history exists. Ordinary expense PUT does not cache `Idempotency-Key`; after an uncertain outcome, reread before retrying.

## Payments

PUT `/api/v1/expenses/:expenseId/payment-status`

Set Paid/Unpaid with pre-write context and revisions. Requires expenses.payments.

GET `/api/v1/expenses/:expenseId/payments`

Read archived payments and current binary totals.

POST `/api/v1/expenses/:expenseId/payments`

Retired: 410 LEGACY\_PAYMENT\_OPERATION\_REMOVED. Use PUT payment-status.

DELETE `/api/v1/expenses/:expenseId/payments`

Retired: 410 LEGACY\_PAYMENT\_OPERATION\_REMOVED. Archive is retained.

DELETE `/api/v1/expenses/:expenseId/payments/:paymentId`

Retired: 410 LEGACY\_PAYMENT\_OPERATION\_REMOVED. Archive is retained.

## Insights

Deterministic analytics endpoints: aggregations, trends and forecasts computed from expense data. All require `expenses.read` permission. See [Insights reference](https://getfiga.com/help/developers/api-insights) for full query parameters, response schemas and curl examples.

POST `/api/v1/insights/range/summary`

Read summary for a date range and filters. Requires expenses.read; no mutation or Idempotency-Key.

POST `/api/v1/insights/range/comparison`

Read comparison for a date range and filters. Requires expenses.read; no mutation or Idempotency-Key.

POST `/api/v1/insights/range/recurring`

Read recurring for a date range and filters. Requires expenses.read; no mutation or Idempotency-Key.

POST `/api/v1/insights/range/expenses`

Read expenses for a date range and filters. Requires expenses.read; no mutation or Idempotency-Key.

GET `/api/v1/insights/summary`

Spending summary for period.

GET `/api/v1/insights/comparison`

Period-over-period comparison.

GET `/api/v1/insights/trends`

Spending trends over time.

GET `/api/v1/insights/anomalies`

Anomaly detection in spending.

GET `/api/v1/insights/forecast`

Spending forecast.

### Range analytics request

POST range reads accept a `range` with inclusive ISO dates and optional `filters`. Comparison also requires a `comparison` date range. Expense detail supports `page`, `pageSize` (10, 25 or 50), `sort` and `direction`.

```
{ "range": { "start": "2026-01-01", "end": "2026-01-31" } }
```

## Context & Notifications

GET `/api/v1/context`

Workspace metadata, plan tier, defaults, capabilities. Requires workspaces.read.

GET `/api/v1/notifications`

List notifications. Requires workspaces.read.

GET `/api/v1/notifications/unread-count`

Unread notification count.

### Context response

`GET /api/v1/context` returns `schemaVersion: 3`. Version 3 adds `capabilities.tags` and removes the old AI quota fields; Figa AI is no longer offered. Clients that pin v2 must update their context parser. `defaults` and `capabilities` describe available workflows.

-   `defaults.baseCurrency` is the workspace currency to prefill forms when a request body omits `currency`.
-   `capabilities` are derived from the configured API key, the current workspace role and the external route permission matrix.
-   Read-only keys report read capabilities but `false` for write, delete, and payment mutations.
-   Write keys report mutation capabilities, including payment routes currently accepted through the write fallback.
-   The response uses Cache-Control: private, no-store. Refresh it after plan or permission changes.

```
{
  "success": true,
  "message": "Context retrieved",
  "data": {
    "schemaVersion": 3,
    "generatedAt": 1770000000,
    "workspace": {
      "id": "workspace-uuid",
      "name": "Acme Finance",
      "baseCurrency": "PLN"
    },
    "defaults": {
      "baseCurrency": "PLN"
    },
    "capabilities": {
      "expenses": {
        "read": true,
        "write": false,
        "delete": false,
        "payments": false
      },
      "categories": {
        "read": true,
        "write": false,
        "delete": false
      },
      "recipients": {
        "read": true,
        "write": false,
        "delete": false
      },
      "tags": { "read": true, "write": false, "delete": false },
      "workspaces": {
        "read": true
      }
    },
    "plan": {
      "tier": "pro",
      "criticalLimits": {
        "apiKeysPerWorkspace": 10,
        "apiKeysPerOrganization": null,
        "maxApiRequestsPerMinute": 1000,
        "maxExpensesPerMonth": null
      }
    }
  }
}
```

## Create expense: request body

`POST /api/v1/expenses` · Permission: `expenses.write`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | Expense name, 1–200 characters. |
| `amount` | integer \| null | Yes | Minor units, ≥ 1; `14250` = 142.50 PLN. For variable bills this is the starting estimate; `null` is allowed except with manual forecasts. |
| `amountMode` | string | No | `fixed` (default) or `variable`. Ordinary variable bills require a monthly schedule. |
| `forecastStrategy` | string | No | Variable bills only: `last_confirmed`, `recent_average`, `seasonal` or `manual`. See [bill forecasts](https://getfiga.com/help/developers/api/#bill-forecasts). |
| `categoryInput` | string \| object \| null | No | Omit or send null for no category. Existing category ID, or a local name/draft with name and parentId/parentName. Drafts create/reuse siblings; prefer IDs for reassignment. |
| `recipientInput` | string \| UUID \| null | No | Existing recipient UUID, new name (auto-created), or `null`. |
| `description` | string \| null | No | Free-text note, max 1000 characters. |
| `expenseDate` | integer | No | Unix timestamp in **seconds**. Defaults to now. |
| `currency` | string | No | ISO 4217 code (`PLN`, `USD`, `EUR`). Defaults to workspace base currency. |
| `schedule` | object \| null | No | Include to make expense recurring. See [Schedule reference](https://getfiga.com/help/developers/api-schedule). |

**categoryInput / recipientInput**

These fields accept **either** an existing UUID **or** a plain-text name. When a name is provided and no matching category/recipient exists, one is created automatically. This simplifies integrations that don't want to pre-create lookup data.

### Minimal one-time expense

```
curl -X POST https://api.figa.cc/api/v1/expenses \
  -H "x-api-key: et_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "AWS June invoice",
    "amount": 14250
  }'
```

This minimal create has no category. Sending `categoryInput: null` has the same result. Invalid or inaccessible category IDs and empty category drafts remain validation errors.

### Full one-time expense

```
curl -X POST https://api.figa.cc/api/v1/expenses \
  -H "x-api-key: et_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Office rent February",
    "amount": 420000,
    "categoryInput": "Housing",
    "recipientInput": "WeWork",
    "description": "Monthly office rent, invoice #2026-02",
    "expenseDate": 1739577600,
    "currency": "PLN"
  }'
```

### Recurring expense (monthly rent)

```
curl -X POST https://api.figa.cc/api/v1/expenses \
  -H "x-api-key: et_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Office rent",
    "amount": 420000,
    "categoryInput": "Housing",
    "recipientInput": "WeWork",
    "schedule": {
      "frequency": "monthly",
      "startDate": "2026-01-01",
      "dayOfMonth": 1
    }
  }'
```

For all schedule variants (weekly, quarterly, yearly, limited installments, etc.) see the [Recurring schedule reference](https://getfiga.com/help/developers/api-schedule).

## Confirm amount and optionally mark Paid

`PUT /api/v1/expenses/:expenseId/bill` requires `expenses.write`. Send the actual `amount` in original-currency minor units, inclusive `period`, a UUID `requestId`, `expectedRevision` and `expectedSettingsRevision`. Services also require `expectedActivityRevision`.

Omit `markPaid` (or send false) to preserve the current payment status. With `markPaid: true`, send `expectedEditContext` and `expectedPaymentStatusRevision` from expense detail. Confirmation and Paid share one transaction; all expectations refer to the state before the request. Failure leaves neither a saved amount nor a new occurrence. This operation creates no payment-history row.

For example, confirming an unknown January bill with `amount: 10000`, its January period, `markPaid: true` and current context/revisions saves a confirmed Paid bill for 100.00. Correcting an already Paid bill to 80.00 preserves its known or unknown Paid date. Optional `paymentDate` uses Unix seconds and applies only when changing Unpaid to Paid; omission uses the server date for that transition.

Retry with the same ID and unchanged payload, including intent and supplied date or its absence. Different input under a retained ID returns 409. Ordinary bills retain only their last request receipt; service receipts retain their existing durable scope. Old ordinary receipts without a verified fingerprint fail closed. Once no receipt remains, stale expectations return 409. A matching replay may return an earlier response without writing: it never restores Paid after a later Unpaid. Always read the current expense after confirmation or replay.

## Update expense: request body

`PUT /api/v1/expenses/:expenseId` · Permission: `expenses.write`

Partial update: include only the fields you want to change.

Expense detail GET returns an opaque `editContext`. Keep it with the draft and send it unchanged as optional `expectedEditContext` on PUT, including range and legacy start-only edits. If persisted expense or series state changed, the API returns 409 `EXPENSE_WRITE_CONFLICT` without saving anything or returning a replacement token. Preserve the draft; do not automatically load a new token and retry the old changes. Discard the draft and read the current version before creating a new intended edit. Legacy requests without the condition remain accepted but have no stale-form protection. Existing `seriesContext`, bill/settings revisions and `previewToken` requirements still apply. For an uncertain range response, resend the exact original body/requestId: a retained receipt replays without another mutation, even with the original edit context.

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | 1–200 characters. |
| `amount` | integer | Cents (minor units), ≥ 1. |
| `categoryInput` | string \| object \| null | Omit to preserve the category; send null to clear it within the selected edit scope. An existing ID or legal local draft assigns a category. |
| `recipientInput` | string \| UUID \| null | UUID, new name, or `null` to clear. |
| `description` | string \| null | Max 1000 characters. `null` to clear. |
| `expenseDate` | integer | Unix timestamp in seconds. |
| `occurrenceDate` | integer | Unix seconds for the selected occurrence only, independently of `editScope`. Do not combine with `expenseDate`. |
| `schedule` | object \| null | Replace schedule. `null` converts recurring to one-time. |
| `forecastSettings` | object | Series-wide variable bill forecasts with `ALL_INSTANCES`. See [fields](https://getfiga.com/help/developers/api/#bill-forecasts). |
| `recurringSettings` | object | Fixed/variable monthly settings with `FUTURE_INSTANCES`. See [fields and scope](https://getfiga.com/help/developers/api/#recurring-billing-settings). |
| `editScope` | string | Recurring only. See below. |

### Add earlier recurring history

Send `range: { start }` to the same PUT, with the `seriesContext` from a fresh expense detail and a new UUID `requestId`. The start accepts a valid YYYY-MM-DD or integer Unix seconds within reporting years 1970–2099 in the series timezone. Monthly lists and totals support reading the added history from 1970. It is normalized to the first term on or after that boundary using the existing recurrence rule. It applies to the entire family, independently of `editScope`. The current release supports daily, weekly, monthly, quarterly and yearly families, including interval and weekday patterns, with date, count or no end. Lifecycle history retains its restrictions on active count. Both backfill and lower-boundary changes support dated method history, with the inheritance and protection rules below. Variable amounts keep their existing monthly-only configuration. The original calendar and date mode must survive normalization; an unprovable backfill returns SERIES\_RULE\_NOT\_PRESERVED without saving.

For example, changing 3 January 2026 to 3 January 2025 adds the 2025 terms. Existing confirmed amounts, payments, bill periods, manual dates, skipped and deleted occurrences stay unchanged. New fixed terms are unpaid; unknown variable terms are incomplete until explicitly confirmed. No payment or bill is recorded automatically.

```
{
  "range": { "start": "2025-01-03" },
  "seriesContext": "series-v1:...",
  "requestId": "5dcbfc0a-c745-4e9d-81e0-3af6dc8c09b4"
}
```

`range` may share one atomic PUT with `occurrenceDate`, metadata and tags, alongside `seriesContext`, `requestId` and an unchanged raw `schedule`. The range applies to the family and occurrenceDate to the selected occurrence; `editScope` applies to the other fields. FUTURE uses the original scheduled date, even if the expense date moves. A retained stored document may be edited before the new start. An explicit edit of an occurrence that disappears or is excluded rejects the whole save; ALL/FUTURE metadata may still use its original boundary without materializing it. `forecastSettings` still requires `editScope: ALL_INSTANCES`. Rhythm, method and whole-period amount changes give 400 `SERIES_COMPOSITION_UNSUPPORTED`. A fixed occurrence amount may be edited with explicit THIS\_INSTANCE, subject to the existing money guards. Combining occurrenceDate with legacy expenseDate is invalid. Failure rolls back every part, including new categories/tags; retries cover the complete PUT. The editable end can be changed as described below. Extending before the anchor also supports dated method history: the prefix inherits the first period’s fixed amount or variable strategy/start amount, while later boundaries stay unchanged. For example, an explicit first period of 180 USD overrides a legacy default of 250 USD. A retained document whose billing settings would change causes 409 `SERIES_HISTORY_CONFLICT`, even when its amount is equal. Actual method-history or `variableFrom` changes advance bill settings revision once; lower-boundary edits alone do not. Refetch before a new confirmation after a revision conflict; completed identical bill retries remain idempotent. Fixed historical amounts enter forecasts only after explicit historical confirmation, including after a verified payment; different original currencies, skips and deleted sources remain excluded. An unsupported calendar gives `SERIES_RANGE_UNSUPPORTED`; services require their own bill operation. Missing request ID is 400 `SERIES_REQUEST_ID_REQUIRED`; missing/stale context is 409 `SERIES_CONTEXT_REQUIRED` or `SERIES_CONTEXT_CONFLICT`. Calendar preview proves the dates, not all write-time admission checks. Existing plan limits and the separate financial projection work limit can still refuse the save with no partial mutation.

A combined range and forecast save first inherits the prefix settings, then applies forecast ALL to every resulting variable period and the root. Fixed amounts stay unchanged. Omit strategy to keep each period’s strategy; a null start amount is invalid while the root or any remaining variable period is manual. Both expected revisions refer to the state before the save. The whole operation advances bill settings revision once, including an explicit forecast no-op, and returns one range receipt. An unchanged range does not advance range revision. Failure rolls back both parts and their required audits; confirmed amounts and first-confirmation forecasts remain unchanged.

Legacy compatibility change: replacing only `schedule.startDate` now changes the series range, preserving recorded occurrence dates and money. Send the complete raw `schedule`, explicit `editScope: ALL_INSTANCES`, and top-level `seriesContext`/`requestId`. Read the expense first; its raw schedule belongs to `scheduleSourceTemplateId`. This adapter accepts only the earliest segment. A child start edit returns `SERIES_RANGE_TARGET_REQUIRED`; use `range` from that same occurrence. Omitted/THIS/FUTURE scope returns `SERIES_RANGE_SCOPE_REQUIRED`. Older clients without context/requestId receive an explicit error; the API never supplies those values automatically.

An unchanged raw schedule is a no-op, including when the visible start is later. Raw January plus a June lower boundary and a legacy March start moves the boundary to March, leaving the January anchor. For a January/March/May rhythm, a February start normalizes to March. Preserve every raw end field: omitting one changes the end, and start+end must use `range`. Actual rhythm or timezone changes use the existing guarded rebase; changing the start representation between ISO and Unix returns `SERIES_DATE_MODE_CHANGE_UNSUPPORTED`. The direct template endpoint refuses start-only writes. Legacy `expenseDate` ALL/FUTURE edits keep their existing scope and money protections.

After reading the expense, send e.g. `{ schedule: { ...expense.schedule, startDate: "2025-01-03" }, editScope: "ALL_INSTANCES", seriesContext: expense.seriesContext, requestId: newUuid }` when its raw start is ISO. Use Unix seconds for a numeric raw start. On a context conflict, read again and submit the reviewed change with a new request ID. On an uncertain response, resend the identical original body and request ID; do not replace its context. Success returns `data.rangeOperation`, including on replay after the selected virtual term disappears. A changed body with the same ID returns `SERIES_REQUEST_ID_CONFLICT`. The optional `Idempotency-Key` must equal the body request ID.

Moving forward stores a lower generation boundary, without rewriting the raw `schedule.startDate`, amounts or method history. Sending the original start in `range` removes that filter; resending an unchanged raw schedule does not. Details expose the whole family’s `effectiveStartDate` and `effectiveStartDateMode` (`iso` or `unix`), independent of the opened segment. Use that mode to display the returned start; the selected segment may use a different date representation. Saved documents before the boundary remain available for permitted confirmation, payment, editing and deletion. Earlier virtual IDs cannot create new documents. If the selected virtual term leaves the range, the first success returns the root as `data.expense` and `selection.kind=series` with its `resultExpenseId`; a replay still succeeds without recreating the term.

For a permanently ended series, details expose `permanentEndDate` (YYYY-MM-DD in the series root timezone) separately from the editable `effectiveEnd`. The App displays that permanent end as read-only and omits `range.end` when changing the start. Start-only edits preserve pauses, lifecycle exclusions and the permanent end whether it is in the past or future. The first nominal term must remain on or before that end; no new term can generate or materialize after it. Every explicit end, even the same date or a shorter end, returns `EXPENSE_LIFECYCLE_CONFLICT`. An explicit count end is refused while an uncanceled pause remains, preserving the ability to resume or cancel that pause. Active count together with a permanent end also refuses the edit; date/none family overlays deactivate raw count limiters. A naturally expired schedule without `endedAfter` is still editable. Services retain `BILL_OPERATION_REQUIRED`.

While a lower boundary or a lower-plan exclusion is active, legacy FUTURE deletion and conversion return 409 `EXPENSE_LIFECYCLE_CONFLICT` before changing any document. Use the series end action where supported; count-based series can change their editable end through the range operation. Changing the end preserves recorded documents and is not bulk deletion. Individual deletion and permitted whole-series deletion retain their own guards. Legacy yearly projections return `BILL_READ_UNSUPPORTED`; use canonical unified statistics.

An optional `Idempotency-Key` header must equal the body UUID; it does not replace it. On an uncertain response, resend exactly the same target and payload with the same ID. The durable receipt is checked before the old context. Reusing the ID for different input returns 409 `SERIES_REQUEST_ID_CONFLICT`. Current workspace permission and plan access are checked on every replay.

For a supported family with multiple versions, `range.end` date/count/none applies to the whole family without deleting versions or saved documents. Omission preserves the current end and its ownership. The first explicit end stores a root-owned `generationEnd`; null keeps each legacy segment’s limits, whereas explicit none removes their effect. Later metadata or rhythm edits retain that family end. Activation that would fill an existing interior calendar gap fails with `SERIES_HISTORY_CONFLICT`. A changed raw end then fails with `SERIES_COMPOSITION_UNSUPPORTED`; use `range.end`. Destructive FUTURE deletion/conversion fails with `EXPENSE_LIFECYCLE_CONFLICT`, including with explicit none. A family count stores nominal positions across all segments from the physical start, before the lower boundary, skips and deleted documents. Its effective count starts at the normalized lower boundary. This remains true if ALL later collapses the versions; no separate count is copied into each child.

For a series with a count end, `effectiveOccurrenceCount` is the raw count minus nominal terms before the lower boundary. Skips and deleted documents still consume nominal positions; this is not `commitment.totalOccurrences`, which counts active charges. For a supported series, send `range: { end: { type: "count", value: 10 } }` to set the effective count. You may combine it with `range.start`; the count then starts at the final normalized first term. The App uses the same operation for its occurrence-count control.

Example: raw start 3 January 2026 and count 12 end on 3 December 2026. Moving the start to 3 June shows 7 terms and retains raw 12. Entering 10 effective terms stores raw 15 and ends on 3 March 2027. Starting again from the original raw 12, moving the start back to January 2025 without an `end` stores raw 24 and retains December 2026: omitted end preserves the last date, not the visible number. Raw `schedule` fields remain raw; an unchanged raw count is a no-op.

Family example: monthly terms fall on the 3rd from January through May, then on the 8th from June. A count of 8 ends on 8 August. Moving the start to 1 June normalizes it to 8 June, shows 3 remaining terms, and keeps the August end. Entering 5 then stores raw 10 and ends on 8 October. Omitted end preserves that last date when backfilling. Active count does not support pause/end lifecycle actions; edit the range end instead.

The same `range` accepts `end: { type: "date", value: "2026-08-15" }`, `end: { type: "count", value: 4 }`, or `end: { type: "none" }`. A date includes that entire calendar day but never creates an off-rhythm term: a series due on the 3rd and ending on 15 August has its last term on 3 August. Date input uses the same reporting bounds as start. A count begins at the final normalized start; none removes both editable limiters. Omitted end preserves the existing end type and last scheduled date. The App exposes these same end controls for supported series, including those with dated method history; its rhythm restrictions remain unchanged.

Details expose backend-resolved `effectiveEnd` as date (the selected raw ISO date or Unix seconds, preserving its calendar mode), count (effective nominal count), or none. Existing raw `endDate` and `endAfter` date forms keep their original representation on reads and start-only changes. Older stored calendars with both limiters use the earlier restriction; reads never rewrite them. An unchanged raw PUT remains a no-op. New or genuinely changed raw schedules require one limiter; explicit `range.end` for a single version without a family overlay stores one canonical end (date as `endDate`, count as `endAfter`, none removes both). A family overlay retains the raw segment fields and replaces their effect instead. A changed raw end combined with range gives 400 `SERIES_COMPOSITION_UNSUPPORTED`. A permanent lifecycle end cannot be changed by this operation and returns 409 `EXPENSE_LIFECYCLE_CONFLICT`.

The stored count must remain within 1–1000 after translation. Effective 10 plus 995 earlier nominal terms would require raw 1005, so preview and commit refuse with 400 `SERIES_RANGE_COUNT_LIMIT` and explain the calculation. A start after the last term gives 400 `SERIES_RANGE_EMPTY`. Neither refusal saves anything. The 2100 calendar-proof boundary does not truncate nominal counts whose last term is later; explicit input dates still obey the reporting-date limits. Existing raw anchors before 1970 also count in the nominal prefix; `effectiveStartDate` may report such an inherited date without allowing a new start outside the input limits.

The first success returns `data.expense` and `data.rangeOperation`. The latter contains `requestId`, `rootId`, `committedContext`, `outcome` (`applied` or `no_change`), and `selection` with `requestedExpenseId`, `resultExpenseId` and `kind` (`occurrence` or `series`). A replay may return only `rangeOperation`. Refetch `resultExpenseId` for current data; the receipt is evidence of the completed operation, not a fresh expense snapshot.

Receipts live with the recurring family, independently of audit retention and transport caches. Soft deletion retains them; permanent family/workspace/personal-account deletion removes them. MCP `figa_update_expense` accepts the same range contract and copies its outer `requestId` into the shared operation. If `changes.requestId` is supplied, it must match. Durable receipt replay remains available after the 24-hour MCP response cache expires, subject to current access and plan limits. The deprecated template, instance, materialization and bulk APIs do not accept these fields.

The App sidebar obtains its summary from the read-only session endpoint `POST /api/v1/workspaces/:workspaceId/expenses-unified/:expenseId/range-preview`, with `{ range, seriesContext }` and optional `forecastSettings` plus `editScope: ALL_INSTANCES`. Combined previews validate the same forecast rules and show the resulting variable-prefix settings. Its `data.rangePreview` contains the normalized `effectiveStartDate` and its mode, `extendsHistory` (whether the operation adds a prefix), `effectiveEnd`, `effectiveOccurrenceCount`, `inheritedSettings`, `excludedPlanCandidates` with each document’s UUID, scheduled date and its `scheduledDateMode`, expense date, protection and selected state, and `complete: true` only after a complete proof. It creates no documents or receipt and does not reserve the write; PUT revalidates. This is separate from a lifecycle `previewToken`.

`range.excludePlanIds` optionally selects recorded plans before the resulting first term. Omit it to preserve existing lower-boundary markings, send a UUID array to replace only those markings, or send `[]` to clear them. It may be the only field in `range`. Nothing is selected automatically. Confirmed invoices and Paid expenses cannot be excluded; archived payments alone do not protect an Unpaid plan: commit returns 409 `SERIES_PLAN_PROTECTED` without a partial save. This includes reactivating an old marking after the restored expense was paid or confirmed; remove that marking explicitly. The preview shows such a retained selection as protected so it can be removed.

For example, moving the first term from January to June preserves a recorded January plan unless its UUID is explicitly selected. Moving the first term back to January restores that same document, with its attachments and manual expense date. Skips and deletion remain in force. Excluded documents cannot be edited, paid or materialized through old IDs. These markings do not change pause/end history.

The exclusion preview reads at most 500 recorded documents in the whole family, not 500 selections. A 501st document returns 409 `VIRTUAL_INSTANCE_GENERATION_LIMIT_EXCEEDED`; no partial list is presented as complete. PUT repeats completeness and protection checks under locks. Splitting the selection into smaller requests does not bypass the document limit.

### editScope: recurring expense edits

For example, editing November’s occurrence with `occurrenceDate` set to 8 December moves only that document. Its canonical `scheduledDate`, invoice period, payments and financial snapshots remain unchanged; December’s regular occurrence stays separate. Other fields in the same request retain their selected scope. Dates must fit both the generator domain and reporting years 1970–2099 in the stored series timezone. Conflicting date fields or a financial guard failure roll back the entire update. Legacy `expenseDate` retains its THIS/FUTURE/ALL behavior. The new field is available through this API and MCP `figa_update_expense` in `changes.occurrenceDate`.

`currency` and `isPaid` are not expense PUT fields. Keep the original currency and use payment operations to record money paid.

When updating or deleting a recurring expense, `editScope` controls which occurrences are affected:

| Value | Effect |
| --- | --- |
| `THIS_INSTANCE` | Only the specific occurrence. Creates a materialized override. |
| `FUTURE_INSTANCES` | This occurrence and all future occurrences. |
| `ALL_INSTANCES` | Entire series (past + future). Updates the template directly. |

### Update single occurrence of a recurring expense

```
curl -X PUT https://api.figa.cc/api/v1/expenses/{expenseId} \
  -H "x-api-key: et_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 450000,
    "editScope": "THIS_INSTANCE"
  }'
```

### Update all future occurrences

```
curl -X PUT https://api.figa.cc/api/v1/expenses/{expenseId} \
  -H "x-api-key: et_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 450000,
    "editScope": "FUTURE_INSTANCES"
  }'
```

## List expenses: query parameters

`GET /api/v1/expenses` · Permission: `expenses.read`

| Param | Type | Required | Description |
| --- | --- | --- | --- |
| `year` | integer | Yes | Year (2000–2100). |
| `month` | integer | Yes | Month (1–12). |
| `includeTemplates` | boolean | No | Include recurring templates. Default: `true`. |
| `showPaidOnly` | boolean | No | Only paid expenses. Default: `false`. |
| `showUnpaidOnly` | boolean | No | Only unpaid expenses. Default: `false`. |
| `categoryId` | UUID | No | Filter by category. |
| `recipientId` | UUID | No | Filter by recipient. |
| `limit` | integer | No | Max results (1–50). |

### List unpaid expenses for February 2026

```
curl -X GET "https://api.figa.cc/api/v1/expenses?year=2026&month=2&showUnpaidOnly=true" \
  -H "x-api-key: et_live_abc123..."
```

## Delete expense

`DELETE /api/v1/expenses/:expenseId` · Permission: `expenses.delete`

For recurring expenses, pass `editScope` as a query parameter to control what gets deleted:

```
# Delete single occurrence
curl -X DELETE "https://api.figa.cc/api/v1/expenses/{expenseId}?editScope=THIS_INSTANCE" \
  -H "x-api-key: et_live_abc123..."

# Delete entire recurring series
curl -X DELETE "https://api.figa.cc/api/v1/expenses/{expenseId}?editScope=ALL_INSTANCES" \
  -H "x-api-key: et_live_abc123..."
```

## Set Paid or Unpaid

`PUT /api/v1/expenses/:expenseId/payment-status` · Permission: `expenses.payments`

Read the expense detail first. Send the desired `isPaid`, its `editContext` as `expectedEditContext`, and `paymentStatusRevision` as `expectedPaymentStatusRevision`. A confirmed bill also requires `expectedBillRevision`. Copy the returned context unchanged; never construct it or silently bind an old command to a fresh read.

```
{
  "isPaid": true,
  "expectedEditContext": "COPY_FROM_EXPENSE_DETAIL",
  "expectedPaymentStatusRevision": 3,
  "expectedBillRevision": 2,
  "paymentDate": 1739577600
}
```

The example revisions are placeholders. Omit `expectedBillRevision` for an ordinary non-bill expense. Optional `paymentDate` uses Unix seconds and defaults to now only on a real Unpaid → Paid transition. Repeating Paid with fresh expectations preserves its known or unknown date; Unpaid clears the current date. Only a real transition advances the status revision.

No `amount`, `paymentAmount`, method or notes are accepted, and no payment-history row is created or removed. Unknown, estimated or skipped bills cannot become Paid before legal confirmation/restoration. Use [atomic confirmation with markPaid](https://getfiga.com/help/developers/api/#confirm-bill) to save an actual amount and set Paid together. A later correction from 100 to 80 or 120 preserves status and date; it does not edit the archive.

Keep a UUID `Idempotency-Key` and the whole payload unchanged for an uncertain retry within the existing 24-hour cache. A conflict requires a fresh read and a reviewed new intent. Replay may return an earlier receipt; GET the expense for current status before reporting success. After a later Unpaid, replay cannot restore Paid, including after cache expiry.

Old POST/DELETE payment mutations and legacy amount writers return `410 LEGACY_PAYMENT_OPERATION_REMOVED`, with the replacement operation. A stale App card must refresh; it cannot report an old payment as success.

## Read archived payments and current totals

`GET /api/v1/expenses/:expenseId/payments` · Permission: `expenses.read`

The archive remains readable and exportable. Each row keeps its currency-labelled amount, base snapshot and `currencySemantics`. Current `totalPaid`/`remainingAmount` use the expense’s original currency and its binary status: the whole known amount belongs to one side. The base pair uses the current stored base valuation. Unknown valuations stay null. Archived rows do not create a current partial balance.

For example, an Unpaid 100 USD expense valued at 410 PLN with a historical 25 USD payment returns:

```
{
  "payments": [{
    "amount": 2500,
    "currency": "USD",
    "baseAmountMinor": 10250,
    "baseCurrency": "PLN",
    "currencySemantics": "explicit"
  }],
  "metadata": {
    "totalPaid": 0,
    "remainingAmount": 10000,
    "currency": "USD",
    "baseTotalPaid": 0,
    "baseRemainingAmount": 41000,
    "baseCurrency": "PLN",
    "archivedTotalPaid": 2500,
    "archivedCurrency": "USD",
    "currencySemantics": "explicit",
    "paymentCount": 1
  }
}
```

Missing historical base values do not invalidate a complete current expense valuation. `currencySemantics` describes the archive; legacy ambiguous rows retain their labelled raw value. When archived row currencies differ, `archivedTotalPaid` and `archivedCurrency` are null. Never add different currencies or fill missing historical FX with today’s rate.

## Monthly totals: query parameters

`GET /api/v1/expenses/monthly-totals` · Permission: `expenses.read`

| Param | Type | Required | Description |
| --- | --- | --- | --- |
| `startYear` | integer | Yes | Starting year (2000–2100). |
| `startMonth` | integer | Yes | Starting month (1–12). |
| `months` | integer | No | Number of months to return (1–12). Default: `7`. |

```
curl -X GET "https://api.figa.cc/api/v1/expenses/monthly-totals?startYear=2026&startMonth=1&months=6" \
  -H "x-api-key: et_live_abc123..."
```

## Bulk actions

`POST /api/v1/expenses/bulk` · Permission: `expenses.write` · HTTP 207 with per-item results.

Supported actions are `markAsPaid`, `markAsUnpaid` and `delete`, with 1–100 IDs. Status actions require `expectations[id]` copied from each reviewed expense detail, including the confirmed bill revision where applicable. Missing/stale expectations fail that item; successful items remain committed. A fresh no-op is skipped. Unpaid preserves archived payments.

```
{
  "action": "markAsPaid",
  "ids": ["EXPENSE_ID"],
  "expectations": {
    "EXPENSE_ID": {
      "expectedEditContext": "COPY_FROM_EXPENSE_DETAIL",
      "expectedPaymentStatusRevision": 0
    }
  },
  "payload": { "paymentDateMode": "expenseDate" }
}
```

`paymentDateMode: "expenseDate"` selects each current expense date under locks. Alternatively send one `paymentDate`, or omit both to use now. Combining both is invalid. `paymentMethod` belongs to the retired payment model and is refused. This API contract does not enable a disabled bulk feature in the App.

## Skip / restore occurrence

`POST /api/v1/expenses/:expenseId/skip/toggle` · Permission: `expenses.write`

Toggle the skip status of a single recurring occurrence. Only unpaid instances can be skipped. Skipped instances remain visible but are excluded from totals.

| Field | Type | Description |
| --- | --- | --- |
| `scheduledDate` | integer \| string | Occurrence identifier: Unix timestamp (seconds) or ISO date `YYYY-MM-DD`. |

### Skip using Unix timestamp

```
curl -X POST https://api.figa.cc/api/v1/expenses/{expenseId}/skip/toggle \
  -H "x-api-key: et_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{"scheduledDate": 1740787200}'
```

### Skip using ISO date (recommended for automations)

```
curl -X POST https://api.figa.cc/api/v1/expenses/{expenseId}/skip/toggle \
  -H "x-api-key: et_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{"scheduledDate": "2026-03-30"}'
```

If no matching occurrence exists, API returns `404` (for both timestamp and ISO date).

## Idempotency

POST mutations support the `Idempotency-Key` header (UUID format). If the same key is sent again within 24 hours, the server returns the cached response instead of processing the request again, including a completed 5xx response. Bill confirmation/forecast and service-calculation PUTs also support this header. Other existing PUT/DELETE routes do not cache a supplied idempotency key.

```
curl -X POST https://api.figa.cc/api/v1/expenses \
  -H "x-api-key: et_live_abc123..." \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -d '{"name":"AWS","amount":14250,"categoryInput":"Cloud"}'
```

**Idempotency is optional**

Without the header, requests are processed normally. The header is recommended for automation integrations where retries are common. Use a fresh UUID per new operation and retain it for retries. Reusing it for a different body or endpoint returns `IDEMPOTENCY_KEY_MISMATCH` (409). Pending duplicates return `IDEMPOTENT_REQUEST_IN_PROGRESS` (409). An interrupted request may have committed: inspect the resource after an uncertain result, including `IDEMPOTENCY_OUTCOME_UNKNOWN`, before sending another mutation with a new key. Replay protection has a 24-hour lifetime; it is not an unlimited exactly-once guarantee.

## Unavailable to workspace API keys

Account/security settings, whole-account exports, organization/workspace management, membership, invitations, billing, key management, admin/console/Pulse, UI locks and global category/recipient mutations are not authorized by a workspace API key. Figa AI is not an API capability. Attachment transfer and CSV import are not exposed through this API; structured expense creation and bulk actions are available. Notification reads are workspace-scoped; personal notification settings and read/dismiss actions remain app-only.

## Error handling

| Status | Meaning | Action |
| --- | --- | --- |
| `400` | Validation error | Fix request payload or query params. Response body contains field-level errors. |
| `401` | Unauthorized | Check API key is valid and not expired. |
| `403` | Forbidden | Key lacks required permission or route not in allowlist. |
| `404` | Not found | Resource ID does not exist or is not accessible. |
| `409` | Conflict | Duplicate name, plan limit reached, or concurrent idempotent request. |
| `429` | Rate limited | Wait for the Retry-After header before retrying. |
| `5xx` | Server error | A replay returns the same result. Read the resource to resolve an uncertain outcome; do not blindly submit a new mutation. |

**Troubleshooting 403 / Cloudflare blocks**

Distinguish a JSON permission/plan error from an edge challenge page. Keep the timestamp and request/Ray ID for support when Cloudflare blocks a call. A browser challenge needs an edge configuration fix; changing API keys or blindly retrying will not resolve it.

**About timestamps**

All date fields (`expenseDate`, `paymentDate`, `scheduledDate`, `startDate` as integer) are Unix timestamps in **seconds** (not milliseconds). String dates accept ISO 8601 format (`YYYY-MM-DD`).

The App’s CSV importer accepts a missing `category` column or an empty cell as no category, whether category auto-creation is on or off. A nonempty unknown name is created only when auto-creation is enabled; otherwise that row fails. CSV still supports one-time expenses and fixed recurring templates, not variable bills or services. Account exports preserve `categoryId: null` in JSON and an empty categoryId cell in CSV. The account archive is a historical export, not the CSV importer’s input format.
