Everything the Flutter app needs for the Account Incharge and Approver flows.
Endpoints under /api/v1 return JSON only, are independent of the admin
panel sessions, and are authenticated with Bearer JWT (HS256).
Base URLhttp://<host>/api/v1Auth Bearer JWT (HS256)TTL 30 daysServer CodeIgniter 4Content application/json
Account-wise approval (Aug 2026):
create/submit now require account_id; project detail returns nested
accounts[]; any one approver completes a level. See
docs/APP_INTEGRATION_ACCOUNT_FLOW.md and
docs/MOBILE_API.md for the full app integration guide.
Open this documentation:
double-click docs/MOBILE_API.html · or visit
http://localhost:8080/docs/api · or
http://localhost:8080/docs/mobile-api.html
(Markdown source: docs/MOBILE_API.md)
No endpoints match your search.
01Conventions
Read these once, then you can skim the rest of the doc.
1.1 Base URL
Base URL
http://<your-server>/api/v1
The app is built on CodeIgniter 4. In development the base URL is
http://localhost:8080/api/v1. Use the value provided by the backend for
staging/production.
If fcm_token is provided, only that device is deactivated;
otherwise all devices for the user are deactivated. Tokens are stateless,
so logout is essentially a courtesy + device cleanup — the app should
also discard the token locally.
Request body (optional)
JSON · Request
{ "fcm_token": "fcmTokenString" }
GET/auth/meAuth
Currently authenticated user
Returns the same user payload as login.
3.2Profile
GET/profileAuth
Get my profile
Same shape as /auth/me.
PUT/profileAuth
Update my profile
Also accepts POST /profile. Any subset of fields is allowed.
Returns the project plus nested accounts[] (incharges + multi-approver levels).
Use accounts[].can_raise for the create-request account picker.
403 if the user has no account membership on this project.
See docs/APP_INTEGRATION_ACCOUNT_FLOW.md.
GUIDEHow to create a payment request — end-to-end flowFor mobile devs
A payment request is the central entity of this app. The mobile flow is built around a
two-stage commit: first you build a DRAFT (header + attachments) and
only when the incharge is happy you submit it for level-wise approval. The
sections below describe the recommended Flutter-side approach.
Why two stages? Once a request leaves DRAFT / AMENDMENT_REQUIRED,
the server locks header edits and attachment uploads/deletes. Trying to
modify a submitted request returns 409 Conflict — keep the user on the editor
screen until they explicitly tap Submit.
1. Suggested screen flow
Project picker — call GET /projects?role=incharge and only show projects where is_incharge: true. Save project_id.
Vendor picker — call GET /vendors?search=… with debounce. Save vendor_id + show bank details from the response so the incharge can sanity-check.
Header form — invoice no, invoice date, and invoice amount.
Attachments — invoice scan, work photos, work order, measurement sheet etc. Upload one file at a time.
Review & submit — show a summary; on confirm, call POST .../submit.
2. Two integration patterns — pick whichever fits your UX
Pattern A — Step-by-step wizard (recommended)
Save header → POST /payment-requests with action: "draft" → returns id.
For each file picked → POST .../{id}/attachments (multipart).
If the user edits the header again → PUT /payment-requests/{id} (partial).
On Submit → POST .../{id}/submit.
Pros: attachments can be uploaded as soon as they're picked; the user can resume the
draft later from any device; the request_no is shown right away.
Pattern B — One-shot submit
Collect header fields locally (Hive).
Send header with action: "submit" → 201.
Loop through picked files → POST .../{id}/attachments only if you used action: "draft" first.
Caveat: attachments uploaded after submission will fail with 409
because the request is already UNDER_APPROVAL. Use this only for quick
attachment-less requests, or upload all files before calling submit
by sending action: "draft" first.
3. Sequence diagram
Request lifecycle from the app's perspective
App Server
│ │
│ GET /projects?role=incharge │
│ ───────────────────────────► │
│ ◄─── projects (mine) │
│ │
│ GET /vendors?search=acme │
│ ───────────────────────────► │
│ ◄─── vendors │
│ │
│ POST /payment-requests │ action: "draft"
│ { project_id, vendor_id, …} │ → 201 { id, request_no, status:"DRAFT" }
│ ───────────────────────────► │
│ ◄─── { id: 42, request_no } │
│ │
│ POST /…/42/attachments │ multipart/form-data — per file
│ ───────────────────────────► │ → 201
│ │
│ PUT /payment-requests/42 │ partial header edits (optional)
│ ───────────────────────────► │ → 200
│ │
│ POST /…/42/submit │ → 200 { status: "UNDER_APPROVAL", current_level: 1 }
│ ───────────────────────────► │
│ │ ← Server pushes APPROVAL_PENDING notification to L1 approver
│ │
│ GET /payment-requests/42 │ poll / pull-to-refresh later for live status
│ ◄─── full bundle │
4. State the request will pass through
DRAFT— submit →UNDER_APPROVAL— final approve →APPROVED— accountant →PAID / PARTIALLY_PAID
UNDER_APPROVAL— reject →REJECTED— or — cancel →CANCELLED
5. UX rules to enforce on the client
Show the edit / attachment / delete buttons only whenstatus is DRAFT or AMENDMENT_REQUIREDand the logged-in user is the requested_by. The server enforces this too (returns 403 / 409) but blocking it on the UI is much friendlier.
Disable the Submit button until mandatory header fields are valid. Submission triggers FCM to the L1 approver — there's no "undo".
Refresh after submit by re-fetching GET /payment-requests/{id} — it returns the freshly created approvals array (one row per level) and the first status_logs entries which power the timeline UI.
Handle AMENDMENT_REQUIRED gracefully — the rejecting approver's remarks live in approval_logs[…].remarks and the latest status_logs entry. Surface that prominently so the incharge knows what to fix before resubmitting.
6. Caching & offline (Hive)
Persist drafts in Hive keyed by a client-side UUID before the first POST succeeds — so a flaky network doesn't lose user input.
After POST /payment-requests returns { id }, write that id back to the local draft and switch to PUT-based edits.
Queue attachment uploads — the multipart endpoint is per-file, so a queue lets you retry individually without re-uploading everything.
Cache the latest /projects and /vendors responses for offline pickers; refresh them on app focus.
7. Error handling cheat-sheet
HTTP
What it means here
Suggested UX
401
JWT expired / missing
Force re-login. Don't try to refresh in this build.
403
Not the incharge of this project, or not the requester
Toast: "You're not assigned as incharge for this project." Grey out submit.
409
Wrong status (already submitted, already paid, etc.)
Refresh the request bundle and re-render — the server's state is the truth.
422
Validation errors — see the errors map
Highlight individual fields by key. Each value is a user-readable string.
500
Server error (rare)
"Something went wrong, please try again." Log to crashlytics.
POST/payment-requestsAuth · Incharge
Create a payment request
Account Incharge only.account_id is required and must belong to
project_id. If action: "submit", the request is created
and moved straight to UNDER_APPROVAL (all L1 assignees notified).
See docs/APP_INTEGRATION_ACCOUNT_FLOW.md.
{
"status": "success",
"message": "Payment request created.",
"data": {
"id": 42,
"request_no": "PR-20260523-00042-7",
"status": "DRAFT", // or "UNDER_APPROVAL" if action was "submit"
"current_level": 0, // 1 once submitted
"max_level": 0, // computed when submitted
"attachments": [],
"approvals": [],
"status_logs": [ { "new_status": "DRAFT", "remarks": "Draft created from mobile app." } ]
// …rest of bundle
}
}
Errors
403 caller is not an active incharge of the chosen project422 validation
PUT/payment-requests/{id}Auth · Incharge
Partial update of a payment request
Also accepts POST /payment-requests/{id}. Partial update of any subset
of header fields. Allowed only while status is DRAFT or
AMENDMENT_REQUIRED. Caller must be the original requester.
Errors
403 not the requester409 wrong status422 validation
DELETE/payment-requests/{id}Auth · Incharge
Soft-delete a draft
Soft-deletes a DRAFT request. Once submitted, use cancel.
POST/payment-requests/{id}/submitAuth · Incharge
Submit for approval
Moves DRAFT / AMENDMENT_REQUIRED →
UNDER_APPROVAL and notifies the level-1 approver.
Request body (optional)
JSON · Request
{ "remarks": "..." }
POST/payment-requests/{id}/cancelAuth · Incharge
Cancel a request
Allowed in DRAFT, SUBMITTED, UNDER_APPROVAL,
AMENDMENT_REQUIRED, REJECTED. Sets status to
CANCELLED.
Request body (optional)
JSON · Request
{ "remarks": "..." }
3.7Attachments
Only modifiable while the request is DRAFT or
AMENDMENT_REQUIRED and only by the requester.
403 not the current-level approver409 request is not UNDER_APPROVAL
POST/payment-requests/{id}/rejectAuth · Approver
Reject the request
Supports JSON or multipart/form-data. Sets request status to
REJECTED and notifies the requester. remarks are required.
Optional file is stored against the rejection action.
Request body
JSON · Request
{ "remarks": "Duplicate invoice" }
Errors
403 not the current-level approver409 request is not UNDER_APPROVAL422 remarks missing
POST/payment-requests/{id}/returnAuth · Approver
Return for amendment
Supports JSON or multipart/form-data. Sets request status to
AMENDMENT_REQUIRED, resets approval rows for re-submission from level 1,
and notifies the requester. Optional file may be attached.
remarks are required.
Request body
JSON · Request
{ "remarks": "Please attach work order" }
Errors
403 not the current-level approver409 request is not UNDER_APPROVAL422 remarks missing
3.9Notifications
Notifications are written to notifications and pushed via FCM to every
active device registered for the recipient. Delivery status is tracked in
notification_deliveries.
POST/payment-requests/{id}/submit→ moves to UNDER_APPROVAL (L1)
4.2 Approver — clear inbox
POST/auth/login→ token
GET/approvals/inbox→ list of pending decisions
GET/payment-requests/{id}→ review full bundle (attachments included)
POST/payment-requests/{id}/approve→ with remarks, advances to next level / final POST/payment-requests/{id}/reject→ with mandatory remarks (final) POST/payment-requests/{id}/return→ with mandatory remarks (back to incharge)
4.3 Incharge — respond to an amendment
GET/payment-requests?status=AMENDMENT_REQUIRED
GET/payment-requests/{id}→ see the rejection remark in approval_logs[…].remarks
PUT/payment-requests/{id}→ fix header fields POST/payment-requests/{id}/attachments→ attach the missing doc
POST/payment-requests/{id}/submit→ re-issues from level 1 again
4.4 Token refresh
The current implementation uses long-lived tokens (30 days). If a request returns
401 with "Token has expired.", force the user back to the
login screen. A dedicated refresh-token endpoint can be added later if needed.
05Server-side notes (FYI for the mobile dev)
5.1 CORS
A permissive CORS filter is applied to /api/* so that browser-based debug tools
(Postman web, Swagger UI, curl-from-browser) work without configuration.
5.2 CSRF
CSRF protection is not applied to the /api/v1 group — the mobile app does
not need to send a CSRF token. Authentication is purely Bearer.
5.3 File uploads
Avatars: POST /profile/avatar — field name avatar, max 5 MB.
Attachments: POST /payment-requests/{id}/attachments — field name file, max 25 MB.
Server stores files under public/uploads/ and returns absolute
file_url / url.
5.4 Push notifications
FCM HTTP v1 dispatch is wired through NotificationService +
FcmClient. Configure in .env:
credentialsPath may be absolute or relative to the project root.
Do not commit the JSON key file — it is gitignored.
When a notification is created the server persists the inbox row, queues delivery per device,
and sends the FCM push immediately (best-effort). Retry stuck rows with:
CLI
php spark push:flush --limit 100
Mobile apps must register FCM tokens via POST /devices/register. Invalid tokens are auto-deactivated.
5.5 Configuration knobs
.env exposes the following API knobs (server side):
.env
api.jwtSecret = '...' # rotate this in production
api.jwtIssuer = 'dyu-vpts'
api.jwtTtlMinutes = 43200 # 30 days
api.uploadBasePath = 'uploads'
firebase.enabled = true
firebase.credentialsPath = 'vendorapp-97434-firebase-adminsdk-fbsvc-8b55d1521a.json'
06Test credentials (dev only)
Email
Password
Role on the seeded data
chougule.koustubh@gmail.com
admin@123
Project Incharge + L1 Approver of project #2
admin@dyu.com
admin@123
L2 (final) Approver of project #2
These credentials are for development only and must not exist in production.
07cURL quick start
bash · smoke test
# 1. Login
TOKEN=$(curl -sS -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"chougule.koustubh@gmail.com","password":"admin@123"}' \
| python3 -c "import sys,json;print(json.load(sys.stdin)['data']['token'])")
# 2. Me
curl -sS http://localhost:8080/api/v1/auth/me \
-H "Authorization: Bearer $TOKEN" | jq
# 3. List my projects
curl -sS "http://localhost:8080/api/v1/projects?role=all" \
-H "Authorization: Bearer $TOKEN" | jq
# 4. Create + submit a request in one go
curl -sS -X POST http://localhost:8080/api/v1/payment-requests \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"project_id":2,"vendor_id":2,"invoice_no":"INV-001",
"invoice_amount":50000,
"action":"submit"
}' | jq
# 5. Inbox of an approver
curl -sS http://localhost:8080/api/v1/approvals/inbox \
-H "Authorization: Bearer $TOKEN" | jq
Versioning
Current version: v1 (under /api/v1).
Backwards-incompatible changes will land under /api/v2 with at least one
minor release of overlap.
Adding fields to existing responses is not considered a breaking change — please
ignore unknown fields on the client.