GEBETA Mobile API

Base URL: https://gebetaexpress.com/api/v1 · Flutter --dart-define=GEBETA_API=https://gebetaexpress.com/api/v1

GEBETA Mobile API (complete)

Base URL: https://gebetaexpress.com/api/v1

See MOBILE_TEAM_HANDOFF.md for the short partner + mobile checklist.

Envelope:

{ "success": true, "message": "...", "data": {}, "errors": [] }

Auth: Authorization: Bearer <raw-token> and the ci_session cookie from that login. See developer.md. 401 Unauthorized is a missing token. 422 is a validation error.

Login data.token is the raw token (bin2hex(random_bytes(32)), 64 hex chars). That is *not* a SHA-256 digest — SHA-256 of the raw value is also 64 hex, which is why this is easy to mix up.

| Step | What happens |
|------|----------------|
| Issue | Store hash('sha256', $raw) in api_tokens.token_hash (legacy token column also gets the hash). Return $raw to the client. |
| Verify | Read Authorization: Bearer <token>, compute sha256 once, look up token_hash (or token). If the client sent a legacy hash, that value is also accepted. |

X-APP-KEY, X-DEVICE-ID, X-APP-VERSION are not required. CORS allows them.

Never parse, decode, or re-hash the token on the client. Never send the DB hash as data.token.

Phone is primary. Customer and rider may share the same phone as separate users rows (role-scoped).

---

Recurring sync loops (recommended)

Call GET /bootstrap once on launch — use data.poll.*_ms and each response’s poll_after_ms.

| Loop | Endpoint | Auth | Typical interval |
|------|----------|------|------------------|
| Cold start | GET /bootstrap?role=passenger\|rider | no | once |
| Heartbeat | GET /ping or /heartbeat | no | 30–60s |
| Passenger home | GET /sync/passenger?since= | passenger | poll_after_ms (~8s) |
| Order map / status | GET /orders/{id}/tracking or GET /orders/status/{id\|pnr} | owner / rider / restaurant / admin / public PNR or tracking_token | ~15s while active (poll_after_ms) |
| Public track | GET /track/{pnr\|token} | PNR or tracking token | ~15s |
| Notifications | GET /sync/notifications | yes | ~15s |
| Mark read | POST /sync/notifications/read | yes | on open |
| Rider online | GET /rider/sync + POST /riders/location | rider | offers ~4s, GPS ~12s |
| Catalog refresh | GET /catalog/restaurants?q= | no | ~60s |
| Meal plan today | GET /passenger/meal-plan/today | passenger | with orders sync |

Every status payload includes server_time, status / wait, and poll_after_ms when applicable.

---

Passenger auth

| Method | Path | Body |
|--------|------|------|
| POST | /auth/passenger/login | { phone, password } |
| POST | /auth/passenger/otp/request | { phone } |
| POST | /auth/passenger/otp/verify | { phone, code } |
| POST | /auth/passenger/register | { phone, password, full_name } |
| POST | /auth/passenger/password/reset | { phone } then { phone, code, password } |
| POST | /auth/passenger/logout | Bearer |
| GET | /auth/passenger/me | Bearer |

Rider auth & ops

| Method | Path |
|--------|------|
| POST | /auth/rider/register | { full_name, phone, password, email?, vehicle_type? } — creates pending rider. Confirmation email if SMTP is set. No token until admin approves, then /auth/rider/login. |
| POST | /auth/rider/login |
| POST | /auth/rider/otp/request |
| POST | /auth/rider/otp/verify |
| POST | /auth/rider/password/reset |
| POST | /auth/rider/logout |
| GET | /auth/rider/me |
| GET | /rider/sync |
| POST | /riders/availability | { online } |
| POST | /riders/location or /rider/location | { latitude, longitude, accuracy?, heading?, speed?, timestamp? } — rider_id from Bearer only |
| GET | /orders/{id}/tracking | Owner, assigned rider, restaurant, admin, or ?pnr= / ?token= |
| GET | /track/{pnr\|token} | Public live map payload (PNR or tracking_token, not sequential id) |
| GET | /delivery/offers |
| POST | /delivery/{id}/accept |
| POST | /auth/rider/orders/{id}/reject |
| POST | /delivery/{id}/pickup |
| POST | /auth/rider/orders/{id}/arrive |
| POST | /auth/rider/orders/{id}/on-the-way |
| POST | /delivery/{id}/delivered | { pod_code? } |
| GET | /auth/rider/history |
| GET | /auth/rider/earnings |

Home rails (nearby / top rated / sponsored)

Call GET /catalog/home?lat=&lng= once for the passenger home screen, or the named endpoints below.

| Method | Path | Auth | Notes |
|--------|------|------|--------|
| GET | /catalog/home?lat=&lng= | no | nearby, top_rated, sponsored, deals |
| GET | /catalog/nearby?lat=&lng= | no | aliases /restaurants/nearby |
| GET | /catalog/top-rated | no | aliases /restaurants/top-rated |
| GET | /catalog/sponsored | no | is_featured ads; aliases /restaurants/sponsored |
| GET | /catalog/deals | no | discount > 0 |
| GET | /catalog/restaurants?sort=nearby\|top\|sponsored\|deals&lat=&lng= | no | same payload via sort |
| GET | /notifications | Bearer | alias of /sync/notifications; ?since= ?mark_read=1 ?limit= |
| GET | /notifications/unread | Bearer | { unread } |
| POST | /notifications/read | Bearer | alias of /sync/notifications/read |

GET /search?q= also returns products and services for market / book.

---

Market, book, service, delivery

Same orders table and PNR as food. service_type: FOOD \| MARKET \| DELIVERY \| BOOKING \| SERVICE.

| Method | Path | Auth |
|--------|------|------|
| GET | /market or /market/products?q=&aisle= | no |
| GET | /market/products/{id} | no |
| POST | /market/checkout | passenger Bearer { items:[{product_id,quantity}], address, lat, lng, payment_method } |
| GET | /book | no — scheduled bookings (kind=book) |
| GET | /book/{id} | no |
| POST | /book | Bearer { service_id, scheduled_start, address, lat, lng, payment_method } |
| GET | /services | no — bookable offerings (kind=service + book) |
| POST | /services | Bearer — same body as POST /book |
| POST | /reservations | Bearer { restaurant_id, guest_name, phone, party_size, date, time } |
| POST | /delivery/quote | optional |
| POST | /delivery | Bearer — parcel send |
| GET | /delivery | Bearer — my parcels |

Partner (Zemen Gebeya) still uses X-Api-Key on /partner/market/catalog — partner-api.md.

---

Catalog & meal plans

| Method | Path |
|--------|------|
| GET | /restaurants?q= |
| GET | /restaurants/{id} |
| GET | /restaurants/slug/{slug} |
| GET | /catalog/restaurants?q= |
| GET | /catalog/restaurants/{id\|slug} |
| GET | /catalog/dishes?q= | popular / searchable dishes |
| GET | /search?q= | restaurants + dishes + categories |
| GET | /search/suggest?q= | same as /search |
| GET | /categories |
| GET | /payments |
| GET | /location/suggest?q= |
| GET | /location/reverse?lat=&lng= |
| GET | /meal-plans/{restaurantId} | weekly/monthly from + after-discount |
| GET | /passenger/meal-plan/today | today’s meal-plan orders |
| GET | /passenger/orders | Bearer — recent orders for the status loop |
| GET | /passenger/sync | Bearer — alias of /sync/passenger |

Cart, quote & checkout (Flutter)

Cart is client-side. Send items on quote/checkout.

| Method | Path | Auth |
|--------|------|------|
| POST | /orders/quote | no |
| POST | /orders/checkout | Bearer, or phone in body (guest, same as website) |
| GET | /favorites | Bearer |
| POST | /favorites/toggle | Bearer { restaurant_id, menu_item_id? } |
| POST | /auth/passenger/profile | Bearer { full_name, email? } |
POST /orders/quote
{
  "items": [{ "menu_item_id": 1, "quantity": 2 }],
  "lat": 9.03,
  "lng": 38.75,
  "order_type": "delivery"
}
POST /orders/checkout
{
  "items": [{ "menu_item_id": 1, "quantity": 2 }],
  "full_name": "Abebe",
  "phone": "0911234567",
  "email": "abebe@example.com",
  "delivery_address": "Bole, Edna Mall",
  "lat": 9.01,
  "lng": 38.79,
  "payment_method": "cod",
  "order_type": "delivery",
  "instructions": "Call on arrival"
}

payment_method: cod | chapa | telebirr | stripe | debopay.

Checkout response includes order_id, pnr, checkout_url (open in WebView when not COD).

Telegram (bot + web)

| Method | Path |
|--------|------|
| POST | /telegram/webhook |
| POST | /telegram/webhook/r/{id} |
| GET | /telegram/continue/{token}?pay=stripe | cart + pay method → /order |
| GET | /eat | browser menu (not Mini App) |

Bot checkout: asks name then phone (Share contact or type 09…). Location is a named Addis sub-city from Super Admin zones, then a predefined landmark (Edna Mall, CMC, …) — or GPS / typed place. Delivery fee is restaurant pin → that place (km × zone per-km + zone base). Status pushes: food ready, rider coming, track link — same orders as the website.

---

Live tracking

Pickup / dropoff coordinates are snapshotted onto the order at create time. Later restaurant or profile moves do not change an existing order.

Status values stay the database names (assigned, picked_up, on_delivery, near_customer, …). UI should show status_label. Do not invent extra kitchen statuses.

POST /riders/location (alias POST /rider/location)

Auth: rider Bearer. Never send rider_id in the body — it is ignored.

{
  "latitude": 9.03,
  "longitude": 38.75,
  "accuracy": 12.4,
  "heading": 180,
  "speed": 4.2,
  "source": "gps",
  "timestamp": "2026-08-24T17:00:00+03:00"
}

Poll ~12s while online with an active delivery. Stop when the order is terminal. Routing is throttled server-side (~150 m or ~90 s), not on every ping.

GET /orders/{id}/tracking

Auth: Bearer (customer owner, assigned rider, restaurant of the order, admin) or ?pnr=847291 / ?token=<tracking_token>. Sequential order IDs without auth return 401.

GET /track/{pnr|token}

Public. 6-digit PNR or opaque tracking_token only.

Payload (data)

{
  "order_id": 12,
  "pnr": "847291",
  "status": "on_delivery",
  "status_label": "On the way",
  "progress_percent": 62,
  "progress_source": "route",
  "eta_minutes": 8,
  "eta_label": "Arriving in about 8 min",
  "remaining_km": 2.4,
  "stale": false,
  "poll_after_ms": 15000,
  "pickup": { "name": "Restaurant", "address": "…", "latitude": 9.01, "longitude": 38.76 },
  "dropoff": { "name": "Customer", "address": "…", "latitude": 9.03, "longitude": 38.75 },
  "rider": { "id": 3, "name": "Abebe", "latitude": 9.02, "longitude": 38.75, "heading": 90, "updated_at": "…" },
  "route": { "points": [[9.02, 38.75], [9.03, 38.75]], "distance_km": 3.1, "remaining_km": 2.4, "eta_minutes": 8 },
  "server_time": "2026-08-24T17:00:00+03:00"
}

progress_percent is remaining-route based while the rider is on the way (not delivered ? 100 : 0). When poll_after_ms is 0, stop polling (delivered / cancelled / failed).

GET /orders/status/{id|pnr} includes the same object under tracking, plus poll_after_ms.

---

Restaurant payout webhook

POST https://gebetaexpress.com/webhooks/restaurant-payout

Marks delivered orders paid to the restaurant (restaurant_net). Requires env RESTAURANT_PAYOUT_WEBHOOK_SECRET. Header X-Gebeta-Payout-Secret (or JSON secret). sleep is accepted as an alias of slip.

{
  "restaurant_id": 1,
  "order_ids": [12, 13],
  "transaction_number": "CBE-123",
  "slip": "https://example.com/slip.jpg",
  "pay_all": false,
  "note": "evening COD"
}

Also: guest-api.md, rider-api.md.

All API docs

Back to home