Complete reference for all API endpoints, including request/response schemas, authentication details, query parameters, and error responses.
http://localhost:5000
All private endpoints require a JWT token sent via the Authorization header:
Authorization: Bearer <your_jwt_token>
Tokens are obtained via the /api/auth/login or /api/auth/register endpoints.
| Property | Value |
|---|---|
| Type | JWT (JSON Web Token) |
| Algorithm | HS256 |
| Expiry | Configurable via JWT_EXPIRE env var (default: 7d) |
| Payload | { "id": "<user_mongo_id>", "iat": ..., "exp": ... } |
All responses follow a consistent JSON structure:
{
"success": true,
"message": "Optional success message",
"data": { ... },
"token": "..."
}{
"success": false,
"message": "Error description",
"stack": "..."
}The
stackfield only appears whenNODE_ENV=development.
{
"success": true,
"total": 50,
"page": 1,
"pages": 5,
"count": 10,
"data": [ ... ]
}| Field | Type | Description |
|---|---|---|
total |
Number | Total number of matching documents |
page |
Number | Current page number |
pages |
Number | Total number of pages |
count |
Number | Number of documents in current page |
data |
Array | Array of documents |
| Code | Meaning | Usage |
|---|---|---|
200 |
OK | Successful GET, PUT, DELETE |
201 |
Created | Successful POST (resource created) |
400 |
Bad Request | Validation errors, invalid input |
401 |
Unauthorized | Missing, invalid, or expired token |
403 |
Forbidden | Insufficient role permissions |
404 |
Not Found | Route does not exist |
409 |
Conflict | Duplicate key violation |
429 |
Too Many Requests | Rate limit exceeded |
500 |
Internal Server Error | Unexpected server failure |
| Limiter | Scope | Window | Max Requests |
|---|---|---|---|
| General API | All routes | 15 minutes | 100 |
| Auth | /api/auth/login, /api/auth/register |
10 minutes | 10 |
When rate limited, the response is:
{
"success": false,
"message": "Too many requests. Please try again later."
}Standard rate limit headers are included:
RateLimit-LimitRateLimit-RemainingRateLimit-Reset
Check if the API is running.
Authentication: None
Response (200):
{
"success": true,
"message": "Finance Dashboard API is running",
"version": "1.0.0"
}Register a new user account.
Authentication: None
Rate Limit: Auth limiter (10 req / 10 min)
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
name |
String | Yes | 2–50 chars, trimmed | User's full name |
email |
String | Yes | Valid email format | User's email address |
password |
String | Yes | Min 6 chars | Account password |
role |
String | No | viewer or analyst only |
Desired role (defaults to viewer) |
Note: Setting
role: "admin"is silently ignored. The role will default toviewer.
{
"success": true,
"message": "User registered successfully",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"data": {
"_id": "660a1b2c3d4e5f6a7b8c9d0e",
"name": "John Doe",
"email": "john@example.com",
"role": "viewer",
"status": "active",
"createdAt": "2026-04-01T12:00:00.000Z",
"updatedAt": "2026-04-01T12:00:00.000Z"
}
}| Status | Message | Cause |
|---|---|---|
400 |
Name, email, and password are required |
Missing required fields |
400 |
Name, email, and password must be strings |
Invalid field types |
400 |
Name must be at least 2 characters |
Name too short or whitespace-only |
400 |
Name cannot exceed 50 characters |
Name too long |
400 |
Invalid email format |
Malformed email |
400 |
Password must be at least 6 characters long |
Weak password |
400 |
User already exists with this email |
Duplicate email |
Authenticate a user and receive a JWT token.
Authentication: None
Rate Limit: Auth limiter (10 req / 10 min)
| Field | Type | Required | Description |
|---|---|---|---|
email |
String | Yes | Registered email address |
password |
String | Yes | Account password |
{
"success": true,
"message": "Login successful",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"data": {
"_id": "660a1b2c3d4e5f6a7b8c9d0e",
"name": "John Doe",
"email": "john@example.com",
"role": "viewer",
"status": "active",
"lastLogin": "2026-04-03T16:00:00.000Z",
"createdAt": "2026-04-01T12:00:00.000Z",
"updatedAt": "2026-04-03T16:00:00.000Z"
}
}| Status | Message | Cause |
|---|---|---|
401 |
Email and password are required |
Missing fields |
401 |
Email and password must be strings |
Invalid types |
401 |
Invalid email format |
Malformed email |
401 |
Invalid email or password |
Wrong email or password |
401 |
Your account is inactive. Contact admin. |
Account deactivated |
Security: The same error message
Invalid email or passwordis returned whether the email doesn't exist or the password is wrong. This prevents user enumeration.
Get the currently authenticated user's profile.
Authentication: Required (Bearer Token)
{
"success": true,
"data": {
"_id": "660a1b2c3d4e5f6a7b8c9d0e",
"name": "John Doe",
"email": "john@example.com",
"role": "viewer",
"status": "active",
"lastLogin": "2026-04-03T16:00:00.000Z",
"createdAt": "2026-04-01T12:00:00.000Z",
"updatedAt": "2026-04-03T16:00:00.000Z"
}
}| Status | Message | Cause |
|---|---|---|
401 |
Access denied. No token provided. |
No Authorization header |
401 |
Invalid token. |
Malformed token |
401 |
Token expired. Please login again. |
Expired JWT |
404 |
User not found |
User deleted or doesn't exist |
List financial records for the authenticated user with optional filtering, search, and pagination.
Authentication: Required (Bearer Token)
Roles: All (viewer, analyst, admin)
| Parameter | Type | Default | Description |
|---|---|---|---|
page |
Number | 1 |
Page number (min: 1) |
limit |
Number | 10 |
Records per page (min: 1, max: 100) |
type |
String | — | Filter by record type: income or expense |
category |
String | — | Filter by exact category (case-insensitive) |
startDate |
String (ISO) | — | Filter records on or after this date |
endDate |
String (ISO) | — | Filter records on or before this date |
search |
String | — | Search in note and category fields (max 100 chars) |
sort |
String | -date |
Sort field. Prefix with - for descending. Allowed: date, amount, type, category, createdAt |
GET /api/records?type=income&page=1&limit=5
GET /api/records?category=salary&startDate=2026-01-01&endDate=2026-03-31
GET /api/records?search=rent&sort=-amount
{
"success": true,
"total": 10,
"page": 1,
"pages": 2,
"count": 5,
"data": [
{
"_id": "660a1b2c3d4e5f6a7b8c9d0e",
"user": "660a0a1b2c3d4e5f6a7b8c9d",
"amount": 5000,
"type": "income",
"category": "salary",
"date": "2026-03-15T00:00:00.000Z",
"note": "March salary",
"createdAt": "2026-03-15T12:00:00.000Z",
"updatedAt": "2026-03-15T12:00:00.000Z"
}
]
}Create a new financial record.
Authentication: Required (Bearer Token)
Roles: admin only
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
amount |
Number | Yes | ≥ 0 | Transaction amount |
type |
String | Yes | income or expense |
Record type |
category |
String | Yes | Non-empty, trimmed | Category label |
date |
String (ISO) | Yes | Valid date | Transaction date |
note |
String | No | Max 200 chars | Optional description |
{
"amount": 2500,
"type": "income",
"category": "Freelance",
"date": "2026-04-10T00:00:00Z",
"note": "Web design contract"
}{
"success": true,
"message": "Record created successfully",
"data": {
"_id": "660b2c3d4e5f6a7b8c9d0e1f",
"user": "660a0a1b2c3d4e5f6a7b8c9d",
"amount": 2500,
"type": "income",
"category": "freelance",
"date": "2026-04-10T00:00:00.000Z",
"note": "Web design contract",
"createdAt": "2026-04-03T16:00:00.000Z",
"updatedAt": "2026-04-03T16:00:00.000Z"
}
}Note: The
categoryis automatically converted to lowercase.
| Status | Message | Cause |
|---|---|---|
400 |
Amount is required |
Amount not provided |
400 |
Type, category, and date are required |
Missing required fields |
400 |
Amount must be a valid number |
Non-numeric amount |
400 |
Amount cannot be negative |
Negative amount |
400 |
Invalid record type. Must be 'income' or 'expense' |
Invalid type value |
400 |
Category must be a non-empty string |
Empty category |
400 |
Invalid date format |
Unparseable date |
400 |
Note must be a string |
Non-string note |
400 |
Note cannot exceed 200 characters |
Note too long |
403 |
Access denied. Role '...' is not permitted. |
Non-admin role |
Update an existing financial record.
Authentication: Required (Bearer Token)
Roles: admin only
| Parameter | Type | Description |
|---|---|---|
id |
String | MongoDB ObjectId of the record |
All fields are optional. Only provided fields will be updated.
| Field | Type | Constraints | Description |
|---|---|---|---|
amount |
Number | ≥ 0 | Updated amount |
type |
String | income or expense |
Updated type |
category |
String | Non-empty | Updated category |
date |
String (ISO) | Valid date | Updated date |
note |
String | Max 200 chars | Updated note |
{
"amount": 3000,
"note": "Updated payment amount"
}{
"success": true,
"message": "Record updated successfully",
"data": {
"_id": "660b2c3d4e5f6a7b8c9d0e1f",
"amount": 3000,
"note": "Updated payment amount"
}
}| Status | Message | Cause |
|---|---|---|
400 |
Invalid record ID |
Malformed ObjectId |
400 |
Record not found or unauthorized |
Record doesn't exist or belongs to another user |
400 |
Amount must be a valid number |
Non-numeric amount |
400 |
Amount cannot be negative |
Negative amount |
400 |
Invalid type. Must be 'income' or 'expense' |
Invalid type |
400 |
Category must be a non-empty string |
Empty category |
400 |
Invalid date format |
Unparseable date |
400 |
Note cannot exceed 200 characters |
Note too long |
Soft delete a financial record. The record is marked as deleted but not removed from the database.
Authentication: Required (Bearer Token)
Roles: admin only
| Parameter | Type | Description |
|---|---|---|
id |
String | MongoDB ObjectId of the record |
{
"success": true,
"message": "Record deleted successfully"
}| Status | Message | Cause |
|---|---|---|
400 |
Invalid record ID |
Malformed ObjectId |
400 |
Record not found or unauthorized |
Record doesn't exist, already deleted, or belongs to another user |
Get aggregated dashboard analytics for the authenticated user.
Authentication: Required (Bearer Token)
Roles: analyst, admin
{
"success": true,
"data": {
"totals": {
"income": 15800,
"expense": 4300,
"balance": 11500
},
"categoryBreakdown": [
{
"_id": {
"category": "salary",
"type": "income"
},
"total": 15000
},
{
"_id": {
"category": "rent",
"type": "expense"
},
"total": 2400
},
{
"_id": {
"category": "freelance",
"type": "income"
},
"total": 800
}
],
"monthlyTrends": [
{
"_id": {
"year": 2026,
"month": 1,
"type": "income"
},
"total": 5000
},
{
"_id": {
"year": 2026,
"month": 1,
"type": "expense"
},
"total": 1500
}
],
"recentTransactions": [
{
"_id": "660b2c3d4e5f6a7b8c9d0e1f",
"amount": 5000,
"type": "income",
"category": "salary",
"date": "2026-03-15T00:00:00.000Z",
"note": "March salary"
}
]
}
}| Field | Type | Description |
|---|---|---|
totals.income |
Number | Sum of all income records |
totals.expense |
Number | Sum of all expense records |
totals.balance |
Number | income - expense |
categoryBreakdown |
Array | Totals grouped by category and type, sorted by amount descending |
monthlyTrends |
Array | Totals grouped by year, month, and type, sorted chronologically |
recentTransactions |
Array | Last 5 transactions sorted by date descending |
| Status | Message | Cause |
|---|---|---|
403 |
Access denied. Role 'viewer' is not permitted. |
Viewer role |
500 |
Failed to fetch dashboard data |
Server error |
List all users with optional filtering, search, and pagination.
Authentication: Required (Bearer Token)
Roles: admin only
| Parameter | Type | Default | Description |
|---|---|---|---|
page |
Number | 1 |
Page number (min: 1) |
limit |
Number | 10 |
Users per page (min: 1, max: 100) |
role |
String | — | Filter by role: viewer, analyst, or admin |
status |
String | — | Filter by status: active or inactive |
search |
String | — | Search in name and email fields |
GET /api/users?role=viewer&status=active
GET /api/users?search=john&page=1&limit=5
{
"success": true,
"total": 4,
"page": 1,
"pages": 1,
"count": 4,
"data": [
{
"_id": "660a0a1b2c3d4e5f6a7b8c9d",
"name": "Admin User",
"email": "admin@zorvyn.com",
"role": "admin",
"status": "active",
"createdAt": "2026-04-01T12:00:00.000Z",
"updatedAt": "2026-04-03T16:00:00.000Z"
}
]
}Get a single user by ID.
Authentication: Required (Bearer Token)
Roles: admin only
| Parameter | Type | Description |
|---|---|---|
id |
String | MongoDB ObjectId of the user |
{
"success": true,
"data": {
"_id": "660a0a1b2c3d4e5f6a7b8c9d",
"name": "Admin User",
"email": "admin@zorvyn.com",
"role": "admin",
"status": "active",
"lastLogin": "2026-04-03T16:00:00.000Z",
"createdAt": "2026-04-01T12:00:00.000Z",
"updatedAt": "2026-04-03T16:00:00.000Z"
}
}| Status | Message | Cause |
|---|---|---|
500 |
Invalid user ID |
Malformed ObjectId |
500 |
User not found |
User doesn't exist or is soft-deleted |
Update a user's role and/or status.
Authentication: Required (Bearer Token)
Roles: admin only
| Parameter | Type | Description |
|---|---|---|
id |
String | MongoDB ObjectId of the user to update |
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
role |
String | No | viewer, analyst, or admin |
New role |
status |
String | No | active or inactive |
New status |
- Self-modification blocked: Admins cannot change their own role or status
- Both fields are optional — omitted fields are not changed
{
"role": "analyst",
"status": "active"
}{
"success": true,
"message": "User updated successfully",
"data": {
"_id": "660a1b2c3d4e5f6a7b8c9d0e",
"name": "Viewer User",
"email": "viewer@zorvyn.com",
"role": "analyst",
"status": "active"
}
}| Status | Message | Cause |
|---|---|---|
500 |
Invalid user ID |
Malformed ObjectId |
500 |
User not found |
User doesn't exist |
500 |
You cannot modify your own role/status |
Self-modification attempt |
500 |
Invalid role. Must be 'viewer', 'analyst', or 'admin' |
Invalid role value |
500 |
Invalid status. Must be 'active' or 'inactive' |
Invalid status value |
Soft delete a user. The user is marked as deleted and set to inactive.
Authentication: Required (Bearer Token)
Roles: admin only
| Parameter | Type | Description |
|---|---|---|
id |
String | MongoDB ObjectId of the user to delete |
- Self-delete blocked: Admins cannot delete themselves
- Last admin protected: The last active admin user cannot be deleted
- Soft delete sets
isDeleted: trueandstatus: "inactive"
{
"success": true,
"message": "User deleted successfully"
}| Status | Message | Cause |
|---|---|---|
500 |
Invalid user ID |
Malformed ObjectId |
500 |
User not found |
User doesn't exist or already deleted |
500 |
You cannot delete your own account |
Self-delete attempt |
500 |
Cannot delete the last active admin. Promote another user to admin first. |
Last admin protection |
{
"_id": "660a0a1b2c3d4e5f6a7b8c9d",
"name": "John Doe",
"email": "john@example.com",
"role": "viewer",
"status": "active",
"lastLogin": "2026-04-03T16:00:00.000Z",
"createdAt": "2026-04-01T12:00:00.000Z",
"updatedAt": "2026-04-03T16:00:00.000Z"
}Note:
passwordandisDeletedare never included in responses.
{
"_id": "660b2c3d4e5f6a7b8c9d0e1f",
"user": "660a0a1b2c3d4e5f6a7b8c9d",
"amount": 2500,
"type": "income",
"category": "freelance",
"date": "2026-04-10T00:00:00.000Z",
"note": "Web design contract",
"createdAt": "2026-04-03T16:00:00.000Z",
"updatedAt": "2026-04-03T16:00:00.000Z"
}Note:
isDeletedis never included in responses.
1. User registers: POST /api/auth/register → receives JWT token
2. User logs in: POST /api/auth/login → receives JWT token
3. User includes token in all subsequent requests:
Authorization: Bearer <token>
4. Protected routes verify the token and check:
- Token validity and expiry
- User exists in database
- User is not soft-deleted
- User account is active
- User role matches required role(s)
When a resource is "deleted":
- The
isDeletedfield is set totrue - For users,
statusis also set to"inactive" - The resource no longer appears in list queries
- The resource cannot be fetched by ID
- The data remains in the database for audit/recovery purposes
- A soft-deleted user cannot log in (filtered out during login lookup)