9.7 KiB
Home Page API Calls
Document for the backend team. Describes every HTTP request the mobile/web client fires when the user opens /home, including call order, parameters, and what blocks the UI from rendering.
Context: Home page load feels slow. The client currently waits on 3 sequential API calls before showing content. A 4th call runs in the background; a 5th may fire when the user has no "your tenders".
Base URL: https://app.opplenz.com
Auth: All requests include Authorization: Bearer <access_token>.
Summary
| # | Endpoint | Method | Blocking UI? | When |
|---|---|---|---|---|
| 1 | /api/v1/tender-approvals/stats |
GET | Yes | Always on home load |
| 2 | /api/v1/tender-approvals?limit=10&offset=0 |
GET | Yes | Always on home load |
| 3 | /api/v1/feedback?feedback_type=like&limit=1&offset=0 |
GET | Yes | Always on home load |
| 4 | /api/v1/notifications?seen=false&limit=1&event_type=PUSH |
GET | No (background) | App shell mount (parallel) |
| 5 | /api/v1/tenders/recommend?limit=10&offset=0 |
GET | No | Only when call #2 returns an empty tender list |
Minimum calls on first load: 4 (calls 1–3 + 4)
Maximum calls on first load: 5 (adds call 5 when user has no your-tenders)
Perceived load time (client-side): Sum of response times for calls 1 → 2 → 3 (sequential await chain).
Call sequence
User opens /home
│
├─ [parallel, background] App shell
│ └─ GET /api/v1/notifications?seen=false&limit=1&event_type=PUSH
│
└─ [blocking] HomeViewModel.init()
│
├─ 1. GET /api/v1/tender-approvals/stats ← await
├─ 2. GET /api/v1/tender-approvals?limit=10&offset=0 ← await
│ └─ if tenders[] is empty (fire-and-forget, not awaited):
│ GET /api/v1/tenders/recommend?limit=10&offset=0
└─ 3. GET /api/v1/feedback?feedback_type=like&limit=1&offset=0 ← await
└─ UI renders (_isLoading = false)
Source: lib/view_models/home_view_model.dart → init()
API details
1. Tender approval stats
Partnership / self-apply progress rings on the home dashboard.
| Field | Value |
|---|---|
| Method | GET |
| Path | /api/v1/tender-approvals/stats |
| Query params | None |
| Blocking | Yes — first call in init() |
| Response fields used | data.partnership_count, data.self_apply_count |
Example:
GET /api/v1/tender-approvals/stats
Authorization: Bearer <token>
Accept: application/json
2. Your tenders (horizontal list)
Main tender carousel and "Your Tenders" section.
| Field | Value |
|---|---|
| Method | GET |
| Path | /api/v1/tender-approvals |
| Query params | limit=10, offset=0 |
| Optional filters | Not sent from home (status, created_from, created_to are empty) |
| Blocking | Yes — second call in init() |
| Response fields used | data.tenders[].tender (full tender objects), data.metadata.pages (pagination) |
Example:
GET /api/v1/tender-approvals?limit=10&offset=0
Authorization: Bearer <token>
Accept: application/json
Note: This is likely the heaviest call — it returns up to 10 full tender objects with nested data.
3. Liked tenders count
"Liked Tenders" stat card count.
| Field | Value |
|---|---|
| Method | GET |
| Path | /api/v1/feedback |
| Query params | feedback_type=like, limit=1, offset=0 |
| Blocking | Yes — third call in init() |
| Response fields used | data.meta.total only (client ignores the single returned item) |
Example:
GET /api/v1/feedback?feedback_type=like&limit=1&offset=0
Authorization: Bearer <token>
Accept: application/json
Optimization opportunity: Client only needs a count. A dedicated count endpoint (or meta.total without loading feedback records) would be faster than a full feedback query.
4. Unread notification check (background)
Red dot on the notification tab icon.
| Field | Value |
|---|---|
| Method | GET |
| Path | /api/v1/notifications |
| Query params | seen=false, limit=1, event_type=PUSH |
| Blocking | No — fired from app shell postFrameCallback |
| Response fields used | Presence of any result (updates NotificationStateService) |
Example:
GET /api/v1/notifications?seen=false&limit=1&event_type=PUSH
Authorization: Bearer <token>
Accept: application/json
Source: lib/core/routes/app_shell/app_shell_screen.dart
5. Recommended tenders (conditional fallback)
Shown when the user has no "your tenders". Replaces the horizontal list label with "Recommended Tenders".
| Field | Value |
|---|---|
| Method | GET |
| Path | /api/v1/tenders/recommend |
| Query params | limit=10, offset=0 |
| Blocking | No — triggered inside getYourTenders() without await |
| Condition | Fires only when call #2 returns data.tenders empty |
| Response fields used | data.tenders[] (full tender objects) |
Example:
GET /api/v1/tenders/recommend?limit=10&offset=0
Authorization: Bearer <token>
Accept: application/json
Pagination (scroll, not initial load)
After the home page renders, scrolling the horizontal tender list near the end triggers additional calls:
| Mode | Endpoint | Params |
|---|---|---|
| Your tenders | GET /api/v1/tender-approvals |
limit=10, offset=<page * 10> |
| Recommended (fallback mode) | GET /api/v1/tenders/recommend |
limit=10, offset=<page * 10> |
Page size is always 10.
Re-fetch triggers
The full init() sequence (calls 1–3, and possibly 5) runs again when:
- User switches away from Home tab and returns to it
- User navigates to Liked Tenders and comes back (
homeViewModel.init()on return)
This means the same 3–5 API calls can repeat on every tab switch back to Home.
APIs defined but NOT called on home load
| Endpoint | Notes |
|---|---|
GET /api/v1/feedback/stats/company |
Defined in client (HomeApi.statsCompany) but never called from home |
Some home stat cards show hardcoded 0 values (approved tenders count, tender value) — no API backing today.
Performance notes for backend
Client-side bottleneck
The client does not parallelize calls 1, 2, and 3. Total time-to-interactive ≈ T(stats) + T(your-tenders) + T(liked-count).
Suggested backend investigations
GET /api/v1/tender-approvals?limit=10&offset=0— Likely the slowest call. Check query plans, N+1 joins, and payload size (10 full tender objects).GET /api/v1/tender-approvals/stats— Runs before every home load; consider caching per company/user.GET /api/v1/feedback?feedback_type=like&limit=1&offset=0— Client only readsmeta.total. A lightweightGET /api/v1/feedback/count?feedback_type=likewould avoid loading feedback rows.GET /api/v1/tenders/recommend— May involve recommendation/AI logic; profile separately from tender-approvals.
Suggested backend optimizations
| Idea | Benefit |
|---|---|
Single aggregated GET /api/v1/home endpoint |
One round-trip instead of 3 sequential |
| Return stats + liked count alongside tender-approvals list | Eliminate calls 1 and 3 |
| Slim tender DTO for list views (id, title, deadline, status only) | Smaller payloads on call #2 and #5 |
| Cache stats and liked count with short TTL | Faster repeat visits / tab switches |
Suggested client optimizations (for reference)
These are frontend changes the mobile team could make independently:
- Run calls 1, 2, and 3 in parallel (
Future.wait) - Do not re-run full
init()on every tab switch — refresh only stale data - Await the recommend fallback before clearing loading state (avoids UI flicker)
Source files (client)
| File | Role |
|---|---|
lib/view_models/home_view_model.dart |
Orchestrates all home API calls |
lib/data/repositories/home_repository.dart |
Repository layer |
lib/data/services/home_service.dart |
Stats, recommend, notifications |
lib/data/services/your_tenders_service.dart |
Your tenders list |
lib/data/services/liked_tenders_service.dart |
Liked count |
lib/data/services/api/home_api.dart |
Endpoint path constants |
lib/data/services/api/your_tenders_api.dart |
Tender-approvals URL builder |
lib/data/services/api/liked_tenders_api.dart |
Feedback URL builder |
lib/core/routes/app_shell/app_shell_screen.dart |
Background notification check |
lib/views/home/pages/home_screen.dart |
Re-init on tab return |
Quick test commands
Replace $TOKEN with a valid access token.
# 1. Stats
curl -s -w "\n%{http_code} %{time_total}s\n" \
'https://app.opplenz.com/api/v1/tender-approvals/stats' \
-H "Authorization: Bearer $TOKEN" | tail -1
# 2. Your tenders
curl -s -w "\n%{http_code} %{time_total}s\n" \
'https://app.opplenz.com/api/v1/tender-approvals?limit=10&offset=0' \
-H "Authorization: Bearer $TOKEN" | tail -1
# 3. Liked count
curl -s -w "\n%{http_code} %{time_total}s\n" \
'https://app.opplenz.com/api/v1/feedback?feedback_type=like&limit=1&offset=0' \
-H "Authorization: Bearer $TOKEN" | tail -1
# 4. Notifications (background)
curl -s -w "\n%{http_code} %{time_total}s\n" \
'https://app.opplenz.com/api/v1/notifications?seen=false&limit=1&event_type=PUSH' \
-H "Authorization: Bearer $TOKEN" | tail -1
# 5. Recommended (only when your tenders empty)
curl -s -w "\n%{http_code} %{time_total}s\n" \
'https://app.opplenz.com/api/v1/tenders/recommend?limit=10&offset=0' \
-H "Authorization: Bearer $TOKEN" | tail -1
Run all five and compare time_total to identify the slowest endpoint.