Base URL examples:
- Prod:
https://api.expense-manager.com/api - Dev:
https://dev-api.expense-manager.com/api
All endpoints below are prefixed with /api (omitted in headings for brevity).
-
Access token: short-lived JWT (e.g., 15 min)
- Sent as:
Authorization: Bearer <access_token>
- Sent as:
-
Refresh token: long-lived, httpOnly, secure,
SameSite=strictcookie- Used only with
/auth/refreshand/auth/logout.
- Used only with
-
Some admin endpoints may require additional roles in JWT (
role: 'admin').
- Requests:
Content-Type: application/json - Responses:
Content-Type: application/json
All responses:
{
"data": { ... }, // null on error
"error": null // or { "code": "STRING_CODE", "message": "Human readable" }
}- Timestamps in ISO 8601 (UTC), e.g.
2025-11-16T14:23:00Z. - IDs are UUIDv4 strings, e.g.
"c9dc4b0c-...".
-
Query parameters:
page,limitpage(1-based), default1limit, default20, max100
-
Response includes:
{
"data": {
"items": [ ... ],
"pagination": {
"page": 1,
"limit": 20,
"totalItems": 123,
"totalPages": 7
}
},
"error": null
}- Common filters:
start,end(dates),category,wallet_id,jar_id. - Sorting:
sort_by(e.g.paid_at),sort_order(asc|desc).
AUTH_INVALID_CREDENTIALSAUTH_TOKEN_EXPIREDAUTH_FORBIDDENRESOURCE_NOT_FOUNDVALIDATION_ERRORRATE_LIMIT_EXCEEDEDCONFLICT_IDEMPOTENTSERVER_ERROR
Create a new user account (email/password).
Auth: Public Body:
{
"email": "user@example.com",
"name": "Ashish",
"password": "password123",
"timezone": "Asia/Kolkata",
"currency": "INR"
}Responses:
201 Created
{
"data": {
"user": {
"id": "uuid",
"email": "user@example.com",
"name": "Ashish",
"timezone": "Asia/Kolkata",
"currency": "INR",
"created_at": "2025-11-16T14:23:00Z"
}
},
"error": null
}- Sets refresh token cookie (httpOnly, secure).
Email/password login.
Auth: Public Body:
{
"email": "user@example.com",
"password": "password123"
}Responses:
200 OK
{
"data": {
"access_token": "jwt-access-token"
},
"error": null
}- Sets refresh token cookie.
Obtain a new access token using the refresh token cookie.
Auth: Requires valid refresh token cookie Body: none
Response:
200 OK
{
"data": {
"access_token": "new-jwt-access-token"
},
"error": null
}Invalidate refresh token & clear cookie.
Auth: Requires refresh token cookie Body: none
Response:
204 No Content(Envelope can be{ "data": null, "error": null }if you prefer consistency.)
Handle OAuth callback for providers like google, apple.
Auth: Public Path params:
provider:google|apple
Body (depending on provider, often query params / code exchange):
{
"code": "oauth-auth-code",
"redirect_uri": "https://app.expense-manager.com/oauth/callback"
}Response:
200 OKSame as login: returnsaccess_tokenand sets refresh cookie.
Get current authenticated user profile.
Auth: Bearer access token
Response:
{
"data": {
"id": "uuid",
"email": "user@example.com",
"name": "Ashish",
"timezone": "Asia/Kolkata",
"currency": "INR",
"created_at": "2025-11-16T14:23:00Z",
"updated_at": "2025-11-16T14:23:00Z"
},
"error": null
}Update user profile.
Auth: Bearer
Body (partial):
{
"name": "Ashish C",
"timezone": "Asia/Kolkata",
"currency": "USD"
}Response:
200 OKwith updated user.
Change password (for email/password users).
Auth: Bearer
Body:
{
"current_password": "oldPass123",
"new_password": "newPass456"
}Represents accounts like "Cash", "Bank", "Credit Card".
List wallets for current user.
Auth: Bearer
Query: pagination optional.
Response:
{
"data": {
"items": [
{
"id": "uuid",
"name": "HDFC Bank",
"type": "bank",
"currency": "INR",
"initial_balance": 5000,
"current_balance": 4200,
"created_at": "2025-11-16T14:23:00Z"
}
],
"pagination": { ... }
},
"error": null
}Create a wallet.
Body:
{
"name": "HDFC Bank",
"type": "bank",
"currency": "INR",
"initial_balance": 5000
}Get wallet details.
Update wallet (e.g., name, type).
Soft delete wallet (if no constraints violated); may reject if it has transactions.
Optional monthly/weekly budgets per category/wallet.
List budgets.
Create a budget.
Body:
{
"name": "Monthly Food",
"period": "monthly",
"start_date": "2025-11-01",
"amount": 10000,
"currency": "INR",
"category": "food",
"wallet_id": "uuid"
}Budget details with usage summary.
Update budget.
Delete budget.
List all categories (system + user-defined).
Create a custom category.
Update custom category.
Delete custom category (may require re-mapping or deny if in use).
Primary transaction resource.
List expenses with filters.
Auth: Bearer
Query params:
start: ISO date (optional)end: ISO date (optional)category: string (optional)wallet_id: UUID (optional)jar_id: UUID (optional)is_recurring: boolean (optional)page,limit
Response: paginated list.
Create an expense.
Auth: Bearer
Body:
{
"amount": 500,
"currency": "INR",
"category": "food",
"note": "Dinner with friends",
"paid_at": "2025-11-16T19:30:00+05:30",
"wallet_id": "uuid-wallet",
"jar_id": "uuid-jar-optional",
"is_recurring": false,
"recurring_rule": null, // optional (RRULE JSON or custom structure)
"metadata": {
"merchant": "Swiggy",
"tags": ["friends", "weekend"]
}
}Server behaviour:
-
Wrap in DB transaction:
- Insert expense row.
- Adjust
wallet.current_balance. - If
jar_idpresent, create jar transaction and update jar amount. - Insert outbox events for analytics & notifications.
Response:
201 Createdwith created expense.
Get one expense.
Design choice: Rather than directly modifying historic rows, you can:
-
Either:
- Allow limited updates (note/category only), or
-
For amount changes, append reversing transaction internally.
Body:
{
"amount": 600,
"category": "food",
"note": "Updated: included dessert"
}Soft delete an expense.
- Marks as deleted and adds reversing effect to wallet/jar.
- Response:
204 No Contentor200with updated expense state.
List all jars.
Response:
{
"data": {
"items": [
{
"id": "uuid",
"name": "Emergency Fund",
"mode": "auto" , // 'auto' | 'manual'
"current_amount": 15000,
"target_amount": 50000,
"auto_amount": 500,
"auto_frequency": "daily", // daily|weekly|monthly
"allow_manual_topup": true,
"last_auto_run": "2025-11-15",
"created_at": "2025-10-01T00:00:00Z"
}
],
"pagination": { ... }
},
"error": null
}Create a jar.
Body:
{
"name": "Emergency Fund",
"mode": "auto",
"auto_amount": 500,
"auto_frequency": "daily",
"target_amount": 50000,
"allow_manual_topup": true
}Jar details (including summary).
Update jar settings (mode, auto rules, target, name).
Delete or archive jar (if allowed).
List jar-specific transactions.
Query params:
start,end,page,limit
Manual or external top-up.
Body:
{
"amount": 1000,
"source": "manual", // 'manual' | 'external'
"note": "Salary topup",
"wallet_id": "uuid-wallet" // optional; if provided, money moves from wallet
}-
Wrapped in DB transaction:
- Update jar amount.
- Adjust wallet if provided.
- Insert jar_transaction row.
- Insert outbox event for analytics.
Withdraw from jar into wallet.
Body:
{
"amount": 2000,
"target_wallet_id": "uuid-wallet",
"note": "Emergency hospital bill"
}List recurring rules for current user.
Query: page, limit, type, enabled
Create recurring rule for expenses or jars.
Body:
{
"type": "expense", // 'expense' | 'jar_topup'
"rule": {
"rrule": "FREQ=MONTHLY;BYMONTHDAY=1"
},
"start_date": "2025-11-01",
"enabled": true,
"template": {
"amount": 2000,
"currency": "INR",
"category": "rent",
"wallet_id": "uuid-wallet",
"note": "Monthly rent"
}
}Details of a recurring rule.
Update recurring rule (enable/disable, change frequency, etc.).
Delete recurring rule.
Admin/debug endpoint to trigger scheduled jobs (e.g., for testing).
Auth: Admin only Body:
{
"job_type": "jar_daily_save",
"jar_id": "uuid",
"date": "2025-11-16"
}High-level aggregates.
Auth: Bearer
Query:
range:7d|30d|90d|customstart,end(ifrange=custom)
Example Response:
{
"data": {
"total_expense": 25000,
"total_income": 30000,
"net_savings": 5000,
"by_category": [
{ "category": "food", "amount": 9000 },
{ "category": "rent", "amount": 12000 }
],
"by_wallet": [
{ "wallet_id": "uuid", "amount": 15000 }
]
},
"error": null
}Time-series data for charts.
Query:
range:30d(etc.)granularity:daily|weekly|monthly
Response:
{
"data": {
"points": [
{ "date": "2025-11-01", "expense": 500, "income": 0 },
{ "date": "2025-11-02", "expense": 700, "income": 0 }
]
},
"error": null
}(optional separate endpoint)
Summary of jars for dashboard widget.
List notifications.
Query:
status:unread|allpage,limit
Response:
{
"data": {
"items": [
{
"id": "uuid",
"type": "jar_auto_credit_failed",
"title": "Jar auto-save failed",
"body": "We couldn't auto-save ₹500 to Emergency Fund.",
"read": false,
"created_at": "2025-11-16T14:23:00Z"
}
],
"pagination": { ... }
},
"error": null
}Mark notifications as read.
Body:
{
"ids": ["uuid1", "uuid2"]
}Delete (or archive) a single notification.
Send a message to AI assistant.
Auth: Bearer
Body:
{
"session_id": "uuid-optional",
"message": "How much did I spend on food this month?",
"include_personal_data": true // if true, server will use aggregates/analytics
}Server behaviour:
- Fetch aggregate data (never raw PII unless absolutely necessary).
- Call vector DB + LLM.
- Optionally store conversation summary (not raw transaction data).
Response:
{
"data": {
"session_id": "uuid",
"reply": "You've spent ₹9,000 on food so far this month.",
"sources": [
{
"type": "aggregate",
"range": "2025-11-01 to 2025-11-16",
"category": "food"
}
]
},
"error": null
}Get AI conversation history.
Query:
session_id(filter single session)- or list last N sessions.
Basic health check.
Auth: Public or IP-restricted.
Response:
{
"data": {
"status": "ok",
"uptime": 12345,
"dependencies": {
"db": "ok",
"redis": "ok",
"queue": "ok"
}
},
"error": null
}Prometheus metrics endpoint.
Auth: Protected via IP allowlist or basic auth.
- Exposes plain text metrics (not JSON).
Trigger manual refresh of materialized views / analytics caches.
Auth: Admin only Body:
{
"scope": "all" // 'all' | 'dashboard' | 'category_breakdown'
}Admin-only user list (for support / management).
Auth: Admin
Query: email, page, limit, etc.
Request data export (e.g., CSV of expenses).
Body:
{
"format": "csv", // 'csv' | 'xlsx'
"start": "2025-11-01",
"end": "2025-11-30",
"email_when_ready": true
}- Enqueues job; sends email when completed with link to S3 object.
Response:
{
"data": {
"export_id": "uuid",
"status": "queued"
},
"error": null
}Check export status or download link.
Account/data deletion request (for compliance).
For non-safe endpoints that might be retried (e.g., payments, jar top-ups):
- Header:
Idempotency-Key: <uuid> - If a request with the same key was already processed, server returns previous response and
409or200depending on approach.
-
Per-IP and per-user rate limits on:
/auth/login/auth/signup/ai/chat
-
Too many requests →
429 Too Many Requestswith:
{
"data": null,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests. Please try again later."
}
}