Each block below is one network call, in the order it happens during a booking. For every call: URL, direction, payload, response, and what it does.
Both sides now allow multiple concurrent active bookings. A customer can place several bookings; a driver can be assigned to several at once. There is no DB unique constraint or picker filter preventing this — the system is per-booking, not per-user. Every WS event carries a booking_id and clients must route by that id, not by "the current booking."
DRIVER_RESPONSE_TIME — seconds a driver has to respond to a booking_assigned offer. Default 30. After this they're treated as a non-response, the attempt is consumed, and the chain moves on.CUSTOMER_WAITING_TIME — seconds a customer has to accept or reject after arrival_time_offered. Default 120. After this the booking auto-accepts.MAX_ATTEMPTS for the driver retry chain is hard-coded to 3 in driverAssignment.js.Customer creates a new booking. Server picks a driver, inserts the row, then pushes booking_assigned on the driver's socket.
POST /api/mobile/customer/booking/request Authorization: Bearer <firebase_id_token> X-Device-ID: <device_id> // routing hint for FCM X-Device-Model: <device_model> // optional Content-Type: application/json
{
"pickup_location_title": "Home",
"pickup_location_address": "12 King St, Sydney",
"pickup_map_id": "ChIJxxxxxxxxxxx",
"pickup_latitude": -33.8688,
"pickup_longitude": 151.2093,
"no_of_passengers": 2,
"note": null,
"scheduled_date": null,
"drop_offs": [
{
"drop_off_location_title": "Airport",
"drop_off_location_address": "Sydney Airport",
"drop_off_map_id": "ChIJxxxxxxxxxxx",
"drop_off_latitude": -33.9399,
"drop_off_longitude": 151.1753
}
]
}
{
"meta": { "code": 201, "status": "success", "timestamp": "..." },
"code": "CREATED",
"data": { "booking_id": "uuid", "booking_type": "instant", "driver_id": "uuid" },
"user_message": { "message": "Booking request submitted successfully." }
}
Server tells the assigned driver about the new ride. Fired automatically after Step 1. Starts the driver-response timer (DRIVER_RESPONSE_TIME, default 30 s).
WS /ws/driver?token=<firebase_id_token>
Server → Driver
{
"type": "booking_assigned",
"booking_id": "uuid",
"booking_status": "requested",
"booking_type": "instant",
"date": "2026-05-12T10:30:00.000Z",
"no_of_passengers": 2,
"note": null,
"customer_name": "Alice",
"customer_phone": "211234567",
"customer_phone_code": {
"id": "uuid",
"name": "New Zealand",
"code": "NZ",
"ph_code": "+64",
"flag": "https://.../uploads/flags/nz.png"
},
"pickup_location": { "title": "...", "address": "...", "map_id": "...", "latitude": -33.86, "longitude": 151.20 },
"dropoff_location": { "title": "...", "address": "...", "map_id": "...", "latitude": -33.93, "longitude": 151.17 },
"chat_history": []
}
Driver accepts an instant booking by sending how many seconds until pickup. DB: arrival_time = N, booking_status = "confirmation". Triggers Step 4. Stops the driver-response timer; starts the customer-response timer (CUSTOMER_WAITING_TIME, default 2 min).
WS /ws/driver?token=<firebase_id_token>
Driver → Server
{
"type": "submit_arrival_time",
"booking_id": "uuid",
"arrival_time": 300
}
// Driver ack: { "type": "arrival_time_accepted", "booking_id": "uuid", "booking_status": "confirmation", "arrival_time": { "value": 5, "unit": "minutes", "label": "5 min", "seconds": 300 }, "chat_history": [] } // Customer gets `arrival_time_offered` (Step 4). // On failure: { "type": "error", "code": "BOOKING_NOT_ASSIGNABLE" | "INVALID_PAYLOAD", "message": "..." }
Server forwards the driver's arrival time to the customer with a friendly display shape. Carries the driver's contact info.
WS /ws/customer?token=<firebase_id_token>
Server → Customer
{
"type": "arrival_time_offered",
"booking_id": "uuid",
"booking_status": "confirmation",
"arrival_time": { "value": 5, "unit": "minutes", "label": "5 min", "seconds": 300 },
"no_of_passengers": 2,
"driver_name": "Bob",
"driver_phone": "219876543",
"driver_phone_code": { "id": "...", "name": "...", "code": "...", "ph_code": "+64", "flag": "..." },
"vehicle_number": "ABC-123",
"driver_code": "DRV-0042",
"pickup_location": { ... },
"dropoff_location": { ... },
"chat_history": []
}
Customer confirms the offer. DB: booking_status = "on_the_way". Cancels the customer-response timer. Triggers Step 6.
WS /ws/customer?token=<firebase_id_token>
Customer → Server
{ "type": "accept", "booking_id": "uuid" }
// Customer ack (socket stays open for the ride): { "type": "accept_accepted", "booking_id": "uuid", "booking_status": "on_the_way", "driver_name": "Bob", "driver_phone": "...", "driver_phone_code": { ... }, "vehicle_number": "ABC-123", "driver_code": "DRV-0042", "chat_history": [] } // Driver gets `customer_accepted` (Step 6). // On failure: { "type": "error", "code": "BOOKING_NOT_ACCEPTABLE", "message": "..." }
Server tells the driver the ride is on. Includes the customer's contact info. Carries an auto_promoted: true flag when this fires because the offer timer expired (instead of a manual customer accept).
WS /ws/driver?token=<firebase_id_token>
Server → Driver
{
"type": "customer_accepted",
"booking_id": "uuid",
"booking_status": "on_the_way",
"no_of_passengers": 2,
"note": null,
"customer_name": "Alice",
"customer_phone": "...",
"customer_phone_code": { ... },
"pickup_location": { ... },
"dropoff_location": { ... },
"chat_history": [],
"auto_promoted": false // true if the customer-waiting timer expired
}
Driver ends the ride. DB: booking_status = "completed". Triggers Step 8. Chat for this booking is cleared from memory after the event ships.
WS /ws/driver?token=<firebase_id_token>
Driver → Server
{ "type": "mark_as_completed", "booking_id": "uuid", "status": "completed" }
// Driver gets ack (socket stays open for the next assignment): { "type": "complete_accepted", "booking_id": "uuid", "status": "completed", "chat_history": [ ... ] // final transcript snapshot } // Customer gets `ride_completed` (Step 8), socket closes 200ms later. // On failure: { "type": "error", "code": "BOOKING_NOT_COMPLETABLE", "message": "..." }
Server tells the customer the ride is finished. Customer socket auto-closes ~200 ms later.
WS /ws/customer?token=<firebase_id_token>
Server → Customer
{
"type": "ride_completed",
"booking_id": "uuid",
"status": "completed",
"chat_history": [ ... ]
}
Driver refuses a requested booking. Each booking gets up to MAX_ATTEMPTS (= 3) driver attempts total (initial + 2 retries), with cumulative exclusion — no driver is asked twice for the same booking. An attempt is consumed by an explicit decline, no response within DRIVER_RESPONSE_TIME (default 30 s), or no eligible driver available.
If attempts remain AND a replacement is found → new driver gets booking_assigned, declining driver gets { reassigned: true } (customer sees nothing). If attempts are exhausted OR no eligible driver is left → booking is cancelled with ride_cancelled_by = "admin", customer gets driver_cancelled with reason_code: "NO_DRIVER_AVAILABLE", declining driver gets { reassigned: false }.
WS /ws/driver?token=<firebase_id_token>
Driver → Server
{
"type": "decline_booking",
"booking_id": "uuid",
// exactly one of:
"cancellation_template_id": "uuid", // pre-saved template (admin or driver's own)
"cancellation_description": "free-text reason" // up to 512 chars
}
{ "type": "decline_accepted", "booking_id": "uuid", "reassigned": true | false }
Driver was assigned a booking but didn't act within DRIVER_RESPONSE_TIME. The server consumes their attempt and forwards the booking to the next driver in the chain. The expired driver gets this courtesy notice so their UI clears the offer card.
WS /ws/driver?token=<firebase_id_token>
Server → Driver
{
"type": "offer_expired",
"booking_id": "uuid",
"reason": "Response window elapsed."
}
Server tells the customer the booking has been cancelled by the driver side. Customer socket auto-closes ~200 ms later. reason_code distinguishes the cause:
NO_DRIVER_AVAILABLE — retry chain exhausted (3 attempts) or no eligible driver remained.DRIVER_CANCELLED — the active driver cancelled an in-progress (confirmation or on_the_way) ride via the HTTP cancel endpoint.SCHEDULED_DRIVER_NO_SHOW — the scheduled-booking promotion cron flipped a booking to cancelled because the driver never arrived/started.WS /ws/customer?token=<firebase_id_token>
Server → Customer
{
"type": "driver_cancelled",
"booking_id": "uuid",
"status": "cancelled",
"reason": "No driver available after multiple attempts.",
"reason_code": "NO_DRIVER_AVAILABLE" | "DRIVER_CANCELLED" | "SCHEDULED_DRIVER_NO_SHOW",
"chat_history": [ ... ] // final transcript when chat existed
}
Server tells the driver one of their bookings was cancelled by the system (3-attempt exhaustion, scheduled no-show, etc.). The driver's UI should drop the matching offer/ride from view. Driver socket stays open — they keep any other concurrent bookings.
WS /ws/driver?token=<firebase_id_token>
Server → Driver
{
"type": "booking_cancelled",
"booking_id": "uuid",
"status": "cancelled",
"reason": "...",
"reason_code": "NO_DRIVER_AVAILABLE" | "SCHEDULED_DRIVER_NO_SHOW"
}
Server tells the driver that the customer cancelled the ride via the HTTP cancel endpoint (e.g. customer ended the trip from the app after acceptance). This is distinct from the WS reject branch, which only fires from requested / confirmation.
WS /ws/driver?token=<firebase_id_token>
Server → Driver
{
"type": "booking_cancelled_by_customer",
"booking_id": "uuid",
"status": "cancelled",
"chat_history": [ ... ]
}
Customer cancels their own booking via WS. Allowed from requested or confirmation. Scheduled bookings: blocked within 1 hr of pickup. DB: booking_status = "cancelled", ride_cancelled_by = "customer". Customer socket auto-closes ~200 ms after the ack.
WS /ws/customer?token=<firebase_id_token>
Customer → Server
{ "type": "reject", "booking_id": "uuid" }
// Customer ack, socket terminates ~200 ms later: { "type": "reject_accepted", "booking_id": "uuid", "status": "cancelled", "driver_name": "...", "driver_phone": "...", "driver_phone_code": { ... }, "vehicle_number": "...", "driver_code": "...", "chat_history": [ ... ] } // Driver gets: { "type": "customer_rejected", "booking_id": "uuid", "status": "cancelled", "note": null, "customer_name": "Alice", "customer_phone": "...", "customer_phone_code": { ... }, "chat_history": [ ... ] } // On failure: { "type": "error", "code": "REJECT_WINDOW_CLOSED" | "BOOKING_NOT_REJECTABLE", "message": "..." }
If the customer doesn't accept or reject within CUSTOMER_WAITING_TIME (default 2 min) after Step 4, the server auto-accepts on their behalf — same effect as a manual accept. DB: booking_status = "on_the_way". Customer socket stays open for the ride.
WS /ws/customer?token=<firebase_id_token> WS /ws/driver?token=<firebase_id_token>
Server → Customer AND Server → Driver
// Customer side (socket stays open): { "type": "offer_auto_accepted", "booking_id": "uuid", "booking_status": "on_the_way", "chat_history": [] } // Driver side — same shape as a manual customer_accepted, with auto_promoted flag set: { "type": "customer_accepted", "booking_id": "uuid", "booking_status": "on_the_way", "no_of_passengers": 2, "customer_name": "Alice", "customer_phone": "...", "customer_phone_code": { ... }, "pickup_location": { ... }, "dropoff_location": { ... }, "chat_history": [], "auto_promoted": true }
Driver accepts a scheduled booking. No arrival time involved. Triggers booking_confirmed on the customer side.
WS /ws/driver?token=<firebase_id_token>
Driver → Server
{ "type": "accept_booking", "booking_id": "uuid" }
{
"type": "accept_accepted",
"booking_id": "uuid",
"chat_history": []
}
Server tells the customer their scheduled booking has been confirmed by a driver. The ride flips to on_the_way automatically at pickup time via a cron — that transition fires customer_accepted on the driver and offer_auto_accepted on the customer, just like the instant flow's auto-accept path.
WS /ws/customer?token=<firebase_id_token>
Server → Customer
{
"type": "booking_confirmed",
"booking_id": "uuid",
"booking_status": "confirmation",
"scheduled_date": "2026-05-13T08:00:00.000Z",
"no_of_passengers": 2,
"driver_name": "Bob",
"driver_phone": "...",
"driver_phone_code": { ... },
"vehicle_number": "...",
"driver_code": "...",
"pickup_location": { ... },
"dropoff_location": { ... },
"chat_history": []
}
One-to-one chat between the assigned customer and driver, on the same WS sockets. Persisted in the DB table booking_chat_messages — survives server restarts and reconnects. Allowed only while the booking is on_the_way; before that (assignment, offer, confirmation) chat is gated and the server returns BOOKING_NOT_CHATTABLE.
The server hydrates an in-memory cache from the DB on demand. Every booking-state event (assignment, accept, offer, completion, cancellation, etc.) carries a full chat_history snapshot — clients don't need to call resync just to rehydrate chat. Live chat_message events instead carry a trimmed trailing window (~last 100 entries) for bandwidth.
Per-message text length is capped at 1000 characters. When a booking reaches a terminal state (completed, cancelled, driver_cancelled, customer reject), the final event carries a transcript snapshot, then the in-memory cache for that booking is cleared. The DB rows stay for audit.
Send a text message to the other party. Allowed only while the booking is on_the_way and the sender is the booking's customer or driver.
WS /ws/driver OR /ws/customer
Client → Server (then Server fans out to both sides)
{
"type": "chat_message",
"booking_id": "uuid",
"text": "Hi, I'm at the front entrance."
}
{
"type": "chat_message",
"booking_id": "uuid",
"from": "driver" | "customer",
"text": "Hi, I'm at the front entrance.",
"sent_at": "2026-05-12T10:32:15.000Z",
"chat_history": [ ... ] // trailing ~100 messages
}
// On failure:
{ "type": "error", "code": "BOOKING_NOT_CHATTABLE" | "INVALID_PAYLOAD", "message": "..." }
Every booking-detail payload carries the other party's contact info so each side can call the other if needed.
{
...
"customer_name": "Alice",
"customer_phone": "211234567",
"customer_phone_code": {
"id": "uuid",
"name": "New Zealand",
"code": "NZ",
"ph_code": "+64",
"flag": "https://.../uploads/flags/nz.png"
}
}
{
...
"driver_name": "Bob",
"driver_phone": "219876543",
"driver_phone_code": { "id": "...", "name": "...", "code": "...", "ph_code": "+64", "flag": "..." },
"vehicle_number": "ABC-123",
"driver_code": "DRV-0042"
}
Note: phone numbers are stored as local digits only (no country prefix). Combine ph_code + phone when dialling. The country's phone_limit (expected digit count) is exposed on the profile-fetch endpoints but not on these embedded snapshots.
Server's first message after a successful WS handshake. Lets the client confirm auth + know its own ID. The client may also pass ?device_id=<id> in the connect URL; the server bumps that device's last_seen_at so subsequent FCM pushes for the user route to this device.
WS /ws/driver?token=<firebase_id_token>[&device_id=<id>] WS /ws/customer?token=<firebase_id_token>[&device_id=<id>]
Server → Client
// Driver: { "type": "connected", "driver_id": "uuid" } // Customer: { "type": "connected", "customer_id": "uuid" }
After a network drop the client reconnects and asks the server to rebuild its state from DB. Useful when the client missed a push while offline. Both sides accept resync with or without booking_id.
WS /ws/driver OR /ws/customer
Client → Server
// No booking_id — server returns the driver's OLDEST in-progress booking
// (status in requested / confirmation / on_the_way, ordered by created_at ASC).
// Concurrent bookings: only one snapshot is returned today — other active
// bookings are visible only via their original `booking_assigned` events.
{ "type": "resync" }
// With booking_id — server returns that exact booking (must belong to caller): { "type": "resync", "booking_id": "uuid" } // Without booking_id — server returns the customer's MOST RECENTLY // created in-progress booking (ordered by created_at DESC). When the // customer has no active booking, server returns // { type: "snapshot", booking_id: null, booking_status: null }. { "type": "resync" }
{
"type": "snapshot",
"assignment": {
"booking_id": "uuid",
"booking_status": "requested" | "confirmation" | "on_the_way",
"booking_type": "instant" | "scheduled",
"date": "ISO string",
"no_of_passengers": 2,
"arrival_time": { "value": 5, "unit": "minutes", "label": "5 min", "seconds": 300 } | null,
"note": null,
"customer_name": "Alice" | null,
"customer_phone": "..." | null,
"customer_phone_code": { ... } | null,
"pickup_location": { ... },
"dropoff_location": { ... },
"chat_history": [ ... ]
}
}
// `assignment: null` means no active booking.
{
"type": "snapshot",
"booking_id": "uuid",
"booking_status": "...",
"no_of_passengers": 2,
"arrival_time": { "value": 5, "unit": "minutes", "label": "5 min", "seconds": 300 } | null,
"driver_name": "Bob" | null,
"driver_phone": "..." | null,
"driver_phone_code": { ... } | null,
"vehicle_number": "ABC-123" | null,
"driver_code": "DRV-0042" | null,
"pickup_location": { ... },
"dropoff_location": { ... },
"chat_history": [ ... ]
}
Server kicks the socket — typically when admin blocks/deletes the user or kills the session deliberately. Socket terminates immediately after this event.
WS /ws/driver OR /ws/customer
Server → Client
{ "type": "force_disconnect", "reason": "Account blocked." }
App-level keepalive. Server also sends WS-level ping frames every 30 s and terminates sockets that miss a pong.
WS /ws/driver OR /ws/customer
Client → Server
{ "type": "ping" }
{ "type": "pong" }
Any failed action returns one of these. Some errors also terminate the socket (auth failures, session replacement, account blocked).
Server → Client
{ "type": "error", "code": "<CODE>", "message": "human readable" }
UNAUTHORIZED — no token INVALID_TOKEN — bad/expired Firebase token SESSION_REPLACED — same user connected from elsewhere ACCOUNT_BLOCKED — userStatus = "blocked" ACCOUNT_DELETED — userStatus = "deleted" DRIVER_NOT_FOUND — token email isn't tied to a driver CUSTOMER_NOT_FOUND — token email isn't tied to a customer ONGOING_RIDE_ELSEWHERE — driver has an on_the_way ride; blocks new login here INVALID_JSON — message wasn't valid JSON INVALID_PAYLOAD — required field missing/wrong type BOOKING_NOT_FOUND — resync booking_id not yours or doesn't exist BOOKING_NOT_ASSIGNABLE — submit_arrival_time on wrong booking/state BOOKING_NOT_ACCEPTABLE — accept on wrong booking/state BOOKING_NOT_REJECTABLE — reject too late or wrong booking BOOKING_NOT_DECLINABLE — decline_booking on wrong booking/state BOOKING_NOT_COMPLETABLE — mark_as_completed on wrong booking/state BOOKING_NOT_CHATTABLE — chat sent before the booking is on_the_way, or for a booking that isn't yours / has ended REJECT_WINDOW_CLOSED — scheduled reject within 1 hr of pickup INVALID_TEMPLATE — bad cancellation_template_id UNKNOWN_MESSAGE_TYPE — unrecognised `type` INTERNAL_ERROR — unexpected server-side error
requested ──submit_arrival_time / accept_booking──▶ confirmation
│
├──customer accept──▶ on_the_way ──mark_as_completed──▶ completed
│
├──customer reject──▶ cancelled (by "customer")
│
└──⏱ CUSTOMER_WAITING_TIME elapses──▶ on_the_way
(offer_auto_accepted to customer,
customer_accepted{auto_promoted:true} to driver)
requested ──[MAX_ATTEMPTS exhausted: each driver either declined or timed out at DRIVER_RESPONSE_TIME]──▶ cancelled
(by "admin", reason_code="NO_DRIVER_AVAILABLE")
requested ──customer reject────────────────────────────────────────────────────────────────────────▶ cancelled
(by "customer")
on_the_way ──HTTP cancel by active driver──▶ cancelled (by "driver", reason_code="DRIVER_CANCELLED")
on_the_way ──HTTP cancel by customer──▶ cancelled (by "customer"; driver gets booking_cancelled_by_customer)
scheduled ──promotion cron, no driver in confirmation──▶ cancelled
(by "admin", reason_code="SCHEDULED_DRIVER_NO_SHOW")
SESSION_REPLACED.last_seen_at is newest in customer_devices — bumped on WS connect with ?device_id= and on FCM-token refresh. If no device row exists, the push is silently dropped.drivers row.