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.
camelCaseproperty names. - Authentication:
Authorization: Bearer …on every request. - Cloud subscription: required on every v1 endpoint. See Subscription gate.
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
- Sign in to your Ledgy dashboard and open Settings → API Access.
- Click + New token.
- Give the token a descriptive name (e.g. "Zapier — weekly export"), pick the scopes you need (see Scopes), and optionally set an expiry.
- 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
# Replace lgpat_… with your token curl "https://api.ledgy.app/api/v1/accounts" \ -H "Authorization: Bearer lgpat_a1b2c3d4e5…"
3. Inspect the response
{
"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
}
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
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:
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:
{ "error": "Human-readable message" }
Status codes you should handle:
- 200
- OK — resource returned, or list returned.
- 201
- Created — new resource created.
Locationheader 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
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 to1..200. Default50.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 withcurrencyCode. - 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).
/api/v1/accounts
accounts:read
Return every account the authenticated user owns or has access to.
/api/v1/accounts/{id}
accounts:read
Fetch one account by id. 404 if not found.
/api/v1/accounts
accounts:write
Create a new account. Returns 201 with the created object and a Location header.
| Field | Type | Description |
|---|---|---|
| name | string | requiredDisplay name. Max 100 chars. |
| currencyCode | string | requiredISO-4217. Exactly 3 letters. |
| initialBalance | number | requiredOpening balance in currencyCode. |
| order | integer | requiredSort position. Lower comes first. |
| icon | string | optionalIcon identifier from /api/v1/icons. Max 50. |
| color | string | optionalHex color, e.g. "#4A90E2". Max 20. |
| iconColor | string | optionalOverride the icon tint. |
POST /api/v1/accounts
{
"name": "Cash",
"currencyCode": "EUR",
"initialBalance": 50.00,
"order": 2,
"icon": "wallet",
"color": "#22C55E"
}
{
"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"
}
/api/v1/accounts/{id}
accounts:write
Replace an existing account. Body is identical to Create; all fields must be provided.
/api/v1/accounts/{id}
accounts:write
Soft-delete an account. Decide what happens to its transactions via the action query parameter.
| Field | Type | Description |
|---|---|---|
| action | enum | optionalDetach (default): clear the account ref on each transaction. Move: reassign transactions to moveTargetAccountId. DeleteAll: soft-delete every linked transaction you own. |
| moveTargetAccountId | string | optionalRequired 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.
/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
/api/v1/categories
categories:read
List all user-defined categories.
/api/v1/categories/{id}
categories:read
Fetch one user category by id.
/api/v1/categories
categories:write
Create a user category.
| Field | Type | Description |
|---|---|---|
| name | string | requiredDisplay name. Max 100. |
| type | string | required"income" or "expense". |
| order | integer | requiredSort position within its group. |
| isPinned | boolean | requiredPin to the top of the picker. |
| categoryGroupId | string | optionalParent group id, or null for ungrouped. |
| icon | string | optionalIcon identifier. |
| color | string | optionalHex color. |
/api/v1/categories/{id}
categories:write
Soft-delete a user category. Transactions are not deleted; their categoryId is cleared.
Category groups
/api/v1/categories/groups
categories:read
List your category groups.
/api/v1/categories/groups/{id}
categories:read
Fetch one category group.
/api/v1/categories/groups
categories:write
Create a category group.
| Field | Type | Description |
|---|---|---|
| name | string | requiredDisplay name. Max 100. |
| order | integer | requiredSort position. |
| icon | string | optionalIcon identifier. |
| color | string | optionalHex color. |
/api/v1/categories/groups/{id}
categories:write
Replace a category group.
/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.
/api/v1/budgets
budgets:read
List all budgets.
/api/v1/budgets/{id}
budgets:read
Fetch one budget.
/api/v1/budgets
budgets:write
Create a budget.
| Field | Type | Description |
|---|---|---|
| amount | number | requiredLimit per period. Must be greater than 0. |
| currencyCode | string | requiredISO-4217. |
| period | integer | required0 Weekly · 1 Monthly · 2 Quarterly · 3 Yearly. |
| startDate | string (ISO-8601) | requiredFirst period start. |
| isActive | boolean | requiredWhether this budget is currently enforced. |
| endDate | string (ISO-8601) | optionalStop tracking after this date. |
| name | string | optionalLabel. Max 200. |
| categoryId | string | optionalCategory to track. Omit to budget all spending. |
| groupId | string | optionalGroup to share with. Omit for personal. |
/api/v1/budgets/{id}
budgets:write
Replace a budget.
/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.
/api/v1/groups
groups:read
List groups you own or are a member of.
/api/v1/groups/{id}
groups:read
Fetch one group.
/api/v1/groups
groups:write
Create a group. You become its owner; invite members from the iOS app.
| Field | Type | Description |
|---|---|---|
| name | string | requiredDisplay name. Max 100. |
| description | string | optionalFree text. Max 500. |
| icon | string | optionalIcon identifier. |
| color | string | optionalHex color. |
/api/v1/groups/{id}
groups:write
Replace a group's metadata.
/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.
/api/v1/transactions
transactions:read
List transactions with cursor pagination and filters.
| Field | Type | Description |
|---|---|---|
| limit | integer | optionalPage size. 1..200. Default 50. |
| cursor | string | optionalOpaque continuation token from the previous response. |
| type | string | optional"income", "expense", "transfer", or "adjustment". |
| dateFrom | string (ISO-8601) | optionalInclusive lower bound. |
| dateTo | string (ISO-8601) | optionalInclusive upper bound. |
| categoryId | string | optionalFilter to one category. |
| accountId | string | optionalFilter to one account. |
| groupId | string | optionalFilter to a shared group. |
{
"items": [ /* TransactionDto[] */ ],
"totalCount": 317,
"nextCursor": "eyJrIjoiMjAyNi0wNS0xM1QxMDoz…"
}
/api/v1/transactions/{id}
transactions:read
Fetch one transaction.
/api/v1/transactions
transactions:write
Create a single income or expense transaction. For transfers, use /transfer.
| Field | Type | Description |
|---|---|---|
| type | string | required"income" or "expense". |
| amount | number | requiredPositive amount in currencyCode. |
| currencyCode | string | requiredISO-4217. |
| date | string (ISO-8601) | requiredWhen the transaction occurred (UTC). |
| accountId | string | optionalSource/destination account. |
| categoryId | string | optionalCategory label. |
| payee | string | optionalMerchant or counterparty. Max 200. |
| note | string | optionalFree-form note. Max 2000. |
| groupId | string | optionalShare with a group. |
| exchangeRate | number | optionalFX rate when currencyCode ≠ user primary currency. |
| convertedAmount | number | optionalAmount in user's primary currency. |
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"
}
/api/v1/transactions/{id}
transactions:write
Replace a transaction. Body is identical to Create.
/api/v1/transactions/{id}
transactions:write
Soft-delete one transaction.
/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.
| Field | Type | Description |
|---|---|---|
| fromAccountId | string | requiredSource account. |
| toAccountId | string | requiredDestination account. Must differ from source. |
| amount | number | requiredAmount sent, in currencyCode. |
| currencyCode | string | requiredSource currency, ISO-4217. |
| date | string (ISO-8601) | requiredTransfer date. |
| exchangeRate | number | optionalRequired when source and destination currencies differ. |
| convertedAmount | number | optionalAmount credited to the destination, in its currency. |
| note | string | optionalMax 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.
/api/v1/transactions/bulk
transactions:write
Create up to 100 transactions in one call.
{
"items": [ /* successful TransactionDto[] */ ],
"errors": [
{ "index": 3, "error": "Invalid currency code" }
]
}
/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.
/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.
| Field | Type | Description |
|---|---|---|
| ids | string[] | required1 to 100 transaction ids. |
{
"deleted": 97,
"notFound": [ "tx_old1", "tx_old2", "tx_old3" ]
}
The transaction object
| Field | Type | Description |
|---|---|---|
| id | string | Opaque identifier. |
| type | string | income · expense · transfer · adjustment. |
| amount | number | In currencyCode. |
| currencyCode | string | ISO-4217. |
| exchangeRate | number | null | Set on cross-currency entries. |
| convertedAmount | number | null | Same value in the user's primary currency. |
| date | string | ISO-8601 UTC. |
| accountId | string | null | For transfers this is the source account. |
| toAccountId | string | null | Destination account on transfers only. |
| categoryId | string | null | Null on transfers. |
| groupId | string | null | Set if shared with a group. |
| payee | string | null | Merchant or counterparty. |
| note | string | null | Free text. |
| receiptImagePath | string | null | Path to attached receipt. Fetch via the receipts endpoint (separate from v1). |
| recurringTransactionId | string | null | Set if this row was generated from a recurring rule. |
| source | string | Origin tag — api, mobile, web, import, etc. |
| createdAt | string | ISO-8601 UTC. |
| updatedAt | string | ISO-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.
/api/v1/icons
Return the full icon library, grouped into categories, alongside the supported color palette. Versioned — safe to cache by the version field.
{
"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, oractionvalues 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.