This documentation covers all REST API endpoints for the NearbyMe food delivery platform. Use the navigation on the left to browse by category or scroll through all endpoints.
Test Kitchen Lagos
a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5dCustomer Flow: Browse, Order, Track
Create a customer account
Navigate to /auth/sign-up and select "Customer" role
View available restaurants
GET /api/restaurantscurl -X GET "/api/restaurants?latitude=6.5244&longitude=3.3792"
Get menu items for a restaurant
GET /api/restaurants/{id}/menu-itemscurl -X GET "/api/restaurants/a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d/menu-items"
Add items to shopping cart
POST /api/cartcurl -X POST "/api/cart" -H "Authorization: Bearer TOKEN" -d '{"menu_item_id": "aaaa1111-1111-1111-1111-111111111111", "quantity": 2}'Apply a discount code
POST /api/cart/promocurl -X POST "/api/cart/promo" -H "Authorization: Bearer TOKEN" -d '{"promo_code": "WELCOME20"}'Create order from cart
POST /api/orderscurl -X POST "/api/orders" -H "Authorization: Bearer TOKEN" -d '{"delivery_address": "45 Admiralty Way, Lagos", "delivery_latitude": 6.4355, "delivery_longitude": 3.4500}'Monitor order status
GET /api/orders/{id}curl -X GET "/api/orders/ORDER_ID" -H "Authorization: Bearer TOKEN"
| From Status | To Status | Allowed Roles |
|---|---|---|
| placed | accepted | vendor |
| placed | cancelled | customervendoradmin |
| accepted | preparing | vendor |
| accepted | cancelled | vendoradmin |
| preparing | ready | vendor |
| ready | picked_up | rider |
| picked_up | delivered | rider |
{
"success": false,
"error": "Error message",
"code": "ERROR_CODE"
}Overview
The payment gateway is Paystack (Nigerian). There are no webhooks involved in the mobile flow — verification is entirely client-driven. The mobile app calls the verify endpoint after the user completes payment inside the Paystack UI.
access_code + referenceCreate the Order
Must happen before payment. Call this when the customer taps "Proceed to Checkout".
POST /api/orders
Authorization: Bearer <customer_token>
{
"delivery_address": "12 Olu Adesanya Street, Lagos",
"delivery_latitude": 6.5244,
"delivery_longitude": 3.3792,
"special_instructions": "Leave at gate" // optional
}
// Response
{
"success": true,
"data": {
"id": "order-uuid-here", // ← SAVE THIS
"status": "placed",
"total_amount": 24000, // ← SAVE THIS
"subtotal": 22000,
"delivery_fee": 1500,
"service_fee": 500
}
}Initialize Payment
Calls Paystack internally and returns everything the mobile app needs to open the payment UI.
payment_method accepted values:
card— Paystack card payment onlybank_transfer— Bank transfer + bank channelsussd— USSD onlycash_on_delivery— No Paystack — skips verification entirelyPOST /api/payments
Authorization: Bearer <customer_token>
{
"order_id": "order-uuid-here",
"payment_method": "card",
"callback_url": "nearbyme://payment-callback" // your app deep link
}
// Response
{
"success": true,
"data": {
"id": "payment-uuid-here", // ← SAVE → payment_id for verify
"stripe_payment_intent_id": "CHW-1716-abc123", // ← SAVE → reference for verify
"paystack_access_code": "0peioxfhpn", // ← pass to Paystack SDK
"paystack_authorization_url": "https://checkout.paystack.com/0peioxfhpn",
"amount": 24000,
"status": "pending"
}
}stripe_payment_intent_id stores the Paystack reference string — not a Stripe ID. This is a naming artifact. Treat this value as your Paystack reference.Open Paystack Checkout
Open the Paystack UI using the paystack_access_code from Step 2. Use an in-app WebView or the platform SDK — do not open the device browser.
Flutter (flutter_paystack)
final charge = Charge()
..accessCode = payment.paystackAccessCode
..reference = payment.reference;
final response = await _paystack.checkout(
context,
method: CheckoutMethod.selectable,
charge: charge,
);
if (response.status == true) {
// call verifyPayment()
}React Native (WebView)
<WebView
source={{ uri: authorizationUrl }}
onNavigationStateChange={(nav) => {
if (nav.url.startsWith(
'nearbyme://payment-callback'
)) {
closeWebView()
verifyPayment()
}
}}
/>Verify Payment
Call this immediately after the Paystack SDK fires its success callback. Do not wait for user action. Block the UI during this call.
POST /api/payments/verify
Authorization: Bearer <customer_token>
{
"reference": "CHW-1716-abc123", // stripe_payment_intent_id from Step 2
"payment_id": "payment-uuid-here" // id from Step 2
}
// Success Response
{
"success": true,
"data": {
"id": "payment-uuid-here",
"status": "completed",
"order_id": "order-uuid-here",
"amount": 24000
}
}
// Failure Response (abandoned / declined)
{
"success": false,
"error": "Payment abandoned. Please try again."
}✅ What the API does internally:
1. Fetches payment record from DB
2. Fetches order separately (avoids RLS join issue)
3. Calls Paystack live API to verify the reference
4. Confirms amount paid ≥ order total (in kobo)
5. Updates payment status → completed using service-role client (bypasses RLS)
6. Advances order from placed → accepted
7. Creates a payment_success notification for the customer
Cash on Delivery
No Paystack involved. Skip Steps 3 and 4 entirely.
POST /api/payments
{
"order_id": "order-uuid-here",
"payment_method": "cash_on_delivery"
}
// Returns payment with status: "pending"
// Order moves forward when rider marks it delivered
// No verification step neededState the App Must Track
{
orderId: string, // from POST /api/orders
paymentId: string, // from POST /api/payments → data.id
reference: string, // from POST /api/payments → data.stripe_payment_intent_id
accessCode: string, // pass to Paystack SDK
status: 'idle' | 'creating_order' | 'initializing'
| 'awaiting_paystack' | 'verifying' | 'success' | 'failed'
}
// Persist paymentId + reference to local storage immediately after Step 2
// so an interrupted payment can be resumed on app restartLoading & Error States
| State | What to show |
|---|---|
| POST /api/payments in progress | Spinner: "Setting up payment…" |
| WebView / Paystack SDK open | Paystack handles its own loader |
| POST /api/payments/verify in progress | Full-screen overlay: "Confirming payment… Do not close the app" |
| verify → 400 | "Payment unsuccessful. Tap to try again." — safe to retry from Step 2 |
| verify → 500 | "Something went wrong. If you were charged, contact support." |
| verify → 200 | Navigate to Payment Success screen |
Edge Cases
App closed during payment
On restart, check SharedPreferences / AsyncStorage for a saved paymentId + reference. If found, prompt the user to resume and call verify. The endpoint is idempotent — safe to call again.
Verify called twice (double-tap / retry)
The verify endpoint is fully idempotent. If payment is already completed it returns the existing record and safely re-advances the order. No duplicate charges possible.
Amount mismatch
If Paystack reports a lower amount than the order total, verify returns 400 "Payment amount mismatch". Show an error and a support contact message — do not auto-retry.
Stale pending payment for same order
The initialize endpoint auto-deletes any existing pending payment for the order and creates a fresh Paystack session. Retrying Step 2 for the same order is always safe.
Network drop during verify
Retry up to 3× with exponential backoff. Idempotency makes this safe. After 3 failures show the support message.
Common Mistakes to Avoid
| ❌ Mistake | ✅ Fix |
|---|---|
| Using authorization_url as the reference | reference = stripe_payment_intent_id from the init response |
| Opening authorization_url in the device browser | Use a WebView or Paystack SDK — you must intercept the redirect |
| Calling verify before Paystack closes | Only call verify inside the SDK onSuccess callback or after redirect detected |
| Navigating away during verify | Block the UI — partial verify causes stuck orders |
| Showing success based on WebView close alone | Always confirm with verify endpoint — closed ≠ success |
| Not persisting paymentId + reference | Save to local storage immediately after Step 2 in case app is killed |
No Webhooks in the Mobile Flow
A webhook handler type is defined in the codebase but no webhook endpoint exists yet. The entire payment flow is client-driven — the mobile app is responsible for calling verify. Do not wait for a server push.
🔐 1. Auth & Session Management
Token refresh — do this automatically, not manually
On app launch: check if access token is expired. If yes, call POST /api/auth/refresh before any other request. Also intercept every 401 response and try refresh once — if refresh also fails, redirect to login.
POST /api/auth/refresh
Body: { "refresh_token": "..." }Returns new access_token + refresh_token. Store both. Old tokens are invalidated.
🏠 2. Home Screen — Call Order
GET /api/store-typesCall once on app launch. Renders the Food / Pharmacy / Shops tabs. DB-driven — when admin adds a new store type (e.g. "Bukas"), it appears here with zero code changes on your end. Never hardcode this list.
GET /api/bannersHome carousel. type="vendor" → navigate to store. type="external" → open external_url in WebView.
GET /api/home-sectionsAdmin-curated sections (e.g. "Top Picks", "Xmas Meals"). Render each section as a horizontal scroll row.
GET /api/featured-itemsFeatured menu items strip. Tapping an item navigates to the restaurant, not the item directly.
GET /api/explore?lat=&lng=&is_featured=trueFeatured restaurants section. Pass user lat/lng for distance ordering.
GET /api/recommendations?lat=&lng=Personalised "Based on your preference" section. Works for guests too (returns top-rated).
🏪 3. Store Browsing
GET /api/stores?store_type=restaurant&lat=&lng=&is_open=true&page=1Main store listing. Always pass lat/lng — enables distance sorting and radius filtering. Featured stores come first automatically.
store_typerestaurant | pharmacy | shop
is_featuredtrue = boosted stores only
searchtext search on name
is_opentrue = open now only
lat / lnguser coordinates
radiuskm from user (default 10)
💰 4. Show Pricing Before Checkout
GET /api/pricing/estimate?restaurant_id=X&delivery_lat=Y&delivery_lng=Z&subtotal=NCall this on the cart screen when the user selects a delivery address. Shows full breakdown before they place the order.
Returns:
{
"distance_km": 3.2,
"delivery_fee": 800,
"service_fee": 150,
"tax": 375,
"total": 6325,
"surge": false,
"estimated_delivery_minutes": 35
}If restaurant is outside delivery radius → returns 400 with user-facing message. Show it as an alert.
🎟️ 5. Promo Code Error Handling
Errors now carry a machine-readable code field. Handle each specifically:
| code | UI message to show |
|---|---|
| ERR_PROMO_NOT_FOUND | "This promo code doesn't exist" |
| ERR_PROMO_EXPIRED | "This promo code has expired" |
| ERR_PROMO_NOT_APPLICABLE | Use the message field from the API response |
| ERR_MINIMUM_ORDER_NOT_MET | Use the message field — it includes the exact amount needed |
💳 6. Payment Flow
Create order
POST /api/orders → returns order.id
Initiate payment
POST /api/payments with order_id → returns authorization_url + payment_id + reference. Save payment_id and reference to local storage immediately.
Open WebView
Open authorization_url in an in-app WebView. Intercept the redirect to callbackUrl to detect completion.
Poll payment status
GET /api/payments/:payment_id every 5s until status === "completed". Stop after 2 minutes and show "Payment pending — check your notifications" if not confirmed.
❌ 7. Order Cancellation Policy
Customers can only cancel while status is placed
Once the restaurant accepts, cancellation returns 400 with this message:
"Orders can no longer be cancelled once the restaurant has accepted. Please contact support."Do not show a generic error — display the message field directly to the user.
🔔 8. Push Notifications (FCM)
Register token after login
POST /api/users/me/device-token{ "token": "FCM_TOKEN", "platform": "android" }Also call on every onTokenRefresh event from Firebase.
Unregister on logout
DELETE /api/users/me/device-token?platform=androidNotification data payload — use for deep linking
Every push contains order_id and status in the data map. Navigate the user directly to the order screen when the notification is tapped.
⚡ 9. Live Order Updates (Supabase Realtime)
Use this instead of polling — subscribe to order status changes directly from the DB. RLS ensures users only receive their own order events.
// Subscribe to order status changes
final channel = supabase
.channel('order-$orderId')
.onPostgresChanges(
event: PostgresChangeEvent.update,
schema: 'public',
table: 'orders',
filter: PostgresChangeFilter(
type: FilterType.eq,
column: 'id',
value: orderId,
),
callback: (payload) {
final newStatus = payload.newRecord['status'];
// Update UI
},
)
.subscribe();
// Subscribe to new notifications
final notifChannel = supabase
.channel('notifications-$userId')
.onPostgresChanges(
event: PostgresChangeEvent.insert,
schema: 'public',
table: 'notifications',
filter: PostgresChangeFilter(
type: FilterType.eq,
column: 'user_id',
value: userId,
),
callback: (payload) {
// Update badge count / show banner
},
)
.subscribe();
// Always unsubscribe when leaving the screen
await supabase.removeChannel(channel);🔍 10. Search History
| Endpoint | Action |
|---|---|
| GET /api/search/history | Load recent searches (max 10, deduped) |
| DELETE /api/search/history | Clear all history |
| DELETE /api/search/history/:id | Delete a single entry (swipe-to-delete on each row) |
🛠️ 11. Flutter-only Tasks (no backend needed)
Cart expiry timer
If cart is idle for 30 minutes, show "Your cart has expired" banner and clear it locally. No API call needed.
Offline mode
When there is no internet, show a full-screen "No connection" state. Re-fetch automatically on reconnect using connectivity_plus.
Empty states
Every list screen (orders, search, favorites, notifications) needs an empty state illustration + message — not a blank screen.
Review prompt
30 minutes after an order is marked delivered, prompt the user to rate the restaurant. Show once per order only. Use POST /api/restaurants/:id/reviews.
Cancel error handling
If cancel returns 400 with the policy message, show it as an alert with a "Contact Support" button. Do not show a generic error toast.
Privacy Policy & Terms links
Required for App Store and Play Store. Add links on signup screen and in settings: /privacy and /terms on your domain.
Auth
Authentication endpoints. Login returns access_token + refresh_token. The mobile app must call /api/auth/refresh automatically when a 401 is received.
/api/auth/login/api/auth/refreshRestaurants
Manage restaurants and their settings
/api/restaurants/api/restaurants/api/restaurants/{id}/api/restaurants/{id}/api/restaurants/{id}/statusCart
Shopping cart management
/api/cart/api/cart/api/cart/api/cart/items/{id}/api/cart/items/{id}/api/cart/promo/api/cart/promoOrders
Order management and tracking
/api/orders/api/orders/api/orders/{id}/api/orders/{id}/status/api/orders/{id}/assign-riderRiders
Rider management and tracking
/api/riders/api/riders/api/riders/me/api/riders/me/api/riders/me/status/api/riders/me/location/api/riders/me/earningsPayments
Payment processing and earnings
/api/payments/api/payments/{id}/confirm/api/vendor/earningsCustomer Features
Endpoints for the customer mobile app — profile, addresses, search, favorites, notifications, reviews, and order utilities.
/api/users/me/api/users/me/api/users/me/notification-preferences/api/users/me/notification-preferences/api/addresses/api/addresses/api/addresses/{id}/api/categories/api/search/api/search/suggestions/api/search/history/api/search/history/api/favorites/api/favorites/api/favorites/{id}/api/notifications/api/notifications/{id}/read/api/restaurants/{id}/reviews/api/restaurants/{id}/reviews/api/riders/{id}/reviews/api/orders/{id}/track/api/orders/{id}/confirm/api/orders/{id}/reorder/api/orders/{id}/receipt/api/beneficiaries/api/beneficiaries/api/beneficiaries/{id}/api/beneficiaries/{id}/api/restaurants/{id}/banners/api/banners/api/featured-items/api/explore/api/recommendations/api/home-sections/api/store-types/api/storesAdmin
Admin-only endpoints. All require an admin JWT. Login: admin@nearbyme.io / Admin@NearbyMe2024!
/api/admin/dashboard/api/admin/analytics/api/admin/users/api/admin/users/{id}/api/admin/users/{id}/api/admin/users/{id}/api/admin/vendors/api/admin/vendors/{id}/api/admin/vendors/{id}/api/admin/riders/api/admin/riders/{id}/api/admin/orders/api/admin/orders/{id}/api/admin/orders/{id}/api/admin/earnings/api/admin/payouts/api/admin/payouts/api/admin/payouts/{id}/api/admin/disputes/api/admin/disputes/{id}/api/admin/promo-codes/api/admin/promo-codes/api/admin/promo-codes/{id}/api/admin/promo-codes/{id}/api/admin/audit-logs/api/admin/banners/api/admin/banners/api/admin/banners/{id}/api/admin/banners/{id}/api/admin/banners/{id}/api/admin/home-sections/api/admin/home-sections/api/admin/home-sections/{id}/api/admin/home-sections/{id}/api/admin/home-sections/{id}/items/api/admin/home-sections/{id}/items/{itemId}/api/admin/home-sections/{id}/items/{itemId}