SmartSpend API Documentation
PHP + MySQL backend for the SmartSpend family expense app.
Every endpoint below shows exactly what you send and what you get back.
Running on LIVE SERVER - connected to database "czsoft_smartspend". The API is ready to use.
Setup and base URL
This copy is running on the live server, so there is only one address.
It works from the browser, from Postman, from the emulator and from a real
phone anywhere in the world.
| Calling from | Base URL |
| Everywhere |
https://www.smartspend.creamerzsoft.com/ |
| Put this in the Android app |
https://www.smartspend.creamerzsoft.com/api/ |
Test accounts
| Role | Email | Password |
| Admin | admin@smartspend.com | admin123 |
| Member | ayesha@smartspend.com | member123 |
| Member | hamza@smartspend.com | member123 |
| Member | sara@smartspend.com | member123 |
Sending the token
Call Login first. It returns a token.
Save it in the app, then send it as a header on every endpoint marked
Login required or Admin only:
While testing you can also put it in the URL instead: ?token=d449b6b9...
Every response has the same shape
So in the app you always check success first, show
message to the user, and read the real content from
data.
When it works
{
"success": true,
"message": "Login successful",
"data": {
"...": "..."
}
}
When it fails
{
"success": false,
"message": "Incorrect email or password",
"errors": {
"email": "Invalid email"
}
}
Status codes used
| Code | Meaning |
| 200 | OK |
| 201 | Created - signup, add expense, add member |
| 400 | Bad request |
| 401 | Not logged in, or wrong password |
| 403 | Logged in, but not allowed (not the admin) |
| 404 | Not found |
| 405 | Wrong method - you sent GET to a POST endpoint |
| 409 | Email already registered |
| 422 | Validation failed - check the errors object |
| 500 | Server or database error |
Authentication
Start here. Login gives you a token - every other endpoint needs it.
POST
Sign up
No login needed
api/auth/signup.php
Creates a NEW household and makes this person its ADMIN. Use this on the Register screen. Other family members are added later by the admin using Add Member.
What you send
Headers
Fields in the JSON body
| Field | Type | Required | What it means |
| name |
string |
required |
Full name of the person |
| email |
string |
required |
Must be a valid email and not already registered |
| password |
string |
required |
At least 6 characters |
| householdName |
string |
optional |
Family name. If you leave it out we use "<name>'s Family" |
| monthlyBudget |
number |
optional |
Budget for the whole family. Default 0 |
Example request body
{
"name": "Abdul Rehman",
"email": "abdul@example.com",
"password": "secret123",
"householdName": "Khan Family",
"monthlyBudget": 150000
}
What you get back
HTTP 201 · Content-Type: application/json
{
"success": true,
"message": "Account created successfully",
"data": {
"token": "7f3a9c2e1b4d...64 characters",
"user": {
"id": 1,
"householdId": 1,
"name": "Abdul Rehman",
"email": "abdul@example.com",
"role": "ADMIN",
"assignedBudget": 150000
}
}
}
If something goes wrong
| Code | message you get back |
| 422 |
Please fill all required fields |
| 422 |
Please enter a valid email address |
| 422 |
Password must be at least 6 characters |
| 422 |
A household can only have one admin, and it is the person who created it |
| 409 |
This email is already registered |
POST
Login
No login needed
api/auth/login.php
Checks the email and password. Save the token you get back - you must send it on every protected request.
What you send
Headers
Fields in the JSON body
| Field | Type | Required | What it means |
| email |
string |
required |
The registered email address |
| password |
string |
required |
The account password |
Example request body
{
"email": "admin@smartspend.com",
"password": "admin123"
}
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Login successful",
"data": {
"token": "d449b6b99cefda157444058487d862d6...",
"user": {
"id": 1,
"householdId": 1,
"name": "Abdul Rehman",
"email": "admin@smartspend.com",
"role": "ADMIN",
"assignedBudget": 50000
}
}
}
If something goes wrong
| Code | message you get back |
| 401 |
Incorrect email or password |
| 422 |
Please fill all required fields |
GET
Who am I
Login required
api/auth/me.php
Call this when the app opens to check the saved token is still valid. Also fills the profile screen.
What you send
Headers
Fields
Nothing to send - just the headers above.
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "User loaded",
"data": {
"user": {
"id": 1,
"householdId": 1,
"name": "Abdul Rehman",
"email": "admin@smartspend.com",
"role": "ADMIN",
"assignedBudget": 50000
},
"householdName": "Khan Family",
"totalSpent": 27700,
"remaining": 22300
}
}
If something goes wrong
| Code | message you get back |
| 401 |
Please login first (missing or expired token) |
POST
Forgot password
No login needed
api/auth/forgot_password.php
Creates a 6-digit code that works for 15 minutes. While DEBUG_MODE is true the code comes back in the response so you can test without an email server. The message is always the same whether the email exists or not, so nobody can use this to find out which emails are registered.
What you send
Headers
Fields in the JSON body
| Field | Type | Required | What it means |
| email |
string |
required |
The email to send the reset code to |
Example request body
{
"email": "admin@smartspend.com"
}
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "If that email is registered, a reset code has been sent",
"data": {
"code": "874545",
"note": "Shown only because DEBUG_MODE is true"
}
}
If something goes wrong
| Code | message you get back |
| 422 |
Please enter a valid email address |
POST
Reset password
No login needed
api/auth/reset_password.php
Sets a new password using the 6-digit code. The code can only be used once. All old tokens are deleted, so the user must login again.
What you send
Headers
Fields in the JSON body
| Field | Type | Required | What it means |
| email |
string |
required |
Same email used in Forgot password |
| code |
string |
required |
The 6-digit code |
| newPassword |
string |
required |
At least 6 characters |
Example request body
{
"email": "admin@smartspend.com",
"code": "874545",
"newPassword": "newpass123"
}
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Password changed successfully. Please login again.",
"data": null
}
If something goes wrong
| Code | message you get back |
| 400 |
This code is wrong or has expired |
| 422 |
Password must be at least 6 characters |
POST
Send a test email
Login required
api/auth/test_email.php
Sends a test message to the address on YOUR OWN account and reports exactly what the mail server said. Use this when email "is not working" - forgot_password.php has to stay vague on purpose, this one does not. It always mails the logged-in account's own address, never one you pass in, so it cannot be used to send mail to strangers. To test a different inbox, change your profile email first. Nothing is saved and no reset code is created.
What you send
Headers
Fields
Nothing to send - just the headers above.
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Test email sent to you@example.com",
"data": {
"sentTo": "you@example.com",
"tookSeconds": 3.62999999999999989341858963598497211933135986328125,
"smtp": {
"host": "smtp.gmail.com",
"port": 587,
"username": "smartspend82@gmail.com",
"encryption": "tls",
"fromName": "SmartSpend",
"fromAddress": "smartspend82@gmail.com",
"passwordSet": true
},
"note": "Check that inbox - and the spam folder. Nothing was saved."
}
}
If something goes wrong
| Code | message you get back |
| 503 |
Email is not set up on this server (fill in the smtp_* values in config/secrets.php) |
| 502 |
The mail server refused the message - the reply includes the error and a hint |
| 401 |
Please login first (missing or expired token) |
POST
Change password
Login required
api/auth/change_password.php
For the settings screen, when the user is already logged in and knows their current password.
What you send
Headers
Fields in the JSON body
| Field | Type | Required | What it means |
| currentPassword |
string |
required |
The password they use right now |
| newPassword |
string |
required |
At least 6 characters |
Example request body
{
"currentPassword": "admin123",
"newPassword": "admin456"
}
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Password updated successfully",
"data": null
}
If something goes wrong
| Code | message you get back |
| 401 |
Your current password is incorrect |
| 422 |
New password must be at least 6 characters |
POST
Logout
Login required
api/auth/logout.php
Deletes the current token from the database so it can never be used again. The app should also delete its saved copy. Send deviceToken as well and this phone stops receiving push notifications - otherwise the next person to use it keeps getting this family's messages.
What you send
Headers
Fields in the JSON body
| Field | Type | Required | What it means |
| deviceToken |
string |
optional |
The Firebase token of this phone, so push is switched off for it too |
Example request body
{
"deviceToken": "fMEP0vJ9S...long token from Firebase"
}
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Logged out successfully",
"data": null
}
If something goes wrong
| Code | message you get back |
| 401 |
Please login first (missing or expired token) |
POST
Delete account
Login required
api/auth/delete_account.php
Removes the logged-in user for good, along with their expenses and receipt photos. The password is asked for again because this cannot be undone. If you are the LAST person in the household, the whole household goes with you. If you are the only admin and other members remain, you must nominate a successor with transferTo - they become the admin first.
What you send
Headers
Fields in the JSON body
| Field | Type | Required | What it means |
| password |
string |
required |
Your own password, typed again to confirm |
| transferTo |
int |
optional |
Id of the member who should become admin. Only needed when you are the last admin and other members remain |
Example request body
{
"password": "secret123",
"transferTo": 2
}
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Your account has been deleted",
"data": null
}
If something goes wrong
| Code | message you get back |
| 401 |
That password is not correct |
| 400 |
You are the only admin. Choose which member should take over before deleting your account. |
| 422 |
Please fill all required fields |
Dashboard and Profile
One call fills the whole home screen - no need to call members and expenses separately.
GET
Dashboard
Login required
api/dashboard.php
Everything the home screen needs in ONE request: the logged-in user, the family budget, how much was spent this month, every member with their own numbers, the 5 newest expenses, spending grouped by category for the chart, and the unread count for the bell icon.
What you send
Headers
Fields
Nothing to send - just the headers above.
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Dashboard loaded",
"data": {
"user": {
"id": 1,
"householdId": 1,
"name": "Abdul Rehman",
"email": "admin@smartspend.com",
"role": "ADMIN",
"assignedBudget": 50000
},
"household": {
"id": 1,
"name": "Khan Family",
"monthlyBudget": 150000,
"totalSpent": 63850,
"remaining": 86150,
"usedPercent": 42.60000000000000142108547152020037174224853515625,
"expenseCount": 11,
"monthStart": "2026-08-01",
"monthLabel": "August 2026"
},
"mySpending": {
"assignedBudget": 50000,
"totalSpent": 27700,
"remaining": 22300
},
"members": [
{
"id": 1,
"name": "Abdul Rehman",
"email": "admin@smartspend.com",
"role": "ADMIN",
"assignedBudget": 50000,
"totalSpent": 27700,
"remaining": 22300,
"usedPercent": 55.39999999999999857891452847979962825775146484375,
"lastActive": "Just now"
},
{
"id": 2,
"name": "Ayesha Khan",
"email": "ayesha@smartspend.com",
"role": "MEMBER",
"assignedBudget": 30000,
"totalSpent": 17500,
"remaining": 12500,
"usedPercent": 58.2999999999999971578290569595992565155029296875,
"lastActive": "2 hours ago"
}
],
"recentExpenses": [
{
"id": 11,
"title": "Bus Card Top-up",
"amount": 1200,
"category": "Transport",
"date": "2026-08-12",
"memberId": 4,
"memberName": "Sara Khan"
}
],
"spendingByCategory": [
{
"category": "Groceries",
"amount": 21900,
"percent": 34.2999999999999971578290569595992565155029296875
},
{
"category": "Education",
"amount": 14750,
"percent": 23.10000000000000142108547152020037174224853515625
}
],
"unreadCount": 2
}
}
If something goes wrong
| Code | message you get back |
| 401 |
Please login first (missing or expired token) |
POST
Edit my profile
Login required
api/profile.php
Lets a user change their OWN name or email. Send only the field you want to change - anything you leave out keeps its old value.
What you send
Headers
Fields in the JSON body
| Field | Type | Required | What it means |
| name |
string |
optional |
New display name |
| email |
string |
optional |
New email. Must not belong to someone else |
Example request body
{
"name": "Abdul Rehman Khan"
}
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Profile updated successfully",
"data": {
"user": {
"id": 1,
"householdId": 1,
"name": "Abdul Rehman Khan",
"email": "admin@smartspend.com",
"role": "ADMIN",
"assignedBudget": 50000
}
}
}
If something goes wrong
| Code | message you get back |
| 422 |
Please enter a valid email address |
| 409 |
This email is already used by someone else |
Expenses
By default every list shows only the CURRENT month. Add allMonths=1 to see everything.
GET
List expenses
Login required
api/expenses/list.php?category=Bills&page=1&limit=20
The main expense list. Every filter below is optional and they can be combined, for example ?memberId=3&category=Bills&page=2
What you send
Headers
Values added to the URL
| Field | Type | Required | What it means |
| memberId |
int |
optional |
Only this member's expenses |
| category |
string |
optional |
Groceries, Bills, Transport, Food, Health, Education, Shopping, Rent, Other |
| search |
string |
optional |
Search inside the expense title |
| from |
date |
optional |
Start date, format 2026-08-01 |
| to |
date |
optional |
End date, format 2026-08-31 |
| allMonths |
0 or 1 |
optional |
Send 1 to ignore the current-month filter |
| page |
int |
optional |
Page number. Default 1 |
| limit |
int |
optional |
Rows per page. Default 50, maximum 100 |
Example request
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Expenses loaded",
"data": {
"expenses": [
{
"id": 12,
"title": "Internet Bill",
"amount": 4500,
"category": "Bills",
"date": "2026-08-18",
"memberId": 1,
"memberName": "Abdul Rehman"
},
{
"id": 7,
"title": "Mobile Load",
"amount": 1500,
"category": "Bills",
"date": "2026-08-08",
"memberId": 3,
"memberName": "Hamza Khan"
}
],
"totalCount": 3,
"totalSpent": 15200,
"page": 1,
"limit": 20,
"totalPages": 1,
"categories": [
"Groceries",
"Bills",
"Transport",
"Food",
"Health",
"Education",
"Shopping",
"Rent",
"Other"
]
}
}
If something goes wrong
| Code | message you get back |
| 401 |
Please login first (missing or expired token) |
GET
Get one expense
Login required
api/expenses/get.php?id=12
A single expense, in the same shape the lists use. The edit screen loads this before it fills in the form. canEdit tells you whether THIS user is allowed to change it (their own expense, or any expense if they are the admin), so the app can grey the controls out instead of letting somebody fill in a form the server will refuse.
What you send
Headers
Values added to the URL
| Field | Type | Required | What it means |
| id |
int |
required |
The expense id |
Example request
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Expense loaded",
"data": {
"expense": {
"id": 12,
"title": "Internet Bill",
"amount": 4500,
"category": "Bills",
"date": "2026-08-18",
"memberId": 1,
"memberName": "Abdul Rehman",
"imageUrl": null
},
"canEdit": true
}
}
If something goes wrong
| Code | message you get back |
| 404 |
Expense not found |
POST
Add expense
Login required
api/expenses/add.php
Saves a new expense. It also creates a notification for the family, and if the member goes past their assigned budget it creates a Budget Exceeded warning too. The response tells you overBudget so the app can show a message straight away.
What you send
Headers
Fields in the JSON body
| Field | Type | Required | What it means |
| title |
string |
required |
What the money was spent on |
| amount |
number |
required |
Must be greater than 0 |
| category |
string |
optional |
One of the 9 categories. Anything else becomes "Other" |
| date |
date |
optional |
Format 2026-08-18. Defaults to today |
| image |
file |
optional |
Photo of the receipt. Send the request as multipart/form-data. JPG, PNG or WebP, up to 5 MB. The saved expense comes back with imageUrl filled in |
| (reply) |
note |
optional |
The reply carries outsideCurrentMonth. It is true when the date you sent is BEFORE the household month_start_date - the expense is saved, but every "this month" total filters it out, so the dashboard will not show it. monthStart comes back too so you can word the message. Show it, or the save looks like it did nothing. |
| memberId |
int |
optional |
Who the expense belongs to. Defaults to you. Only an ADMIN may put someone else's id here |
Example request body
{
"title": "Internet Bill",
"amount": 4500,
"category": "Bills",
"date": "2026-08-18"
}
What you get back
HTTP 201 · Content-Type: application/json
{
"success": true,
"message": "Expense added successfully",
"data": {
"expense": {
"id": 12,
"title": "Internet Bill",
"amount": 4500,
"category": "Bills",
"date": "2026-08-18",
"memberId": 1,
"memberName": "Abdul Rehman"
},
"memberSpent": 32200,
"memberBudget": 50000,
"overBudget": false
}
}
If something goes wrong
| Code | message you get back |
| 422 |
Please fill all required fields |
| 422 |
Amount must be greater than 0 |
| 422 |
Date must look like 2026-08-19 |
| 403 |
You can only add expenses for yourself |
| 404 |
That member is not part of your household |
POST
Edit expense
Login required
api/expenses/update.php
Changes an existing expense. Send only the fields you want to change. A normal member can only edit their own expenses; an ADMIN can edit any expense in the household.
What you send
Headers
Fields in the JSON body
| Field | Type | Required | What it means |
| id |
int |
required |
Which expense to edit |
| title |
string |
optional |
New title |
| amount |
number |
optional |
New amount, greater than 0 |
| category |
string |
optional |
New category |
| date |
date |
optional |
New date, format 2026-08-18 |
| image |
file |
optional |
A new receipt photo (multipart/form-data). Replaces the old one, which is deleted from the server |
| removeImage |
0 or 1 |
optional |
Send 1 to delete the receipt photo without adding a new one |
Example request body
{
"id": 12,
"title": "Internet Bill (August)",
"amount": 5000
}
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Expense updated successfully",
"data": {
"expense": {
"id": 12,
"title": "Internet Bill (August)",
"amount": 5000,
"category": "Bills",
"date": "2026-08-18",
"memberId": 1,
"memberName": "Abdul Rehman"
}
}
}
If something goes wrong
| Code | message you get back |
| 404 |
Expense not found |
| 403 |
You can only edit your own expenses |
| 422 |
Amount must be greater than 0 |
POST
Delete expense
Login required
api/expenses/delete.php
Removes one expense. A normal member can only delete their own; an ADMIN can delete any.
What you send
Headers
Fields in the JSON body
| Field | Type | Required | What it means |
| id |
int |
required |
Which expense to delete |
Example request body
{
"id": 12
}
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Expense deleted successfully",
"data": null
}
If something goes wrong
| Code | message you get back |
| 404 |
Expense not found |
| 403 |
You can only delete your own expenses |
GET
Category list
No login needed
api/expenses/categories.php
The 9 categories for the dropdown on the Add Expense screen. No login needed, so the app can load it early.
What you send
Headers
Fields
Nothing to send - just the headers above.
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Categories loaded",
"data": {
"categories": [
"Groceries",
"Bills",
"Transport",
"Food",
"Health",
"Education",
"Shopping",
"Rent",
"Other"
]
}
}
Family Members
Anyone logged in can VIEW members. Only the ADMIN can add, edit or remove them.
GET
List members
Login required
api/members/list.php
Every member of the household with their assigned budget, how much they spent this month, what is left, the percentage used and when they were last active. The admin is always first.
What you send
Headers
Fields
Nothing to send - just the headers above.
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Members loaded",
"data": {
"members": [
{
"id": 1,
"name": "Abdul Rehman",
"email": "admin@smartspend.com",
"role": "ADMIN",
"assignedBudget": 50000,
"totalSpent": 27700,
"remaining": 22300,
"usedPercent": 55.39999999999999857891452847979962825775146484375,
"lastActive": "Just now"
},
{
"id": 3,
"name": "Hamza Khan",
"email": "hamza@smartspend.com",
"role": "MEMBER",
"assignedBudget": 25000,
"totalSpent": 12800,
"remaining": 12200,
"usedPercent": 51.2000000000000028421709430404007434844970703125,
"lastActive": "1 day ago"
}
],
"totalMembers": 4
}
}
If something goes wrong
| Code | message you get back |
| 401 |
Please login first (missing or expired token) |
GET
Member detail
Login required
api/members/get.php?id=3
One member plus all of their expenses this month and their spending grouped by category. Use this for the Member Detail screen.
What you send
Headers
Values added to the URL
| Field | Type | Required | What it means |
| id |
int |
required |
Which member to load |
Example request
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Member loaded",
"data": {
"member": {
"id": 3,
"name": "Hamza Khan",
"email": "hamza@smartspend.com",
"role": "MEMBER",
"assignedBudget": 25000,
"totalSpent": 12800,
"remaining": 12200,
"usedPercent": 51.2000000000000028421709430404007434844970703125,
"lastActive": "1 day ago"
},
"expenses": [
{
"id": 8,
"title": "Cricket Kit",
"amount": 4800,
"category": "Shopping",
"date": "2026-08-11",
"memberId": 3,
"memberName": "Hamza Khan"
}
],
"expenseCount": 3,
"spendingByCategory": [
{
"category": "Transport",
"amount": 6500
},
{
"category": "Shopping",
"amount": 4800
},
{
"category": "Bills",
"amount": 1500
}
]
}
}
If something goes wrong
| Code | message you get back |
| 422 |
Please fill all required fields |
| 404 |
Member not found |
GET
Member expenses only
Login required
api/members/expenses.php?id=3
A lighter version of Member detail - just the expense list and the total. Useful for a "see all" screen.
What you send
Headers
Values added to the URL
| Field | Type | Required | What it means |
| id |
int |
required |
Which member |
| allMonths |
0 or 1 |
optional |
Send 1 to include older months as well |
Example request
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Member expenses loaded",
"data": {
"expenses": [
{
"id": 6,
"title": "Petrol",
"amount": 6500,
"category": "Transport",
"date": "2026-08-04",
"memberId": 3,
"memberName": "Hamza Khan"
}
],
"totalSpent": 12800,
"count": 3
}
}
If something goes wrong
| Code | message you get back |
| 404 |
Member not found |
POST
Add member
Admin only
api/members/add.php
The admin creates a login for a family member. The member can then login with the email and password given here. New members are always MEMBER: A household has exactly one admin - whoever created it at signup - so role is not accepted here. The only handover is auth/delete_account.php, where an admin deleting their own account has to name a successor first.
What you send
Headers
Fields in the JSON body
| Field | Type | Required | What it means |
| name |
string |
required |
Full name |
| email |
string |
required |
Must be valid and not already registered |
| password |
string |
required |
At least 6 characters. Give it to the member |
| assignedBudget |
number |
optional |
Their spending limit. Default 0 |
Example request body
{
"name": "Bilal Khan",
"email": "bilal@smartspend.com",
"password": "bilal123",
"assignedBudget": 15000
}
What you get back
HTTP 201 · Content-Type: application/json
{
"success": true,
"message": "Member added successfully",
"data": {
"member": {
"id": 5,
"name": "Bilal Khan",
"email": "bilal@smartspend.com",
"role": "MEMBER",
"assignedBudget": 15000,
"totalSpent": 0,
"remaining": 15000,
"usedPercent": 0,
"lastActive": "Never"
}
}
}
If something goes wrong
| Code | message you get back |
| 403 |
Only the household admin can do this |
| 422 |
Please fill all required fields |
| 422 |
Password must be at least 6 characters |
| 409 |
This email is already registered |
POST
Edit member
Admin only
api/members/update.php
Changes a member's name, email or budget. Send only what you want to change. A household has exactly one admin - whoever created it at signup - so role is not accepted here. The only handover is auth/delete_account.php, where an admin deleting their own account has to name a successor first. Sending the role the member already has is fine, so posting a whole form back still works; only an actual change is refused. Lowering a budget below what has already been spent is ALLOWED - spent money cannot be un-spent, and the old figure may simply have been typed wrong - but the response says so: overBudget is true and shortfall is how far past the new limit the spending already is, so the screen can confirm before saving. The member gets a BUDGET_WARNING notification instead of the usual INFO one.
What you send
Headers
Fields in the JSON body
| Field | Type | Required | What it means |
| id |
int |
required |
Which member to edit |
| name |
string |
optional |
New name |
| email |
string |
optional |
New email |
| assignedBudget |
number |
optional |
New budget |
Example request body
{
"id": 2,
"name": "Ayesha K.",
"assignedBudget": 32000
}
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Member updated successfully",
"data": {
"member": {
"id": 2,
"name": "Ayesha K.",
"email": "ayesha@smartspend.com",
"role": "MEMBER",
"assignedBudget": 32000,
"totalSpent": 17500,
"remaining": 14500,
"usedPercent": 54.7000000000000028421709430404007434844970703125,
"lastActive": "2 hours ago"
},
"spent": 17500,
"overBudget": false,
"shortfall": 0
}
}
If something goes wrong
| Code | message you get back |
| 403 |
Only the household admin can do this |
| 404 |
Member not found |
| 409 |
This email is already used by someone else |
| 422 |
The household admin cannot be changed |
POST
Set member budget
Admin only
api/members/update_budget.php
The quick version of Edit member for the "Assign Budget" screen - it only changes the money. The response comes back with the member's fresh spending numbers so the screen can refresh immediately. Lowering a budget below what has already been spent is ALLOWED - spent money cannot be un-spent, and the old figure may simply have been typed wrong - but the response says so: overBudget is true and shortfall is how far past the new limit the spending already is, so the screen can confirm before saving. The member gets a BUDGET_WARNING notification instead of the usual INFO one.
What you send
Headers
Fields in the JSON body
| Field | Type | Required | What it means |
| id |
int |
required |
Which member |
| assignedBudget |
number |
required |
The new limit. Cannot be negative |
Example request body
{
"id": 3,
"assignedBudget": 26000
}
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Budget updated successfully",
"data": {
"member": {
"id": 3,
"name": "Hamza Khan",
"email": "hamza@smartspend.com",
"role": "MEMBER",
"assignedBudget": 26000,
"totalSpent": 12800,
"remaining": 13200,
"usedPercent": 49.2000000000000028421709430404007434844970703125,
"lastActive": "1 day ago"
},
"spent": 12800,
"overBudget": false,
"shortfall": 0
}
}
If something goes wrong
| Code | message you get back |
| 403 |
Only the household admin can do this |
| 422 |
Budget cannot be negative |
| 404 |
Member not found |
POST
Remove member
Admin only
api/members/delete.php
Deletes the member AND all of their expenses and tokens. The admin cannot delete themselves.
What you send
Headers
Fields in the JSON body
| Field | Type | Required | What it means |
| id |
int |
required |
Which member to remove |
Example request body
{
"id": 5
}
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Member removed successfully",
"data": null
}
If something goes wrong
| Code | message you get back |
| 403 |
Only the household admin can do this |
| 400 |
You cannot remove yourself |
| 404 |
Member not found |
POST
Reset a member password
Admin only
api/members/reset_password.php
The admin hands out the passwords when they add members, so sooner or later somebody forgets theirs. This sets a new one without going through the email code. The member is logged out of every device and told what happened, so it can never be done quietly behind their back. Use change_password.php for your own password.
What you send
Headers
Fields in the JSON body
| Field | Type | Required | What it means |
| id |
int |
required |
Which member |
| newPassword |
string |
required |
At least 6 characters |
Example request body
{
"id": 3,
"newPassword": "freshstart123"
}
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Password reset. Give the new password to Hamza Khan.",
"data": {
"member": {
"id": 3,
"name": "Hamza Khan",
"email": "hamza@smartspend.com"
}
}
}
If something goes wrong
| Code | message you get back |
| 422 |
Password must be at least 6 characters |
| 403 |
Only the household admin can do this |
| 404 |
Member not found |
| 400 |
Use change_password.php to change your own password |
Household
The household is the family itself - its name, its monthly budget and which month is running.
GET
Get household
Login required
api/household/get.php
The family name, monthly budget, how much is spent and left this month, and how many members there are.
What you send
Headers
Fields
Nothing to send - just the headers above.
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Household loaded",
"data": {
"household": {
"id": 1,
"name": "Khan Family",
"monthlyBudget": 150000,
"totalSpent": 63850,
"remaining": 86150,
"usedPercent": 42.60000000000000142108547152020037174224853515625,
"monthStart": "2026-08-01",
"monthLabel": "August 2026",
"memberCount": 4
}
}
}
If something goes wrong
| Code | message you get back |
| 401 |
Please login first (missing or expired token) |
POST
Edit household
Admin only
api/household/update.php
Changes the family name or the total monthly budget. Send only what you want to change. Lowering a budget below what has already been spent is ALLOWED - spent money cannot be un-spent, and the old figure may simply have been typed wrong - but the response says so: overBudget is true and shortfall is how far past the new limit the spending already is, so the screen can confirm before saving. The family gets a BUDGET_WARNING notification instead of the usual INFO one.
What you send
Headers
Fields in the JSON body
| Field | Type | Required | What it means |
| name |
string |
optional |
New family name |
| monthlyBudget |
number |
optional |
New total budget. Cannot be negative |
Example request body
{
"name": "Khan Family",
"monthlyBudget": 160000
}
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Household updated successfully",
"data": {
"household": {
"id": 1,
"name": "Khan Family",
"monthlyBudget": 160000
},
"spent": 92400,
"overBudget": false,
"shortfall": 0
}
}
If something goes wrong
| Code | message you get back |
| 403 |
Only the household admin can do this |
| 422 |
Budget cannot be negative |
POST
Start new month
Admin only
api/household/new_month.php
The Start New Month button. It does NOT delete anything - old expenses stay in the database so the report screen still works. It moves month_start_date to the FIRST of the current month, and every "this month" figure counts from there. It used to move it to TODAY, which meant pressing the button on the 27th made all 26 earlier days vanish from the dashboard and from every member total - nothing was deleted, but everything read zero and there was no undo. Pressing it twice in the same month is now a harmless no-op: monthChanged comes back false and only the budget is saved.
What you send
Headers
Fields in the JSON body
| Field | Type | Required | What it means |
| monthlyBudget |
number |
optional |
Set a different budget for the new month. Leave it out to keep the same one |
| startDate |
date |
optional |
Only if your household does not budget from the 1st - e.g. payday on the 25th. Format 2026-08-25. Cannot be in the future. Leave it out and the 1st of this month is used |
Example request body
{
"monthlyBudget": 160000
}
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "New month started successfully",
"data": {
"closedMonth": {
"from": "2026-08-01",
"to": "2026-09-01",
"totalSpent": 63850,
"expenseCount": 11
},
"newMonthStart": "2026-09-01",
"monthlyBudget": 160000,
"monthChanged": true
}
}
If something goes wrong
| Code | message you get back |
| 403 |
Only the household admin can do this |
| 422 |
Date must look like 2026-08-01 |
| 422 |
A month cannot start in the future - nothing would be counted until then. |
GET
Reports
Login required
api/household/report.php?months=6
Totals for the reports and charts screen: month by month, then per member, then per category. Unlike the other endpoints this one looks at ALL months, not just the current one.
What you send
Headers
Values added to the URL
| Field | Type | Required | What it means |
| months |
int |
optional |
How many months of history. Default 6, maximum 24 |
Example request
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Report loaded",
"data": {
"monthly": [
{
"month": "2026-08",
"label": "August 2026",
"totalSpent": 63850,
"expenseCount": 11
},
{
"month": "2026-07",
"label": "July 2026",
"totalSpent": 58200,
"expenseCount": 9
}
],
"byMember": [
{
"memberId": 1,
"name": "Abdul Rehman",
"role": "ADMIN",
"totalSpent": 32700,
"expenseCount": 3
},
{
"memberId": 2,
"name": "Ayesha Khan",
"role": "MEMBER",
"totalSpent": 17500,
"expenseCount": 3
}
],
"byCategory": [
{
"category": "Groceries",
"totalSpent": 21900,
"expenseCount": 2
},
{
"category": "Bills",
"totalSpent": 15700,
"expenseCount": 3
}
]
}
}
If something goes wrong
| Code | message you get back |
| 401 |
Please login first (missing or expired token) |
Notifications
The API creates these by itself when an expense is added, a member joins, a budget is passed or a new month starts.
GET
List notifications
Login required
api/notifications/list.php?limit=20
Messages for this user plus the ones sent to the whole family. Newest first. type is INFO, EXPENSE_ADDED or BUDGET_WARNING so the app can show a different icon for each.
What you send
Headers
Values added to the URL
| Field | Type | Required | What it means |
| limit |
int |
optional |
How many to return. Default 50, maximum 100 |
Example request
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Notifications loaded",
"data": {
"notifications": [
{
"id": 4,
"title": "New Expense Added",
"message": "Abdul Rehman added \"Internet Bill\" for Rs 4,500.00",
"type": "EXPENSE_ADDED",
"isRead": false,
"createdAt": "2026-08-19 01:52:14",
"timeAgo": "Just now"
},
{
"id": 2,
"title": "Budget Warning",
"message": "Hamza Khan has used more than 50% of the assigned budget.",
"type": "BUDGET_WARNING",
"isRead": true,
"createdAt": "2026-08-19 01:29:03",
"timeAgo": "23 mins ago"
}
],
"unreadCount": 1
}
}
If something goes wrong
| Code | message you get back |
| 401 |
Please login first (missing or expired token) |
POST
Mark as read
Login required
api/notifications/mark_read.php
Send id to mark ONE notification as read, or send all=1 to clear the whole list. Use all=1 behind a "Mark all as read" button.
What you send
Headers
Fields in the JSON body
| Field | Type | Required | What it means |
| id |
int |
optional |
Which notification. Required unless you send all |
| all |
0 or 1 |
optional |
Send 1 to mark every notification as read |
Example request body
{
"all": 1
}
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "All notifications marked as read",
"data": null
}
If something goes wrong
| Code | message you get back |
| 422 |
Please fill all required fields |
| 404 |
Notification not found |
POST
Send your own notification
Admin only
api/notifications/send.php
The admin writes a message themselves, e.g. "Please add your bills before Friday". It appears on the Notifications screen AND is pushed to the phones. Leave memberId out to send it to the whole family.
What you send
Headers
Fields in the JSON body
| Field | Type | Required | What it means |
| title |
string |
required |
Short heading, up to 150 characters |
| message |
string |
required |
The message itself |
| memberId |
int |
optional |
Send to one member only. Leave it out for everybody |
| type |
string |
optional |
INFO, BUDGET_WARNING or EXPENSE_ADDED. Default INFO |
Example request body
{
"title": "Add your bills",
"message": "Please add this month's bills before Friday."
}
What you get back
HTTP 201 · Content-Type: application/json
{
"success": true,
"message": "Notification sent",
"data": {
"notificationId": 12,
"sentTo": "Everyone in the household",
"devicesNotified": 3,
"pushEnabled": true
}
}
If something goes wrong
| Code | message you get back |
| 422 |
Please fill all required fields |
| 403 |
Only the household admin can do this |
| 404 |
That member is not part of your household |
Push Notifications
Firebase Cloud Messaging. The app registers its device token once after login, and from then on every notification the API creates is also pushed to the phone - even when the app is closed. Nothing else changes: the same messages still appear on the Notifications screen.
POST
Register this phone
Login required
api/devices/register.php
Call this right after login with the token Firebase gave the app (FirebaseMessaging.getInstance().token), and again whenever onNewToken() fires. Sending the same token twice is safe - the row is updated, never duplicated. Careful: the field is called deviceToken, because token already means the login token everywhere else.
What you send
Headers
Fields in the JSON body
| Field | Type | Required | What it means |
| deviceToken |
string |
required |
The FCM registration token from the app (20-255 characters) |
| platform |
string |
optional |
android, ios or web. Default android |
| deviceName |
string |
optional |
Anything you like, e.g. "Samsung A15". Only for your own records |
Example request body
{
"deviceToken": "fMEP0vJ9S...long token from Firebase",
"platform": "android",
"deviceName": "Samsung A15"
}
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Device registered for push notifications",
"data": {
"registered": true,
"platform": "android",
"deviceCount": 1
}
}
If something goes wrong
| Code | message you get back |
| 422 |
That does not look like a Firebase device token |
| 401 |
Please login first (missing or expired token) |
POST
Stop pushes to this phone
Login required
api/devices/unregister.php
Call this when the user logs out, otherwise the next person to use the phone keeps receiving this family's notifications. (api/auth/logout.php does the same thing if you send it deviceToken.) Send all=1 to sign every device of this user out of push.
What you send
Headers
Fields in the JSON body
| Field | Type | Required | What it means |
| deviceToken |
string |
optional |
The device to remove. Required unless you send all |
| all |
0 or 1 |
optional |
Send 1 to remove every device of this user |
Example request body
{
"deviceToken": "fMEP0vJ9S...long token from Firebase"
}
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Device removed from push notifications",
"data": {
"removed": 1
}
}
If something goes wrong
| Code | message you get back |
| 422 |
Please fill all required fields |
| 401 |
Please login first (missing or expired token) |
POST
Send a test push
Login required
api/devices/test.php
Sends a real push to your own phones immediately and tells you exactly what Firebase answered. Use this first when push "is not working" - the errors list gives the real reason. Nothing is saved to the notifications table.
What you send
Headers
Fields in the JSON body
| Field | Type | Required | What it means |
| title |
string |
optional |
Heading. Default "SmartSpend test" |
| message |
string |
optional |
Body text. Default explains that push works |
Example request body
{
"title": "SmartSpend test",
"message": "Testing 1 2 3"
}
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Test push sent to 1 device(s)",
"data": {
"devicesTried": 1,
"sent": 1,
"failed": 0,
"removed": 0,
"errors": []
}
}
If something goes wrong
| Code | message you get back |
| 404 |
No device registered for this account yet |
| 502 |
Firebase did not accept the message (the reason is in errors) |
| 503 |
Push is not set up on this server (no key in config/) |
POST
Send a test push to a member
Admin only
api/devices/test_to_user.php
The admin picks a member and pushes to THAT person's phones immediately. Same live report as the endpoint above, but aimed at somebody else - use it to prove push works for a member, not just for yourself. Nothing is saved to the notifications table. There is a click-it version of this at admin/push_test.php.
What you send
Headers
Fields in the JSON body
| Field | Type | Required | What it means |
| userId |
int |
required |
Which member to push to. Must be in your household |
| title |
string |
optional |
Heading. Default "SmartSpend test" |
| message |
string |
optional |
Body text. Default explains that push works |
Example request body
{
"userId": 3,
"title": "SmartSpend test",
"message": "Testing 1 2 3"
}
What you get back
HTTP 200 · Content-Type: application/json
{
"success": true,
"message": "Test push sent to Hamza Khan (1 device(s))",
"data": {
"memberId": 3,
"member": "Hamza Khan",
"devicesTried": 1,
"sent": 1,
"failed": 0,
"removed": 0,
"errors": [],
"serverProject": "smartspend-e8aef"
}
}
If something goes wrong
| Code | message you get back |
| 403 |
Only the household admin can do this |
| 404 |
That member is not part of your household |
| 404 |
That member has no registered device yet |
| 422 |
Please fill all required fields |
| 502 |
Firebase did not accept the message (the reason is in errors) |
| 502 |
Push is set up against the wrong Firebase project (errors.fix says what to do) |
| 503 |
Push is not set up on this server (no key in config/) |
Debug mode is OFF.
Because this is the live server, database errors are hidden and
Forgot password no longer returns the code in the response.