Developers

API v1 reference

REST · JSON · OAuth 2.0 Bearer https://api.ledgy.app Stable — no breaking changes within v1

Overview

The Ledgy API is a JSON REST API for programmatically managing the same ledger you use in the iOS app — accounts, categories, budgets, shared groups, and transactions. Use it to build importers, exporters, sync bridges, dashboards, or to power your own automations.

  • Base URL: https://api.ledgy.app
  • Version prefix: all endpoints in this document are mounted under /api/v1/.
  • Transport: HTTPS only. HTTP requests are not accepted.
  • Encoding: JSON request and response bodies. UTF-8. camelCase property names.
  • Authentication: Authorization: Bearer … on every request.
  • Cloud subscription: required on every v1 endpoint. See Subscription gate.
Looking for an AI assistant? If you want Claude, ChatGPT, or Cursor to talk to Ledgy on your behalf, use the Model Context Protocol endpoint at https://api.ledgy.app/mcp — see Integrations. The REST API documented here is for code you write yourself.

Quickstart

Three steps to your first authenticated request.

1. Mint a Personal Access Token

  1. Sign in to your Ledgy dashboard and open Settings → API Access.
  2. Click + New token.
  3. Give the token a descriptive name (e.g. "Zapier — weekly export"), pick the scopes you need (see Scopes), and optionally set an expiry.
  4. Copy the token. It's shown once. Tokens start with the prefix lgpat_ followed by 64 hex characters.

The same page lists your active tokens with their last-used timestamp and permission count, surfaces an "Expiring in 30 days" warning, and lets you revoke any token instantly via the trash icon. Each account can hold up to 25 active tokens at a time.

2. Make a request

cURLList accounts
# Replace lgpat_… with your token
curl "https://api.ledgy.app/api/v1/accounts" \
  -H "Authorization: Bearer lgpat_a1b2c3d4e5…"

3. Inspect the response

200 OKapplication/json
{
  "items": [
    {
      "id": "acc_01HK8V…",
      "name": "Wise EUR",
      "currencyCode": "EUR",
      "initialBalance": 1240.50,
      "icon": "wallet",
      "color": "#4A90E2",
      "order": 0,
      "createdAt": "2026-04-12T08:13:09Z",
      "updatedAt": "2026-05-01T17:02:31Z"
    }
  ],
  "totalCount": 1
}
Treat your token like a password. Anyone with the token can read or modify your ledger within the scopes you granted. Revoke compromised tokens immediately from Settings → API Access.

Authentication

Every request to /api/v1/ must carry an Authorization header. Two token types are accepted:

  • Personal Access Token (PAT) — long-lived bearer token you create from the dashboard's Settings → API Access page. Format: lgpat_ + 64 hex characters. Scoped, revocable, optionally expiring. Recommended for all third-party integrations.
  • Session JWT — short-lived token issued to the first-party apps (iOS, dashboard). Has no scope restrictions. You can use it for one-off testing if you can extract one from a signed-in session, but PATs are the supported path.

Header format

HTTP
Authorization: Bearer lgpat_a1b2c3d4e5f6…

Token limits

  • Up to 25 active PATs per Ledgy account.
  • An optional expiration can be set at creation time. Expired tokens return 401 Unauthorized.
  • Revoked tokens stop working immediately — Ledgy stores only a SHA-256 hash of the token, never the value, so a leaked token cannot be recovered, only revoked and replaced.

Common auth failures

401
Missing, malformed, expired, or revoked token.
403
Token is valid but the requested endpoint requires a scope your PAT does not have, or your Cloud subscription is not active.

Scopes

PATs follow a deny-by-default model. A token can only call endpoints whose required scope it holds; everything else returns 403 Forbidden. Grant the narrowest set of scopes your integration actually needs.

Available scopes:

accounts:read accounts:write categories:read categories:write budgets:read budgets:write groups:read groups:write transactions:read transactions:write tags:read tags:write recurring:read recurring:write settings:read settings:write

Each endpoint listed below shows its required scope as a small purple chip. :write scopes do not imply :read — request both if you need both.

Subscription gate

All v1 endpoints — including read-only ones — require the calling user to have an active Ledgy Cloud subscription. If the subscription has lapsed, expired, or never been started, the API responds with:

403 Forbidden
HTTP/1.1 403 Forbidden
X-Subscription-Required: true
Content-Type: application/json

{ "error": "Active cloud subscription required" }

The X-Subscription-Required header lets clients distinguish a subscription block from a generic permission denial. Restore access by reactivating Cloud in the iOS app or at ledgy.app/pricing.

Errors

Errors use standard HTTP status codes. The response body is a single-field JSON object:

JSON
{ "error": "Human-readable message" }

Status codes you should handle:

200
OK — resource returned, or list returned.
201
Created — new resource created. Location header points to the canonical URL.
204
No Content — successful delete; no body.
400
Validation failed — required field missing, value out of range, malformed JSON.
401
Authentication failed — see Authentication.
403
Permission denied — insufficient scope or inactive subscription.
404
Resource not found, or hidden behind authorization.
409
Conflict — e.g. unique constraint violation.
5xx
Server-side failure. Safe to retry idempotent reads with exponential backoff.

Pagination & filters

List endpoints return all matching items by default. Transactions, the only resource that can grow large, supports cursor-based pagination.

Transactions cursor

cURLPaginated list
curl "https://api.ledgy.app/api/v1/transactions?limit=50&type=expense&dateFrom=2026-01-01" \
  -H "Authorization: Bearer lgpat_…"

The response includes a nextCursor. Pass it back as the cursor query param to fetch the next page; when nextCursor is null you've reached the end.

  • limit — page size, clamped to 1..200. Default 50.
  • cursor — opaque token. Treat it as a black box.
  • type, dateFrom, dateTo, categoryId, accountId, groupId — optional filters; see the List transactions endpoint.

Types & formats

  • IDs — opaque strings. Do not parse them; treat them as case-sensitive UTF-8 identifiers.
  • Timestamps — ISO-8601 in UTC with a trailing Z, e.g. "2026-05-13T10:30:00Z".
  • Dates (e.g. transaction date) — the same ISO-8601 shape, but only the date portion is significant.
  • Money — JSON numbers in major units with up to 4 decimal places (e.g. 12.50). Never minor units. Always paired with currencyCode.
  • Currency codes — ISO-4217, exactly three uppercase letters (e.g. "EUR", "USD", "GBP").
  • Deletes — all delete operations are soft deletes. Deleted items stop appearing in list/get responses; sharing partners and historical receipts are preserved.
  • Side effects — deleting an account, category, or group leaves dependent transactions intact; their references are detached. The Delete account endpoint accepts an explicit strategy.

Accounts

Accounts are the buckets that hold balances — a bank account, a credit card, a cash wallet, a brokerage. Every transaction is linked to one (or two, for transfers).

GET /api/v1/accounts accounts:read

Return every account the authenticated user owns or has access to.

GET /api/v1/accounts/{id} accounts:read

Fetch one account by id. 404 if not found.

POST /api/v1/accounts accounts:write

Create a new account. Returns 201 with the created object and a Location header.

Request body
FieldTypeDescription
namestringrequiredDisplay name. Max 100 chars.
currencyCodestringrequiredISO-4217. Exactly 3 letters.
initialBalancenumberrequiredOpening balance in currencyCode.
orderintegerrequiredSort position. Lower comes first.
iconstringoptionalIcon identifier from /api/v1/icons. Max 50.
colorstringoptionalHex color, e.g. "#4A90E2". Max 20.
iconColorstringoptionalOverride the icon tint.
Request
POST /api/v1/accounts
{
  "name": "Cash",
  "currencyCode": "EUR",
  "initialBalance": 50.00,
  "order": 2,
  "icon": "wallet",
  "color": "#22C55E"
}
201 Created
{
  "id": "acc_01HK8V…",
  "name": "Cash",
  "currencyCode": "EUR",
  "initialBalance": 50.00,
  "order": 2,
  "icon": "wallet",
  "color": "#22C55E",
  "iconColor": null,
  "shareInviteToken": null,
  "createdAt": "2026-05-13T10:30:00Z",
  "updatedAt": "2026-05-13T10:30:00Z"
}
PUT /api/v1/accounts/{id} accounts:write

Replace an existing account. Body is identical to Create; all fields must be provided.

DELETE /api/v1/accounts/{id} accounts:write

Soft-delete an account. Decide what happens to its transactions via the action query parameter.

Query parameters
FieldTypeDescription
actionenumoptionalDetach (default): clear the account ref on each transaction. Move: reassign transactions to moveTargetAccountId. DeleteAll: soft-delete every linked transaction you own.
moveTargetAccountIdstringoptionalRequired when action=Move. Destination account id.

Categories

Categories label what a transaction is for (groceries, rent, freelance income). System categories are read-only and shared by all users; user categories are yours to manage. Category groups bundle related categories together.

GET /api/v1/categories/system-categories categories:read

Return Ledgy's curated set of "well-known" categories — the starter set the iOS app ships with. These are versioned globally and safe to cache by version.

User categories

GET /api/v1/categories categories:read

List all user-defined categories.

GET /api/v1/categories/{id} categories:read

Fetch one user category by id.

POST /api/v1/categories categories:write

Create a user category.

Request body
FieldTypeDescription
namestringrequiredDisplay name. Max 100.
typestringrequired"income" or "expense".
orderintegerrequiredSort position within its group.
isPinnedbooleanrequiredPin to the top of the picker.
categoryGroupIdstringoptionalParent group id, or null for ungrouped.
iconstringoptionalIcon identifier.
colorstringoptionalHex color.
PUT /api/v1/categories/{id} categories:write

Replace a user category. Body is identical to Create.

DELETE /api/v1/categories/{id} categories:write

Soft-delete a user category. Transactions are not deleted; their categoryId is cleared.

Category groups

GET /api/v1/categories/groups categories:read

List your category groups.

GET /api/v1/categories/groups/{id} categories:read

Fetch one category group.

POST /api/v1/categories/groups categories:write

Create a category group.

Request body
FieldTypeDescription
namestringrequiredDisplay name. Max 100.
orderintegerrequiredSort position.
iconstringoptionalIcon identifier.
colorstringoptionalHex color.
PUT /api/v1/categories/groups/{id} categories:write

Replace a category group.

DELETE /api/v1/categories/groups/{id} categories:write

Soft-delete a group. Child categories survive — their categoryGroupId is cleared.

Budgets

A budget caps spending against a category (or, when categoryId is null, against the whole ledger) over a recurring window. Share a budget with a group by setting groupId.

GET /api/v1/budgets budgets:read

List all budgets.

GET /api/v1/budgets/{id} budgets:read

Fetch one budget.

POST /api/v1/budgets budgets:write

Create a budget.

Request body
FieldTypeDescription
amountnumberrequiredLimit per period. Must be greater than 0.
currencyCodestringrequiredISO-4217.
periodintegerrequired0 Weekly · 1 Monthly · 2 Quarterly · 3 Yearly.
startDatestring (ISO-8601)requiredFirst period start.
isActivebooleanrequiredWhether this budget is currently enforced.
endDatestring (ISO-8601)optionalStop tracking after this date.
namestringoptionalLabel. Max 200.
categoryIdstringoptionalCategory to track. Omit to budget all spending.
groupIdstringoptionalGroup to share with. Omit for personal.
PUT /api/v1/budgets/{id} budgets:write

Replace a budget.

DELETE /api/v1/budgets/{id} budgets:write

Soft-delete a budget.

Groups

Groups are shared ledgers — a household, a trip, a shared apartment. Every member sees the same transactions; ownership stays personal. Membership and invites are managed in the iOS app; the API surface here is the resource itself.

GET /api/v1/groups groups:read

List groups you own or are a member of.

GET /api/v1/groups/{id} groups:read

Fetch one group.

POST /api/v1/groups groups:write

Create a group. You become its owner; invite members from the iOS app.

Request body
FieldTypeDescription
namestringrequiredDisplay name. Max 100.
descriptionstringoptionalFree text. Max 500.
iconstringoptionalIcon identifier.
colorstringoptionalHex color.
PUT /api/v1/groups/{id} groups:write

Replace a group's metadata.

DELETE /api/v1/groups/{id} groups:write

Soft-delete a group. Members lose visibility; underlying transactions revert to personal.

Transactions

Transactions are the verbs of the ledger. They come in four shapes: income, expense, transfer (account-to-account), and adjustment (one-off rebalancing). The base endpoint creates income/expense; transfers have their own endpoint; bulk variants exist for high-volume importers.

GET /api/v1/transactions transactions:read

List transactions with cursor pagination and filters.

Query parameters
FieldTypeDescription
limitintegeroptionalPage size. 1..200. Default 50.
cursorstringoptionalOpaque continuation token from the previous response.
typestringoptional"income", "expense", "transfer", or "adjustment".
dateFromstring (ISO-8601)optionalInclusive lower bound.
dateTostring (ISO-8601)optionalInclusive upper bound.
categoryIdstringoptionalFilter to one category.
accountIdstringoptionalFilter to one account.
groupIdstringoptionalFilter to a shared group.
200 OK
{
  "items": [ /* TransactionDto[] */ ],
  "totalCount": 317,
  "nextCursor": "eyJrIjoiMjAyNi0wNS0xM1QxMDoz…"
}
GET /api/v1/transactions/{id} transactions:read

Fetch one transaction.

POST /api/v1/transactions transactions:write

Create a single income or expense transaction. For transfers, use /transfer.

Request body
FieldTypeDescription
typestringrequired"income" or "expense".
amountnumberrequiredPositive amount in currencyCode.
currencyCodestringrequiredISO-4217.
datestring (ISO-8601)requiredWhen the transaction occurred (UTC).
accountIdstringoptionalSource/destination account.
categoryIdstringoptionalCategory label.
payeestringoptionalMerchant or counterparty. Max 200.
notestringoptionalFree-form note. Max 2000.
groupIdstringoptionalShare with a group.
exchangeRatenumberoptionalFX rate when currencyCode ≠ user primary currency.
convertedAmountnumberoptionalAmount in user's primary currency.
Request€8.50 coffee yesterday on Wise
POST /api/v1/transactions
{
  "type": "expense",
  "amount": 8.50,
  "currencyCode": "EUR",
  "date": "2026-05-12T09:14:00Z",
  "accountId": "acc_01HK8V…",
  "categoryId": "cat_food_drinks",
  "payee": "Pret",
  "note": "Flat white"
}
PUT /api/v1/transactions/{id} transactions:write

Replace a transaction. Body is identical to Create.

DELETE /api/v1/transactions/{id} transactions:write

Soft-delete one transaction.

POST /api/v1/transactions/transfer transactions:write

Create a transfer between two accounts. No category. For cross-currency transfers, provide exchangeRate and convertedAmount in the destination currency.

Request body
FieldTypeDescription
fromAccountIdstringrequiredSource account.
toAccountIdstringrequiredDestination account. Must differ from source.
amountnumberrequiredAmount sent, in currencyCode.
currencyCodestringrequiredSource currency, ISO-4217.
datestring (ISO-8601)requiredTransfer date.
exchangeRatenumberoptionalRequired when source and destination currencies differ.
convertedAmountnumberoptionalAmount credited to the destination, in its currency.
notestringoptionalMax 2000.

Bulk operations

Designed for importers. Each batch is capped at 100 items and runs best-effort: a single bad row does not roll back the others. Successful items and per-row errors are reported separately so you can retry the failures.

POST /api/v1/transactions/bulk transactions:write

Create up to 100 transactions in one call.

200 OK
{
  "items": [ /* successful TransactionDto[] */ ],
  "errors": [
    { "index": 3, "error": "Invalid currency code" }
  ]
}
PUT /api/v1/transactions/bulk transactions:write

Update up to 100 transactions in one call. Each item must include its id alongside the full transaction body.

POST /api/v1/transactions/bulk-delete transactions:write

Soft-delete up to 100 transactions in one call. Uses POST rather than DELETE so the request body is accepted by all HTTP clients.

Request body
FieldTypeDescription
idsstring[]required1 to 100 transaction ids.
200 OK
{
  "deleted": 97,
  "notFound": [ "tx_old1", "tx_old2", "tx_old3" ]
}

The transaction object

FieldTypeDescription
idstringOpaque identifier.
typestringincome · expense · transfer · adjustment.
amountnumberIn currencyCode.
currencyCodestringISO-4217.
exchangeRatenumber | nullSet on cross-currency entries.
convertedAmountnumber | nullSame value in the user's primary currency.
datestringISO-8601 UTC.
accountIdstring | nullFor transfers this is the source account.
toAccountIdstring | nullDestination account on transfers only.
categoryIdstring | nullNull on transfers.
groupIdstring | nullSet if shared with a group.
payeestring | nullMerchant or counterparty.
notestring | nullFree text.
receiptImagePathstring | nullPath to attached receipt. Fetch via the receipts endpoint (separate from v1).
recurringTransactionIdstring | nullSet if this row was generated from a recurring rule.
sourcestringOrigin tag — api, mobile, web, import, etc.
createdAtstringISO-8601 UTC.
updatedAtstringISO-8601 UTC.

Icons

Ledgy ships with a curated icon set and color palette used everywhere — accounts, categories, groups. Fetch the catalog once, cache it, and reuse the identifiers when creating resources.

GET /api/v1/icons

Return the full icon library, grouped into categories, alongside the supported color palette. Versioned — safe to cache by the version field.

200 OK
{
  "version": "2026.05.10",
  "library": "font-awesome-6",
  "categories": [
    {
      "id": "finance",
      "name": "Finance",
      "icons": [ "wallet", "credit-card", "piggy-bank" ]
    }
  ],
  "colors": [
    { "name": "Blue", "hex": "#4A90E2" },
    { "name": "Green", "hex": "#22C55E" }
  ]
}

Versioning

  • No breaking changes within v1. We will only add new endpoints, new optional fields, and new enum values. A field's type, nullability, or required-ness will not change.
  • New enum values are not breaking. Treat unknown type, source, or action values as "don't render" rather than crashing — we will add them when the product grows.
  • Date-shaped breaking changes (if ever needed) will ship under /api/v2/ with at least 6 months of parallel availability and a deprecation header on v1 responses.

Support

Found a bug, want an endpoint, or hit something undocumented? Open the help center or email support@ledgy.app — please include the request id (echoed in the X-Request-Id response header) when reporting an issue.

For security-sensitive disclosures (a token leak, a permission bypass, an unauthorized read) write to security@ledgy.app.