MExpense · Developer docs

Your finances. Your integrations.

Connect scripts, shortcuts, and AI assistants to MExpense. Everything you need to build with API v1.

Use MExpense with an AI assistant

Copy this prompt, replace the request at the end, and give it to an AI assistant with HTTP tools. Configure your API key privately in that assistant.

Documentation is public and needs no login. Your financial data requires your API key. An assistant running outside your computer needs your deployed site URL instead of localhost.

On this page

MExpense API v1

Manage your transactions from scripts, mobile shortcuts, automation tools, and trusted server applications. The API uses JSON over HTTPS. All paths below are relative to your MExpense site; replace https://your-mexpense.example with your deployment's origin.

AI assistants and public documentation

The human guide at /api-docs, the OpenAPI specification at /openapi.json, and this full plain-text guide at /llms-full.txt are public and require no login or API key. /llms.txt links to these resources for agent discovery. Financial data endpoints still require your Bearer API key.

Open /api-docs and choose Copy AI prompt for a ready-to-use instruction prompt. Add your natural-language request at the end, then give it to an assistant with HTTP tools. Supply your API key privately through that assistant's credentials or environment configuration. Use the deployed site's origin when the assistant cannot reach your local computer.

Quick start

1. Sign in and open Developer API (/developer-api). 2. Choose Generate API key and copy the key immediately. It is displayed only once. 3. Fetch your accounts and categories to obtain their IDs. 4. Create a transaction using those IDs.

export MEXPENSE_URL='https://your-mexpense.example'
export MEXPENSE_API_KEY='YOUR_GENERATED_KEY'

curl "$MEXPENSE_URL/api/v1/accounts" \
  -H "Authorization: Bearer $MEXPENSE_API_KEY"

curl "$MEXPENSE_URL/api/v1/categories?type=expense" \
  -H "Authorization: Bearer $MEXPENSE_API_KEY"

curl "$MEXPENSE_URL/api/v1/transactions" \
  -H "Authorization: Bearer $MEXPENSE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"type":"expense","amount":"12.50","fromAccountId":"ACCOUNT_ID","categoryId":"CATEGORY_ID","note":"Lunch","date":"2026-10-23T12:30:00+06:00"}'

Authentication and key lifecycle

Send Authorization: Bearer YOUR_GENERATED_KEY on every request. Session cookies, URL query keys, and X-API-Key are not accepted. Use HTTPS outside local development. A key has full transaction access for its owner, plus read access to their accounts and categories. There are no restricted scopes or automatic expiry in v1. Banned accounts cannot use the API.

Each user has one active key. Generate creates a new key; Regenerate replaces it and invalidates the previous key for subsequent authentication; Delete revokes it. Requests already authenticated may finish during revocation. Update all integrations after regeneration. If the one-time key is lost, regenerate it. Keys are managed only in the signed-in Developer API screen (/developer-api), not through the public API.

The server stores a SHA-256 hash and creation/display metadata in UserSettings.settings.apiKey. It does not store a recoverable secret. Never put keys in frontend bundles, public repositories, screenshots, analytics, or shared URLs. Store them as server environment variables or in a secrets manager. The key includes an encoded owner identifier; treat the entire value as sensitive.

Endpoints

MethodPathPurposeSuccess
GET/api/v1/transactionsList and filter your transactions200
POST/api/v1/transactionsCreate a transaction and update balances201
GET/api/v1/transactions/{id}Read one transaction200
PATCH/api/v1/transactions/{id}Update selected fields and recalculate balances200
DELETE/api/v1/transactions/{id}Delete a transaction and reverse its balance effect204
GET/api/v1/accountsList your active accounts200
GET/api/v1/categoriesList your categories200

Account and category creation, updates, deletion, budgets, debts, bulk mutations, and key management are outside this API. Unsupported methods return HTTP 405. Use PATCH for transaction updates; PUT is not supported.

Response format and pagination

Single-resource success responses use {"data":{...}}. Lists use {"data":[...],"pagination":{"limit":50,"offset":0,"hasMore":false}}. DELETE returns an empty body. Do not try to parse JSON from a 204 response. POST includes a Location header with the new transaction's relative URL. All implemented responses use Cache-Control: no-store.

Every list accepts limit (transactions: 1–1,000; accounts and categories: 1–100; default 50) and offset (0–1,000,000, default 0). To fetch the next page, add limit to offset until hasMore is false. Transactions are ordered by date descending, then ID descending. Accounts and categories are ordered by ID ascending. Offset pagination can shift if records are added or removed while paging; use a fixed date window and deduplicate by ID when synchronizing. There is no total-count field or snapshot guarantee.

{
  "data": [{
    "id": "TRANSACTION_ID",
    "userId": "OWNER_ID",
    "type": "expense",
    "amount": "12.50",
    "note": "Lunch",
    "date": "2026-10-23T06:30:00.000Z",
    "fromAccountId": "ACCOUNT_ID",
    "toAccountId": null,
    "categoryId": "CATEGORY_ID",
    "createdAt": "2026-10-23T06:31:00.000Z",
    "updatedAt": "2026-10-23T06:31:00.000Z"
  }],
  "pagination": {"limit":50,"offset":0,"hasMore":false}
}

Transaction IDs and related IDs are opaque strings. Decimal amounts and account balances are JSON strings; trailing zeros may be normalized. Timestamps are ISO 8601 strings. References are IDs, not embedded account/category objects. Deleted accounts or categories may leave null references on older transactions.

List and filter transactions

GET /api/v1/transactions supports these query parameters in addition to limit and offset. Filters combine with AND. Unknown query parameters return 400.

ParameterFormatBehavior
typeincome, expense, transferExact transaction type
categoryIdIDExact category ID
accountIdIDMatches either source or destination account
startDateISO timestamp with timezoneInclusive lower bound on transaction date
endDateISO timestamp with timezoneInclusive upper bound on transaction date
searchText, up to 200 charactersCase-insensitive literal substring in note

startDate must not be after endDate. Use timestamps with Z or an explicit timezone offset; date-only values are rejected. Use curl --data-urlencode so plus signs in timezone offsets and special characters are encoded correctly.

curl -G "$MEXPENSE_URL/api/v1/transactions" \
  -H "Authorization: Bearer $MEXPENSE_API_KEY" \
  --data-urlencode 'type=expense' \
  --data-urlencode 'startDate=2026-10-01T00:00:00+06:00' \
  --data-urlencode 'endDate=2026-10-31T23:59:59.999+06:00' \
  --data-urlencode 'search=Lunch' \
  --data-urlencode 'limit=100' --data-urlencode 'offset=0'

GET /api/v1/transactions/{id} returns one transaction, or 404 if the ID does not exist or belongs to another user. Lists never include another user's records.

Create a transaction

POST /api/v1/transactions requires Content-Type: application/json and a JSON object. Request bodies are limited to 16 KiB. Unknown fields, including id, userId, createdAt, and updatedAt, are rejected.

FieldRequiredAccepted values
typeYesincome, expense, or transfer
amountYesPositive decimal string, at most 15 integer and 8 fractional digits
dateNoISO timestamp with timezone; defaults to the current server time
noteNoString of at most 5,000 characters or null; defaults to null
fromAccountIdDepends on typeYour active source account ID or null
toAccountIdDepends on typeYour active destination account ID or null
categoryIdNoYour category ID matching type, or null; defaults to null

Expenses require fromAccountId and a null/omitted toAccountId. Income requires toAccountId and a null/omitted fromAccountId. Transfers require two distinct active accounts with the same currency. A category, when supplied, must belong to you and match the transaction type. Archived accounts cannot be used for new or updated transactions. An account may have a negative resulting balance.

Use strings such as "0.01", "12.50", or "1000" for amount. JSON numbers, negative amounts, zero, exponent notation, commas, currency symbols, and more than eight fractional digits are rejected. Amount is denominated in the associated account's currency. There is no exchange-rate conversion.

{"type":"income","amount":"1500.00","toAccountId":"BANK_ACCOUNT_ID","note":"Salary"}
{"type":"transfer","amount":"100.00","fromAccountId":"BANK_ACCOUNT_ID","toAccountId":"CASH_ACCOUNT_ID","note":"Cash withdrawal"}

Creating an expense subtracts from its source account; income adds to its destination account; transfers subtract from the source and add to the destination. Transaction creation and balance changes commit together or roll back together. Decimal arithmetic avoids floating point rounding.

Update a transaction

PATCH /api/v1/transactions/{id} accepts any nonempty subset of the create fields. Omitted fields retain their values. Set note or categoryId to null to clear them. Set an account field to null when switching transaction type. Required values such as amount, type, and date cannot be null.

The resulting complete transaction must satisfy all creation rules. Changing an expense to income requires setting fromAccountId to null and supplying toAccountId. Updating an older record whose account was archived or deleted requires selecting valid active accounts. The server reverses the old balance effect and applies the new effect atomically, then returns the complete updated transaction in data.

curl -X PATCH "$MEXPENSE_URL/api/v1/transactions/TRANSACTION_ID" \
  -H "Authorization: Bearer $MEXPENSE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"amount":"15.00","note":"Lunch and coffee"}'
{"type":"income","fromAccountId":null,"toAccountId":"BANK_ACCOUNT_ID","categoryId":null}

Delete a transaction

DELETE /api/v1/transactions/{id} needs no body. It permanently removes the transaction and reverses its effect on any remaining linked accounts in the same database transaction. Success returns 204 with no JSON body. Missing or other-user transactions return 404, including a second delete of the same ID.

curl -X DELETE "$MEXPENSE_URL/api/v1/transactions/TRANSACTION_ID" \
  -H "Authorization: Bearer $MEXPENSE_API_KEY"

Discover accounts and categories

GET /api/v1/accounts returns only your active accounts. Each data item contains id, name, type, balance (decimal string), currency (currency code), icon, and color. It accepts limit and offset.

GET /api/v1/categories returns your categories with id, name, type, icon, and color. It accepts limit, offset, and an optional type filter (income, expense, or transfer). Both lists use the pagination envelope above. Follow all pages when selecting IDs.

{"data":[{"id":"ACCOUNT_ID","name":"Cash","type":"cash","balance":"120.50","currency":"USD","icon":"wallet","color":"#10B981"}],"pagination":{"limit":50,"offset":0,"hasMore":false}}
{"data":[{"id":"CATEGORY_ID","name":"Food","type":"expense","icon":"tag","color":"#6366F1"}],"pagination":{"limit":50,"offset":0,"hasMore":false}}

Errors and rate limits

Implemented handlers return errors in this format. Framework-generated errors such as unsupported methods may use a different body.

{"error":{"code":"invalid_request","message":"Expenses require fromAccountId and no toAccountId."}}
StatusCodeMeaning / recovery
400invalid_requestCorrect the fields, filters, account/category ownership, or transaction rules
401unauthorizedSupply a valid current Bearer key; revoked keys fail here; WWW-Authenticate: Bearer is included
403forbiddenAccount access is disabled
404not_foundTransaction is absent or inaccessible
413payload_too_largeReduce the body to at most 16 KiB
415unsupported_media_typeSend Content-Type: application/json
429rate_limitedWait for the Retry-After duration in seconds
500internal_errorRequest failed; check outcome before retrying a write

The API allows 120 authenticated requests per minute per user per server process. All endpoints share the same allowance. This is an in-memory safeguard: multiple instances have independent counters and restarts reset them. Deployments needing a global limit should enforce it in their gateway. The rate limiter runs after authentication, so it is not a distributed brute-force throttle.

Do not blindly retry POST after timeouts or 500 responses: a write may have committed before the response was lost. v1 does not support idempotency keys. Check the transaction list before retrying uncertain writes; identical POST requests create separate transactions. Reads can be retried with backoff. There are no webhooks or updated-since filters in v1.

JavaScript example

Run this in a trusted server process with environment variables configured. Browser pages on another origin are not granted CORS access by default; use a server-side integration. Never embed the key in a public client application.

const base = process.env.MEXPENSE_URL;
const key = process.env.MEXPENSE_API_KEY;

async function api(path, options = {}) {
  const response = await fetch(`${base}/api/v1${path}`, {
    ...options,
    headers: {
      Authorization: `Bearer ${key}`,
      ...(options.body ? { "Content-Type": "application/json" } : {}),
      ...options.headers,
    },
  });
  if (response.status === 204) return null;
  const result = await response.json();
  if (!response.ok) throw new Error(`${response.status}: ${result.error?.message}`);
  return result;
}

const accounts = await api('/accounts');
const account = accounts.data[0]; // Select the intended account in real integrations.
if (!account) throw new Error('Create an account in MExpense first.');
const created = await api('/transactions', {
  method: 'POST',
  body: JSON.stringify({ type: 'expense', amount: '9.99', fromAccountId: account.id, note: 'Automation example' }),
});
await api(`/transactions/${created.data.id}`, {
  method: 'PATCH', body: JSON.stringify({ note: 'Updated by automation' }),
});
// To remove it: await api(`/transactions/${created.data.id}`, { method: 'DELETE' });

Deployment and compatibility

The OpenAPI 3.1 specification is available at /openapi.json for importing into API clients and generating integrations. This guide is available in the app at /api-docs and in the repository at docs/API.md.

This feature adds one nullable JSON column, UserSettings.settings. A migration is included for this column. Apply it to each deployment with bun run migrate before running this version against that database; the command skips migrations already applied. Existing preferences are preserved; existing settings rows begin with no API key. No API keys are enabled automatically. Do not store API credentials in database backups shared with other users.

The /api/v1 prefix identifies this API version. Clients should tolerate additional response fields. Existing transaction writes made through the app remain visible through the API; refresh an already-open app view to see external changes.