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.