DYU
Vendor Payment API
Mobile · v1
API Reference · v1

DYU Vendor Payment Tracking — Mobile API

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 URL http://<host>/api/v1 Auth Bearer JWT (HS256) TTL 30 days Server CodeIgniter 4 Content 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)

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.

1.2 Standard envelope

Every response uses the same envelope:

JSON · Response envelope
{
  "status":  "success",         // "success" | "error"
  "message": "OK",              // human-readable
  "data":    { ... } | [ ... ], // present on success
  "errors":  { ... } | null     // present on validation errors (status=error, http=422)
}

Always check the HTTP status code first, then status, then data.

1.3 HTTP status codes

CodeMeaning
200Success
201Created (returned by POST endpoints that create something)
400Bad request (logical error)
401Authentication required / invalid or expired token
403Authenticated but not authorised for this resource
404Resource not found
409Conflict (e.g. trying to edit a submitted request)
422Validation failed — see errors map
500Server error — please report

1.4 Authentication header

Every protected endpoint requires:

HTTP headers
Authorization: Bearer <jwt token>
Content-Type: application/json     (only for JSON bodies)

Tokens are issued by POST /auth/login. They are HS256 JWT signed with a server-side secret and expire after 30 days by default (configurable).

1.5 Date and currency formats

TypeFormatExample
DateYYYY-MM-DD2026-05-23
Date + TimeYYYY-MM-DD HH:MM:SS2026-05-23 16:42:11
CurrencyDecimal (INR)117500.00

1.6 Pagination

List endpoints accept page (default 1) and per_page (default 25, max 100) and return:

JSON · Paginated payload
"data": {
  "rows":        [ ... ],
  "total":       147,
  "page":        2,
  "per_page":    25,
  "total_pages": 6
}

1.7 Common error examples

401 Unauthorized
HTTP/1.1 401 Unauthorized
{ "status": "error", "message": "Token has expired.", "errors": null }
422 Unprocessable Entity
HTTP/1.1 422 Unprocessable Entity
{
  "status": "error",
  "message": "Validation failed.",
  "errors": {
    "project_id": "Project is required.",
    "invoice_amount": "Invoice amount must be greater than zero."
  }
}

02Domain model

High-level relationships between the entities exposed by this API:

User (App user) ──┬─ is incharge of ─┐ └─ is approver at ├─→ Project ─→ approval_levels (L1, L2, …, Ln) └─→ Payment Requests ─→ Attachments ├─→ Approvals (one row per level) └─→ Transactions (disbursements)

Payment request state flow

DRAFT — submit → UNDER_APPROVAL — approve (final) → APPROVED
from UNDER_APPROVAL — reject → REJECTED
from UNDER_APPROVAL — return → AMENDMENT_REQUIRED — submit → UNDER_APPROVAL
APPROVED — process → PAYMENT_PROCESSING PARTIALLY_PAID PAID
any time before PAYMENT_PROCESSING — cancel → CANCELLED
Editable only while status is DRAFT or AMENDMENT_REQUIRED, and only by the original requester.

3.1Auth & Session

GET /health Public
Health probe

Quick connectivity probe — useful for the splash screen.

Response 200
JSON · 200 OK
{
  "status": "success",
  "message": "Mobile API is reachable.",
  "data": {
    "time": "...",
    "app": "DYU Vendor Payment Tracking — Mobile API",
    "api_version": "v1"
  }
}
POST /auth/login Public
Authenticate & return a Bearer token

Authenticate the user and return a Bearer token. Optionally register the device for push notifications in the same call.

Request body
JSON · Request
{
  "email":    "chougule.koustubh@gmail.com",
  "password": "admin@123",
  "device": {
    "uuid":         "abc-123",
    "platform":     "ANDROID",          // ANDROID | IOS | WEB
    "fcm_token":    "fcmTokenString",
    "app_version":  "1.0.0",
    "device_model": "Pixel 8"
  }
}
Response 200
JSON · 200 OK
{
  "status": "success",
  "message": "Login successful.",
  "data": {
    "token":      "<jwt>",
    "token_type": "Bearer",
    "expires_in": 2592000,
    "user": {
      "id": 3, "user_code": null, "full_name": "Koustubh Chougule",
      "email": "chougule.koustubh@gmail.com", "mobile": "...",
      "user_type": "WEB_ADMIN", "designation": "Senior Project Incharge",
      "department": null, "profile_image_path": null,
      "profile_image_url": null, "is_active": 1,
      "last_login_at": "2026-05-23 16:30:00",
      "roles": [
        { "id": 3, "role_name": "Project Incharge", "role_key": "PROJECT_INCHARGE", "role_scope": "APP" }
      ]
    }
  }
}
Errors
401 invalid credentials 403 inactive account 422 missing fields
POST /auth/logout Auth
Sign out the current device (or all devices)

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/me Auth
Currently authenticated user

Returns the same user payload as login.

3.2Profile

GET /profile Auth
Get my profile

Same shape as /auth/me.

PUT /profile Auth
Update my profile

Also accepts POST /profile. Any subset of fields is allowed.

Request body
JSON · Request
{
  "full_name":   "...",
  "mobile":      "...",
  "designation": "...",
  "department":  "..."
}
POST /profile/change-password Auth
Change my password
Request body
JSON · Request
{
  "current_password": "...",
  "new_password":     "min 6 chars"
}
Errors
401 current password is wrong 422 validation failure
POST /profile/avatar Auth
Upload a new avatar (multipart/form-data)

Field avatar — JPG / PNG / WEBP, up to 5 MB. Response includes profile_image_path and profile_image_url.

3.3Devices (Firebase / FCM)

POST /devices/register Auth
Register / update a device for push notifications

Idempotent — calling with the same fcm_token updates the existing row. Re-call this whenever Firebase rotates the token.

Request body
JSON · Request
{
  "uuid":         "abc-123",
  "platform":     "ANDROID",
  "fcm_token":    "fcmTokenString",
  "app_version":  "1.0.0",
  "device_model": "Pixel 8"
}
POST /devices/unregister Auth
Deactivate a device
Request body
JSON · Request
{ "fcm_token": "fcmTokenString" }

3.4Vendors (read-only)

GET /vendors Auth
List vendors
Query params
ParamDescription
searchFree-text search
vendor_typeVENDOR | SUPPLIER | CONTRACTOR | CONSULTANT | OTHER
pagePage number (default 1)
per_pagePage size (default 25, max 100)
GET /vendors/{id} Auth
Vendor detail

Returns the full vendor record (without timestamps the app doesn't need).

3.5Projects (scoped to current user)

The list contains only projects where the user is either an active incharge or an active approver on at least one level.

GET /projects Auth
List my projects
Query params
ParamDescription
roleincharge | approver | all (default all)
statusPLANNED | ACTIVE | ON_HOLD | COMPLETED | CANCELLED
searchMatches name / code / location
pagePage number
per_pagePage size
Each row is annotated
JSON · Row
{
  "id": 2,
  "project_code": "PRJ-001",
  "project_name": "Rajaji Nagar Apartment Project",
  "location": "Bengaluru",
  "project_status": "ACTIVE",
  "is_incharge": true,
  "incharge_is_primary": true,
  "is_approver": true,
  "my_levels": [1]
}
GET /projects/{id} Auth
Project detail with accounts

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.

Response 200
JSON · 200 OK
{
  "data": {
    "id": 2,
    "project_name": "...",
    "is_incharge": true,
    "is_approver": true,
    "my_levels": [1],
    "my_account_ids": [1],
    "accounts": [
      {
        "id": 1, "account_code": "DIESEL", "account_name": "Diesel",
        "can_raise": true, "i_am_incharge": true,
        "incharges": [ { "user_id": 5, "full_name": "...", "is_me": true } ],
        "approval_levels": [
          { "level_number": 1, "approvers": [ { "user_id": 4, "full_name": "..." }, { "user_id": 6, "full_name": "..." } ] }
        ]
      }
    ],
    "request_counts": { "DRAFT": 4, "UNDER_APPROVAL": 2, "PAID": 1 },
    "total_requests": 7
  }
}

3.6Payment Requests

GET /payment-requests Auth
List payment requests

Returns a paginated list of compact request rows (header fields + project/vendor names).

Query params
ParamDescription
scopemine (default) / to_approve / all
statusOne of the request states
project_idFilter to one project
vendor_idFilter to one vendor
from, toYYYY-MM-DD filter on created_at
searchrequest_no / invoice_no / project_name / vendor_name
page, per_pagePagination
GET /payment-requests/{id} Auth
Full payment request bundle

Header + attachments + status logs + approvals + approval logs + transactions.

Response 200
JSON · 200 OK
{
  "data": {
    "id": 4, "request_no": "PR-20260523-00004-7",
    "project_id": 2, "vendor_id": 2,
    "project_name": "...", "vendor_name": "...",
    "invoice_no": "INV-AC-001", "invoice_date": "2026-05-20",
    "invoice_amount":    "100000.00",
    "status": "UNDER_APPROVAL", "current_level": 2, "max_level": 2,
    "submitted_at": "...", "approved_at": null,
    "attachments":   [ { "id":..., "url": "http://...", "attachment_type": "INVOICE", ... } ],
    "status_logs":   [ { "previous_status": "DRAFT", "new_status": "UNDER_APPROVAL", "action_by_name": "...", "remarks": "..." } ],
    "approvals":     [ { "level_number": 1, "status": "APPROVED", "approver_name": "...", "attachments": [ { "id": 1, "url": "http://...", "original_file_name": "signoff.pdf" } ] } ],
    "approval_logs": [ { "action": "APPROVED", "level_number": 1, "approver_name": "...", "attachments": [ ... ] } ],
    "approval_attachments": [ { "id": 1, "level_number": 1, "action": "APPROVED", "url": "http://..." } ],
    "transactions":  [ { "transaction_no": "PAY-20260523-...", "payment_amount": "117000.00", "payment_status": "SUCCESS", "processed_by_name": "..." } ],
    "paid_amount":    117000,
    "balance_amount":      0
  }
}
GUIDE How to create a payment request — end-to-end flow For 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
  1. Project picker — call GET /projects?role=incharge and only show projects where is_incharge: true. Save project_id.
  2. Vendor picker — call GET /vendors?search=… with debounce. Save vendor_id + show bank details from the response so the incharge can sanity-check.
  3. Header form — invoice no, invoice date, and invoice amount.
  4. Attachments — invoice scan, work photos, work order, measurement sheet etc. Upload one file at a time.
  5. 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)
  1. Save header → POST /payment-requests with action: "draft" → returns id.
  2. For each file picked → POST .../{id}/attachments (multipart).
  3. If the user edits the header again → PUT /payment-requests/{id} (partial).
  4. On SubmitPOST .../{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
  1. Collect header fields locally (Hive).
  2. Send header with action: "submit" → 201.
  3. 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 return AMENDMENT_REQUIRED — edit + submit UNDER_APPROVAL
UNDER_APPROVAL reject REJECTED — or — cancel CANCELLED
5. UX rules to enforce on the client
  • Show the edit / attachment / delete buttons only when status is DRAFT or AMENDMENT_REQUIRED and 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
HTTPWhat it means hereSuggested UX
401JWT expired / missingForce re-login. Don't try to refresh in this build.
403Not the incharge of this project, or not the requesterToast: "You're not assigned as incharge for this project." Grey out submit.
409Wrong status (already submitted, already paid, etc.)Refresh the request bundle and re-render — the server's state is the truth.
422Validation errors — see the errors mapHighlight individual fields by key. Each value is a user-readable string.
500Server error (rare)"Something went wrong, please try again." Log to crashlytics.
POST /payment-requests Auth · 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.

Request body
JSON · Request
{
  "project_id": 2,
  "account_id": 1,
  "vendor_id":  2,
  "invoice_no": "INV-AC-001",
  "invoice_date": "2026-05-20",
  "invoice_amount": 100000,
  "action": "draft"
}
Successful response (201)
JSON · 201 Created
{
  "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 project 422 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 requester 409 wrong status 422 validation
DELETE /payment-requests/{id} Auth · Incharge
Soft-delete a draft

Soft-deletes a DRAFT request. Once submitted, use cancel.

POST /payment-requests/{id}/submit Auth · Incharge
Submit for approval

Moves DRAFT / AMENDMENT_REQUIREDUNDER_APPROVAL and notifies the level-1 approver.

Request body (optional)
JSON · Request
{ "remarks": "..." }
POST /payment-requests/{id}/cancel Auth · 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.
GET /payment-requests/{id}/attachments Auth
List active attachments
Each row
JSON · Row
{
  "id": 1, "attachment_type": "INVOICE",
  "file_name":          "1779534358-73301fbc.txt",
  "original_file_name": "invoice.pdf",
  "file_path":          "uploads/payment-requests/4/1779534358-73301fbc.txt",
  "file_url":           "http://localhost:8080/uploads/payment-requests/4/...",
  "url":                "http://localhost:8080/uploads/payment-requests/4/...",
  "mime_type":          "application/pdf",
  "file_size_bytes":    23440,
  "uploaded_from":      "APP",
  "is_active":          1,
  "created_at":         "..."
}
POST /payment-requests/{id}/attachments Auth · Requester
Upload an attachment (multipart/form-data)
Fields
FieldRequiredNotes
file yes jpg, jpeg, png, webp, gif, pdf, doc, docx, xls, xlsx, txt. Max 25 MB.
attachment_type no Default OTHERINVOICE | WORK_PHOTO | WORK_ORDER | MEASUREMENT_SHEET | APPROVAL_DOC | OTHER
DELETE /payment-requests/{id}/attachments/{aid} Auth · Requester
Soft-delete an attachment

Marks is_active = 0. The file remains on disk for audit.

3.8Approval workflow

GET /approvals/inbox Auth · Approver
My pending approvals

Requests where I am the current approver and the level is PENDING. Each row carries a my_level and assigned_at.

Query params
ParamDescription
searchFree-text search
project_idFilter to one project
page, per_pagePagination
GET /approvals/history Auth · Approver
My approval history

Requests I have already acted on (any of APPROVED, REJECTED, RETURNED_FOR_AMENDMENT).

POST /payment-requests/{id}/approve Auth · Approver
Approve at my level

Supports JSON or multipart/form-data. Optional file (max 25 MB) may be attached at every approval level.

Request body
FieldRequiredNotes
remarksnoApproval comment
filenoSupporting document (jpg, png, pdf, doc, xls, txt)
attachment_remarksnoCaption for the file
JSON · Request
{ "remarks": "All good at L1" }
Response 200 (compact)
JSON · 200 OK
{
  "status": "success",
  "message": "Approved and forwarded to next level.",
  "data": {
    "id": 4, "status": "UNDER_APPROVAL", "current_level": 2, "max_level": 2,
    "attachment": { "id": 12, "level_number": 1, "action": "APPROVED", "url": "http://..." }
  }
}
Errors
403 not the current-level approver 409 request is not UNDER_APPROVAL
POST /payment-requests/{id}/reject Auth · 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 approver 409 request is not UNDER_APPROVAL 422 remarks missing
POST /payment-requests/{id}/return Auth · 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 approver 409 request is not UNDER_APPROVAL 422 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.

MethodPathDescription
GET /notifications Paginated inbox
GET /notifications/unread-count Just the badge count
POST /notifications/{id}/read Mark one as read
POST /notifications/read-all Mark all as read
GET /notifications Auth
Inbox
Query params
ParamDescription
only_unread1 to filter unread only
page, per_pagePagination
Each row
JSON · Row
{
  "id": 12, "user_id": 3, "payment_request_id": 4,
  "title":   "Payment processed in full",
  "message": "Request PR-20260523-00004-7: ₹ 117,000.00 via NEFT (Txn PAY-...)",
  "notification_type": "PAYMENT_PROCESSED",
  "data": { "transaction_no": "PAY-...", "payment_amount": 117000, "new_status": "PAID" },
  "is_read": 0, "read_at": null,
  "created_at": "2026-05-23 17:01:11"
}

Notification types currently emitted by the server

TypeWhen
APPROVAL_PENDINGA request just landed in your inbox (you are the current approver)
REQUEST_APPROVEDFinal approval cleared (notifies the requester)
REQUEST_REJECTEDA request you raised was rejected
AMENDMENT_REQUIREDA request you raised was returned for amendment
PAYMENT_PROCESSEDThe accounts team processed (full or partial) payment
GENERAL"Level X approved, moved to level Y" progress nudges

3.10Dashboard

GET /dashboard Auth
Home-screen summary (user-aware)

A single summary that powers the home screen. Works equally well for incharges and approvers — fields that don't apply will simply be empty.

Response 200
JSON · 200 OK
{
  "data": {
    "requested_by_me": {
      "total": 14,
      "by_status": { "DRAFT": 3, "UNDER_APPROVAL": 4, "APPROVED": 2, "PAID": 5 },
      "recent": [
        {
          "id": 4, "request_no": "PR-...", "status": "UNDER_APPROVAL",
          "current_level": 2, "max_level": 2,
          "invoice_amount": "100000.00",
          "project_name": "Rajaji Nagar Apartment Project",
          "vendor_name":  "Acme Construction Pvt Ltd",
          "created_at": "..."
        }
      ],
      "total_disbursed_inr": 542300.00
    },
    "inbox": {
      "pending": 3,
      "teaser": [
        {
          "id": 4, "request_no": "...",
          "invoice_amount": "...",
          "project_name": "...", "vendor_name": "...",
          "my_level": 1, "assigned_at": "..."
        }
      ]
    },
    "projects":      { "count": 2 },
    "notifications": { "unread": 5 }
  }
}

04End-to-end flow examples

4.1 Project Incharge — raise & submit a request

  1. POST /auth/login → token
  2. GET /vendors → pick vendor_id
  3. GET /projects?role=incharge → pick project_id (where is_incharge=true)
  4. POST /payment-requests → body, action="draft"
  5. POST /payment-requests/{id}/attachments file=@invoice.pdf, attachment_type=INVOICE
  6. POST /payment-requests/{id}/submit → moves to UNDER_APPROVAL (L1)

4.2 Approver — clear inbox

  1. POST /auth/login → token
  2. GET /approvals/inbox → list of pending decisions
  3. GET /payment-requests/{id} → review full bundle (attachments included)
  4. 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

  1. GET /payment-requests?status=AMENDMENT_REQUIRED
  2. GET /payment-requests/{id} → see the rejection remark in approval_logs[…].remarks
  3. PUT /payment-requests/{id} → fix header fields
    POST /payment-requests/{id}/attachments → attach the missing doc
  4. 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:

.env
firebase.enabled = true
firebase.credentialsPath = vendorapp-97434-firebase-adminsdk-fbsvc-8b55d1521a.json

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)

EmailPasswordRole 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.

08Change log

DateChange
2026-05-23 Initial v1 release: auth, profile, devices, vendors, projects, payment requests + work details + attachments, approvals, notifications, dashboard.