# 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. ```sh 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 | Method | Path | Purpose | Success | | --- | --- | --- | --- | | GET | /api/v1/transactions | List and filter your transactions | 200 | | POST | /api/v1/transactions | Create a transaction and update balances | 201 | | GET | /api/v1/transactions/{id} | Read one transaction | 200 | | PATCH | /api/v1/transactions/{id} | Update selected fields and recalculate balances | 200 | | DELETE | /api/v1/transactions/{id} | Delete a transaction and reverse its balance effect | 204 | | GET | /api/v1/accounts | List your active accounts | 200 | | GET | /api/v1/categories | List your categories | 200 | 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. ```json { "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. | Parameter | Format | Behavior | | --- | --- | --- | | type | income, expense, transfer | Exact transaction type | | categoryId | ID | Exact category ID | | accountId | ID | Matches either source or destination account | | startDate | ISO timestamp with timezone | Inclusive lower bound on transaction date | | endDate | ISO timestamp with timezone | Inclusive upper bound on transaction date | | search | Text, up to 200 characters | Case-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. ```sh 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. | Field | Required | Accepted values | | --- | --- | --- | | type | Yes | income, expense, or transfer | | amount | Yes | Positive decimal string, at most 15 integer and 8 fractional digits | | date | No | ISO timestamp with timezone; defaults to the current server time | | note | No | String of at most 5,000 characters or null; defaults to null | | fromAccountId | Depends on type | Your active source account ID or null | | toAccountId | Depends on type | Your active destination account ID or null | | categoryId | No | Your 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. ```json {"type":"income","amount":"1500.00","toAccountId":"BANK_ACCOUNT_ID","note":"Salary"} ``` ```json {"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. ```sh 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"}' ``` ```json {"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. ```sh 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. ```json {"data":[{"id":"ACCOUNT_ID","name":"Cash","type":"cash","balance":"120.50","currency":"USD","icon":"wallet","color":"#10B981"}],"pagination":{"limit":50,"offset":0,"hasMore":false}} ``` ```json {"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. ```json {"error":{"code":"invalid_request","message":"Expenses require fromAccountId and no toAccountId."}} ``` | Status | Code | Meaning / recovery | | --- | --- | --- | | 400 | invalid_request | Correct the fields, filters, account/category ownership, or transaction rules | | 401 | unauthorized | Supply a valid current Bearer key; revoked keys fail here; WWW-Authenticate: Bearer is included | | 403 | forbidden | Account access is disabled | | 404 | not_found | Transaction is absent or inaccessible | | 413 | payload_too_large | Reduce the body to at most 16 KiB | | 415 | unsupported_media_type | Send Content-Type: application/json | | 429 | rate_limited | Wait for the Retry-After duration in seconds | | 500 | internal_error | Request 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. ```js 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.