🚕 Highway Taxi — WS Booking Flow Test Console

🚗 Driver App
disconnected
/ws/driver
Active Assignment
No booking assigned yet
Driver Actions
Driver Log
💬 Chat with Customer
No messages yet.
🧍 Customer App
disconnected
/ws/customer
Step 1 — Create Booking (REST)
Active Booking
No booking yet — create one above
Customer Log
💬 Chat with Driver
No messages yet.

📘 Booking Flow — Reference

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.

1Request Booking (REST)

Customer creates a new booking. Server picks a driver, inserts the row, then pushes booking_assigned on the driver's socket.

URL

POST /api/mobile/customer/booking/request
Authorization: Bearer <firebase_id_token>
Content-Type:  application/json

Payload

{
  "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
    }
  ]
}

Success response (201)

{
  "meta": { "code": 201, "status": "success", "timestamp": "..." },
  "code": "CREATED",
  "data": { "booking_id": "uuid", "driver_id": "uuid" },
  "user_message": { "message": "Booking request submitted successfully." }
}

2booking_assigned

Server tells the assigned driver about the new ride. Fired automatically after Step 1.

URL

WS  /ws/driver?token=<firebase_id_token>

Direction

Server → Driver

Response (server pushes)

{
  "type":             "booking_assigned",
  "booking_id":       "uuid",
  "booking_status":   "requested",
  "booking_type":     "instant",
  "date":             "2026-05-12T10:30:00.000Z",
  "no_of_passengers": 2,
  "pickup_location": {
    "title":     "Home",
    "address":   "12 King St, Sydney NSW",
    "map_id":    "ChIJP3Sa8ziYEmsRUKgyFmh9AQM",
    "latitude":  -33.8688,
    "longitude": 151.2093
  },
  "dropoff_location": {
    "title":     "Sydney Airport",
    "address":   "Mascot NSW 2020",
    "map_id":    "ChIJ-aOOcQO6EmsRUaff7uXNFnQ",
    "latitude":  -33.9399,
    "longitude": 151.1753
  }
}

3submit_arrival_time

Driver accepts an instant booking by sending how many seconds until pickup. DB: arrival_time = N, booking_status = "confirmation". Triggers Step 4. Starts the customer-response timer (default 2 min — configurable via the CUSTOMER_WAITING_TIME env var, in seconds).

URL

WS  /ws/driver?token=<firebase_id_token>

Direction

Driver → Server

Payload

{
  "type":         "submit_arrival_time",
  "booking_id":   "uuid",
  "arrival_time": 300
}

Response

// Driver gets ack (socket stays open, UI flips status to "confirmation"):
{
  "type":           "arrival_time_accepted",
  "booking_id":     "uuid",
  "booking_status": "confirmation",
  "arrival_time": {
    "value":   5,
    "unit":    "minutes",
    "label":   "5 min",
    "seconds": 300
  }
}

// Customer gets `arrival_time_offered` (Step 4).

// On failure:
{ "type": "error", "code": "BOOKING_NOT_ASSIGNABLE" | "INVALID_PAYLOAD", "message": "..." }

4arrival_time_offered

Server forwards the driver's arrival time to the customer with a friendly display shape.

URL

WS  /ws/customer?token=<firebase_id_token>

Direction

Server → Customer

Response (server pushes)

{
  "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,
  "pickup_location": {
    "title":     "Home",
    "address":   "12 King St, Sydney NSW",
    "map_id":    "ChIJP3Sa8ziYEmsRUKgyFmh9AQM",
    "latitude":  -33.8688,
    "longitude": 151.2093
  },
  "dropoff_location": {
    "title":     "Sydney Airport",
    "address":   "Mascot NSW 2020",
    "map_id":    "ChIJ-aOOcQO6EmsRUaff7uXNFnQ",
    "latitude":  -33.9399,
    "longitude": 151.1753
  }
}

5accept

Customer confirms the offer. DB: booking_status = "on_the_way". Cancels the customer-response timer. Triggers Step 6.

URL

WS  /ws/customer?token=<firebase_id_token>

Direction

Customer → Server

Payload

{ "type": "accept", "booking_id": "uuid" }

Response

// Customer gets ack (socket stays open for the ride):
{ "type": "accept_accepted", "booking_id": "uuid", "booking_status": "on_the_way" }

// Driver gets `customer_accepted` (Step 6).

// On failure:
{ "type": "error", "code": "BOOKING_NOT_ACCEPTABLE", "message": "..." }

6customer_accepted

Server tells the driver the ride is on. Includes the customer's name.

URL

WS  /ws/driver?token=<firebase_id_token>

Direction

Server → Driver

Response (server pushes)

{
  "type":             "customer_accepted",
  "booking_id":       "uuid",
  "booking_status":   "on_the_way",
  "no_of_passengers": 2,
  "customer_name":    "Alice",
  "pickup_location": {
    "title":     "Home",
    "address":   "12 King St, Sydney NSW",
    "map_id":    "ChIJP3Sa8ziYEmsRUKgyFmh9AQM",
    "latitude":  -33.8688,
    "longitude": 151.2093
  },
  "dropoff_location": {
    "title":     "Sydney Airport",
    "address":   "Mascot NSW 2020",
    "map_id":    "ChIJ-aOOcQO6EmsRUaff7uXNFnQ",
    "latitude":  -33.9399,
    "longitude": 151.1753
  }
}

7mark_as_completed

Driver ends the ride. DB: booking_status = "completed". Triggers Step 8.

URL

WS  /ws/driver?token=<firebase_id_token>

Direction

Driver → Server

Payload

{ "type": "mark_as_completed", "booking_id": "uuid", "status": "completed" }

Response

// Driver gets ack (socket stays open for the next assignment):
{ "type": "complete_accepted", "booking_id": "uuid", "status": "completed" }

// Customer gets `ride_completed` (Step 8), socket closes 200ms later.

// On failure:
{ "type": "error", "code": "BOOKING_NOT_COMPLETABLE", "message": "..." }

8ride_completed

Server tells the customer the ride is finished. Customer socket auto-closes ~200ms later.

URL

WS  /ws/customer?token=<firebase_id_token>

Direction

Server → Customer

Response (server pushes)

{ "type": "ride_completed", "booking_id": "uuid", "status": "completed" }

↪ Branch events (not part of the happy path)

decline_booking

Driver refuses a requested booking. Each booking gets up to 3 driver attempts total (initial + 2 retries), with cumulative exclusion — no driver is asked twice. An attempt is consumed by an explicit decline, no response within the 30-second driver-response window, 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" and customer gets driver_cancelled with reason_code: "NO_DRIVER_AVAILABLE"; declining driver gets { reassigned: false }.

URL

WS  /ws/driver?token=<firebase_id_token>

Direction

Driver → Server

Payload

{
  "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
}

Response

{ "type": "decline_accepted", "booking_id": "uuid", "reassigned": true | false }

driver_cancelled

Server tells the customer the booking ran out of driver-assignment attempts — i.e. up to 3 drivers were tried and the last one either declined, didn't respond within the 30-second window, or no eligible driver was left. A single decline is never surfaced; only the terminal cancellation is. Customer socket auto-closes ~200ms later.

URL

WS  /ws/customer?token=<firebase_id_token>

Direction

Server → Customer

Response (server pushes)

{
  "type":        "driver_cancelled",
  "booking_id":  "uuid",
  "status":      "cancelled",
  "reason":      "No driver available after multiple attempts.",
  "reason_code": "NO_DRIVER_AVAILABLE"
}

reject

Customer cancels their own booking. 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.

URL

WS  /ws/customer?token=<firebase_id_token>

Direction

Customer → Server

Payload

{ "type": "reject", "booking_id": "uuid" }

Response

// Customer gets ack, then socket terminates ~200ms later:
{ "type": "reject_accepted", "booking_id": "uuid", "status": "cancelled" }

// Driver gets:
{ "type": "customer_rejected", "booking_id": "uuid", "status": "cancelled" }

// On failure:
{ "type": "error", "code": "REJECT_WINDOW_CLOSED" | "BOOKING_NOT_REJECTABLE", "message": "..." }

offer_auto_accepted (⏱ customer-timeout auto-accept)

If the customer doesn't accept or reject within the customer-response window (default 2 min, configurable via CUSTOMER_WAITING_TIME) 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.

URL

WS  /ws/customer?token=<firebase_id_token>
WS  /ws/driver?token=<firebase_id_token>

Direction

Server → Customer  AND  Server → Driver

Response (server pushes)

// Customer side (socket stays open):
{
  "type":           "offer_auto_accepted",
  "booking_id":     "uuid",
  "booking_status": "on_the_way"
}

// Driver side — same shape as a manual customer_accepted, plus auto_accepted flag:
{
  "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":  { ... },
  "auto_accepted":     true
}

accept_booking (scheduled only)

Driver accepts a scheduled booking. No arrival time involved. Triggers booking_confirmed on the customer side.

URL

WS  /ws/driver?token=<firebase_id_token>

Direction

Driver → Server

Payload

{ "type": "accept_booking", "booking_id": "uuid" }

Response

{ "type": "accept_accepted", "booking_id": "uuid" }

booking_confirmed (scheduled only)

Server tells the customer their scheduled booking has been confirmed by a driver.

URL

WS  /ws/customer?token=<firebase_id_token>

Direction

Server → Customer

Response (server pushes)

{
  "type":             "booking_confirmed",
  "booking_id":       "uuid",
  "booking_status":   "confirmation",
  "scheduled_date":   "2026-05-13T08:00:00.000Z",
  "no_of_passengers": 2,
  "driver_name":      "Bob",
  "pickup_location": {
    "title":     "Home",
    "address":   "12 King St, Sydney NSW",
    "map_id":    "ChIJP3Sa8ziYEmsRUKgyFmh9AQM",
    "latitude":  -33.8688,
    "longitude": 151.2093
  },
  "dropoff_location": {
    "title":     "Sydney Airport",
    "address":   "Mascot NSW 2020",
    "map_id":    "ChIJ-aOOcQO6EmsRUaff7uXNFnQ",
    "latitude":  -33.9399,
    "longitude": 151.1753
  }
}

💬 Chat (per-booking)

One-to-one chat between the assigned customer and driver, on the same WS sockets. Only available while the booking is on_the_way — before that (driver assigned, arrival-time offer, etc.) chat is disabled. Stored in process memory only — no DB. Chat history is wiped the moment the booking enters a terminal state (completed, cancelled, driver_cancelled, customer reject). On reconnect, the latest history is delivered as part of the snapshot via resync. Bounded to 200 messages × 1000 chars per message.

chat_message

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.

URL

WS  /ws/driver  OR  /ws/customer

Direction

Client → Server  (then Server fans out to both sides)

Payload

{
  "type":       "chat_message",
  "booking_id": "uuid",
  "text":       "Hi, I'm at the front entrance."
}

Response (server pushes — same payload to both sides)

{
  "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"
}

// On failure:
{ "type": "error", "code": "BOOKING_NOT_CHATTABLE" | "INVALID_PAYLOAD", "message": "..." }

Chat history on resync

The snapshot sent in response to resync includes the in-memory chat_history array, so a reconnecting client can rehydrate the conversation:

{
  "type": "snapshot",
  ...
  "chat_history": [
    { "from": "customer", "text": "I'm at the gate.", "sent_at": "..." },
    { "from": "driver",   "text": "Two minutes away.", "sent_at": "..." }
  ]
}

📞 Contact info in booking payloads

Every booking-detail payload now carries the other party's phone so each side can call the other if needed.

Driver-facing payloads (booking_assigned, customer_accepted, snapshot)

{
  ...
  "customer_name":  "Alice",
  "customer_phone": "+64211234567",
  "customer_phone_code": {
    "id":      "nz000000-0000-0000-0000-000000000000",
    "name":    "New Zealand",
    "code":    "NZ",
    "ph_code": "+64",
    "flag":    "https://.../uploads/flags/nz.png"
  }
}

Customer-facing payloads (arrival_time_offered, booking_confirmed, snapshot)

{
  ...
  "driver_name":  "Bob",
  "driver_phone": "+64219876543",
  "driver_phone_code": { "id": "...", "name": "...", "code": "...", "ph_code": "+64", "flag": "..." }
}

🔌 Utility messages

connected (greeting)

Server's first message after a successful WS handshake. Lets the client confirm auth + know its own ID.

URL

WS  /ws/driver  OR  /ws/customer

Direction

Server → Client

Response (server pushes)

// Driver:
{ "type": "connected", "driver_id": "uuid" }

// Customer:
{ "type": "connected", "customer_id": "uuid" }

resync (after reconnect)

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.

URL

WS  /ws/driver  OR  /ws/customer

Direction

Client → Server

Payload

// Driver — server finds its most-recent active booking automatically:
{ "type": "resync" }

// Customer — must include booking_id:
{ "type": "resync", "booking_id": "uuid" }

Response

// Driver:
{
  "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,
    "customer_name":    "Alice" | null,
    "pickup_location": {
      "title":     "Home",
      "address":   "12 King St, Sydney NSW",
      "map_id":    "ChIJP3Sa8ziYEmsRUKgyFmh9AQM",
      "latitude":  -33.8688,
      "longitude": 151.2093
    },
    "dropoff_location": {
      "title":     "Sydney Airport",
      "address":   "Mascot NSW 2020",
      "map_id":    "ChIJ-aOOcQO6EmsRUaff7uXNFnQ",
      "latitude":  -33.9399,
      "longitude": 151.1753
    }
  }
}
// `assignment: null` means no active booking.

// Customer:
{
  "type":             "snapshot",
  "booking_id":       "uuid",
  "booking_status":   "...",
  "no_of_passengers": 2,
  "arrival_time":     { "value": 5, "unit": "minutes", "label": "5 min", "seconds": 300 } | null,
  "pickup_location": {
    "title":     "Home",
    "address":   "12 King St, Sydney NSW",
    "map_id":    "ChIJP3Sa8ziYEmsRUKgyFmh9AQM",
    "latitude":  -33.8688,
    "longitude": 151.2093
  },
  "dropoff_location": {
    "title":     "Sydney Airport",
    "address":   "Mascot NSW 2020",
    "map_id":    "ChIJ-aOOcQO6EmsRUaff7uXNFnQ",
    "latitude":  -33.9399,
    "longitude": 151.1753
  }
}

ping / pong

App-level keepalive. Server also sends WS-level ping frames every 30 s and terminates sockets that miss a pong.

URL

WS  /ws/driver  OR  /ws/customer

Direction

Client → Server

Payload

{ "type": "ping" }

Response

{ "type": "pong" }

error

Any failed action returns one of these. Some errors also terminate the socket (auth failures, session replacement).

Direction

Server → Client

Response (server pushes)

{ "type": "error", "code": "<CODE>", "message": "human readable" }

Common codes

UNAUTHORIZED            — no token
INVALID_TOKEN           — bad/expired Firebase token
SESSION_REPLACED        — same user connected from elsewhere
INVALID_JSON            — message wasn't valid JSON
INVALID_PAYLOAD         — required field missing/wrong type
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
REJECT_WINDOW_CLOSED    — scheduled reject within 1hr of pickup
INVALID_TEMPLATE        — bad cancellation_template_id
UNKNOWN_MESSAGE_TYPE    — unrecognised `type`
INTERNAL_ERROR          — unexpected server-side error while handling the message
BOOKING_NOT_CHATTABLE   — chat sent before the booking is on_the_way, or for a booking that isn't yours / has ended

📊 Status machine

requested  ──submit_arrival_time / accept_booking──▶  confirmation
                                                          │
                                                          ├──customer accept──▶  on_the_way  ──mark_as_completed──▶  completed
                                                          │
                                                          ├──customer reject──▶  cancelled  (by "customer")
                                                          │
                                                          └──⏱ customer timeout─▶  on_the_way  (auto-accepted; window from CUSTOMER_WAITING_TIME, default 2 min)

requested  ──[3rd driver attempt declines / times out / no driver left]─▶  cancelled  (by "admin", reason_code="NO_DRIVER_AVAILABLE")
requested  ──customer reject────────────────────────────────────────────▶  cancelled  (by "customer")