{"openapi":"3.1.0","info":{"title":"JEK Platform API","version":"1.0.0","description":"Unified REST API for the JEK consumer app, merchant dashboard, and admin console. Hosted at https://api.jek.app.\n\nAuthentication uses a Supabase-issued JWT passed as `Authorization: Bearer <token>`. Public endpoints require no auth.","contact":{"name":"JEK Developer Support","url":"https://developer.jek.app"}},"servers":[{"url":"https://api.jek.app","description":"Production"},{"url":"https://jek-beta.app","description":"Beta"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"},"driverBearer":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Driver / device session token (Driver Pro, M1OS)."},"wonderHmac":{"type":"apiKey","in":"header","name":"Authorization","description":"Legacy compatibility scheme: `Wonder-HMAC-SHA256 Credential=<appid>/<yyyyMMdd>/<service>, SignedHeaders=..., Signature=<hex>` sent with `X-JEK-Timestamp` and `X-JEK-Device-Sn`."}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]},"Problem":{"type":"object","description":"RFC 9457 problem details. Branch on `code`, never on `title`.","required":["type","title","status","code"],"properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"code":{"type":"string"},"errors":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"code":{"type":"string"},"message":{"type":"string"}}}}}},"PageMeta":{"type":"object","description":"Cursor pagination metadata returned under `meta`.","properties":{"next_cursor":{"type":"string","nullable":true},"page_size":{"type":"integer","default":25,"maximum":100},"total_count":{"type":"integer"}}},"Money":{"type":"object","description":"Integer minor units plus an ISO 4217 currency code.","required":["amount","currency"],"properties":{"amount":{"type":"integer","example":8850},"currency":{"type":"string","example":"HKD"}}},"Merchant":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"name_en":{"type":"string","nullable":true},"name_zh":{"type":"string","nullable":true},"cuisine":{"type":"string","nullable":true},"address_en":{"type":"string","nullable":true},"image_url":{"type":"string","nullable":true},"logo_url":{"type":"string","nullable":true},"latitude":{"type":"number","nullable":true},"longitude":{"type":"number","nullable":true},"michelin_stars":{"type":"integer","nullable":true}}},"Voucher":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"description":{"type":"string","nullable":true},"discount_type":{"type":"string","enum":["percent","amount"]},"discount_value":{"type":"number"},"starts_at":{"type":"string","format":"date-time"},"ends_at":{"type":"string","format":"date-time"},"image_url":{"type":"string","nullable":true}}},"VehicleType":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"base_fare":{"type":"number"},"per_km":{"type":"number"},"per_min":{"type":"number"}}},"RideQuote":{"type":"object","description":"Response when vehicle_type_id was sent.","properties":{"currency":{"type":"string","example":"HKD"},"fare":{"type":"number"},"vehicle_type":{"$ref":"#/components/schemas/VehicleType"},"quote_ref":{"$ref":"#/components/schemas/QuoteRef"},"expires_at":{"$ref":"#/components/schemas/QuoteExpiry"}}},"QuoteRef":{"type":"string","description":"Signed reference to this quote. Present it as `quote_ref` on POST /api/v1/me/rides and the booking is refused with 409 QUOTE_STALE if the fare has moved since. Optional at both ends; absent when the deployment has no signing secret."},"QuoteExpiry":{"type":"string","format":"date-time","description":"When `quote_ref` stops being accepted. Re-quote past this point."},"ClassQuote":{"type":"object","properties":{"vehicle_type_id":{"type":"string","format":"uuid"},"name":{"type":"string","example":"Express Taxi"},"fare":{"type":"number"},"currency":{"type":"string","example":"HKD"}}},"WithheldClass":{"type":"object","description":"A class priced but NOT offered on this trip, with the floor it did not clear.","properties":{"vehicle_type_id":{"type":"string","format":"uuid"},"name":{"type":"string","example":"85% Meter Taxi"},"fare":{"type":"number","description":"What it would have cost — below `min_fare`."},"currency":{"type":"string","example":"HKD"},"min_fare":{"type":"number","example":90,"description":"The fare this class is not offered below."}}},"RideSelectionQuote":{"type":"object","description":"Response when vehicle_class_ids was sent.","properties":{"currency":{"type":"string","example":"HKD"},"authorize":{"type":"number","description":"The dearest fare among the classes sent — what the passenger pre-authorises."},"dearest":{"$ref":"#/components/schemas/ClassQuote"},"quotes":{"type":"array","items":{"$ref":"#/components/schemas/ClassQuote"}},"withheld":{"type":"array","items":{"$ref":"#/components/schemas/WithheldClass"},"description":"Classes dropped from `quotes` because this trip prices below their minimum fare. Render `quotes`, never these — they are here so the omission can be explained, not shown."},"vehicle_class_limit":{"type":"integer","example":6,"description":"How many classes one booking may accept."},"quote_ref":{"$ref":"#/components/schemas/QuoteRef"},"expires_at":{"$ref":"#/components/schemas/QuoteExpiry"}}},"RideAvailability":{"type":"object","properties":{"supply":{"type":"object","properties":{"status":{"type":"string","enum":["ok","none","unavailable","not_applicable"],"description":"`none` is a measured 'there is no car'; `unavailable` is 'we could not measure'. Neither may be rendered as a reason not to book."},"count":{"type":"integer"},"nearest_m":{"type":"integer","nullable":true},"radius_m":{"type":"integer"},"fix_max_age_s":{"type":"integer"},"reason":{"type":"string","description":"Only on `unavailable`."}}},"eta":{"type":"object","nullable":true,"properties":{"pickup_eta_min":{"type":"integer"},"duration_s":{"type":"integer"},"distance_m":{"type":"integer"},"source":{"type":"string","enum":["provider","estimate"],"description":"The one route actually measured per probe."}}},"classes":{"type":"array","items":{"$ref":"#/components/schemas/ClassAvailability"}},"area":{"type":"object","nullable":true,"properties":{"code":{"type":"string"},"name":{"type":"string"}}},"fulfilment":{"type":"string","nullable":true},"config_version":{"type":"string"},"cached":{"type":"boolean"},"measured_at":{"type":"string","format":"date-time","nullable":true,"description":"When this answer was measured — NOT when it was served. Null when nothing could be measured."},"expires_at":{"type":"string","format":"date-time","nullable":true,"description":"When the cached answer stops being reused."},"age_s":{"type":"integer","nullable":true,"description":"How old the measurement is right now."}}},"ClassAvailability":{"type":"object","properties":{"vehicle_type_id":{"type":"string","format":"uuid"},"name":{"type":"string"},"available":{"type":"boolean","description":"At least one driver in range may serve this class. Informational — never a booking gate."},"count":{"type":"integer"},"nearest_m":{"type":"integer","nullable":true},"eta_min":{"type":"integer","nullable":true},"eta_source":{"type":"string","nullable":true,"enum":["provider","scaled","estimate"],"description":"`provider` — this class's own nearest car was routed. `scaled` — derived from that one measured route by distance ratio. `estimate` — the router did not answer and the base was a straight line."},"eta_confidence":{"type":"string","nullable":true,"enum":["high","medium","low"],"description":"The same fact as a word. Soften how you draw a low-confidence ETA; never disable the class."}}},"Profile":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"email":{"type":"string","nullable":true},"display_name":{"type":"string","nullable":true}}}}},"paths":{"/api/public/v1/health":{"get":{"summary":"Health probe","tags":["Platform"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/public/v1/merchants":{"get":{"summary":"List merchants","tags":["Merchants"],"parameters":[{"name":"q","in":"query","schema":{"type":"string"}},{"name":"cuisine","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","maximum":200,"default":50}}],"responses":{"200":{"description":"Merchant list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Merchant"}}}}}}}}}},"/api/public/v1/merchants/{slug}":{"get":{"summary":"Merchant detail","tags":["Merchants"],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Merchant","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Merchant"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/public/v1/vouchers":{"get":{"summary":"List vouchers","tags":["Promotions"],"parameters":[{"name":"merchant_id","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Voucher list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Voucher"}}}}}}}}}},"/api/public/v1/promotions":{"get":{"summary":"List active promotions","tags":["Promotions"],"responses":{"200":{"description":"Active vouchers","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Voucher"}}}}}}}}}},"/api/public/v1/ride/vehicle-types":{"get":{"summary":"Vehicle types","tags":["Ride"],"description":"The rate card in the order a passenger should read it — Standard, Express, Meter, 85% Meter, Premium, 6-seater, Wheelchair, Extra Luggage, then anything else by name. Render the array as it arrives. A client that must re-sort may only sort by `sort_order` — the operator's ordering from the admin console, which this response already honours — never by name or price.","responses":{"200":{"description":"Vehicle types, in display order","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/VehicleType"}}}}}}}}}},"/api/v1/ride/quote":{"post":{"summary":"Estimate ride fare","tags":["Ride"],"security":[{"bearerAuth":[]}],"description":"Prices one vehicle type (`vehicle_type_id`) or several at once (`vehicle_class_ids`). A quote accepts up to 24 class ids — enough for the whole rate card, so a price list is one call — while a booking (POST /api/v1/me/rides) accepts at most 6; the multi-class response echoes that as `vehicle_class_limit`. `authorize` is the dearest of the classes SENT, so quote the passenger's actual selection before booking rather than reusing a whole-catalogue quote. Every answer carries a signed `quote_ref` and its `expires_at`: send the ref back as `quote_ref` on POST /api/v1/me/rides and the booking is refused with 409 `QUOTE_STALE` rather than charged at a price the passenger never saw. The ref is never a price source — the rate card is still the fare — and it is optional at both ends. A CLASS CAN HAVE A MINIMUM FARE (`85% Meter Taxi` is not offered below HKD 90). On a trip that prices under it the class is DROPPED from `quotes` and listed in `withheld` — draw the price list from `quotes` and the row disappears by itself. When nothing clears its floor, or when `vehicle_type_id` named such a class, the answer is 400 `BELOW_CLASS_MINIMUM`. POST /api/v1/me/rides refuses the same class with the same code rather than dropping it, so a stale screen cannot book one.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["distance_km"],"description":"Exactly one of vehicle_type_id or vehicle_class_ids.","properties":{"vehicle_type_id":{"type":"string","format":"uuid","description":"One class. Omit when sending vehicle_class_ids."},"vehicle_class_ids":{"type":"array","items":{"type":"string","format":"uuid"},"minItems":1,"maxItems":24,"description":"Several classes, priced together. Max 24 per quote; a booking accepts at most 6."},"distance_km":{"type":"number"},"duration_min":{"type":"number"}}}}}},"responses":{"200":{"description":"Quote","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"oneOf":[{"$ref":"#/components/schemas/RideQuote"},{"$ref":"#/components/schemas/RideSelectionQuote"}]}}}}}},"400":{"description":"distance_km missing, no selector, mixed currencies, or more than 24 class ids (the error names the limit)"},"404":{"description":"Unknown vehicle type or class id"}}}},"/api/v1/ride/availability":{"post":{"summary":"Is there a car for this pickup","tags":["Ride"],"security":[{"bearerAuth":[]}],"description":"Supply, per-class availability and pickup ETA for a pickup point. An outage is never a 5xx here: `supply.status` carries `unavailable` with a reason, and a client must show a retry rather than 'no cars'. EVERYTHING IN THIS RESPONSE IS INFORMATIONAL — supply, ETA quality and confidence must never grey out a class or block a booking; `/api/v1/ride/preflight` is the authority on whether a trip may be booked. Answers are cached for ten seconds on a rounded request; `measured_at`, `expires_at` and `age_s` say which answer you are holding, and are null when nothing could be measured.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["pickup"],"properties":{"pickup":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"]},"vehicle_class_ids":{"type":"array","items":{"type":"string","format":"uuid"},"maxItems":24},"radius_m":{"type":"integer","description":"Clamped to the configured bounds."}}}}}},"responses":{"200":{"description":"Availability","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/RideAvailability"}}}}}},"400":{"description":"invalid_coordinates or too_many_classes"},"401":{"description":"AUTH_REQUIRED"},"429":{"description":"rate_limited"}}}},"/api/v1/me/rides/{id}/schedule":{"patch":{"summary":"Reschedule a scheduled ride","tags":["Ride"],"security":[{"bearerAuth":[]}],"description":"Moves an existing scheduled ride to another pickup time. THE FARE DOES NOT MOVE: the rate card has no time-of-day term, so no money is charged or refunded and `quote.repriced` is always false. The new time is validated with the same bounds a booking uses (at least 15 minutes ahead, at most 30 days). A ride already released into the driver pool is withdrawn from it — `status` returns to `scheduled` — provided no driver has accepted it. Only an unclaimed, unfinished, ordinary ride that ALREADY carries a schedule may be moved: this endpoint does not turn a travel-now booking into a scheduled one, and it does not reschedule a wish.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["scheduled_at"],"properties":{"scheduled_at":{"oneOf":[{"type":"integer","format":"int64"},{"type":"string","format":"date-time"}],"description":"Epoch milliseconds, or ISO-8601."}}}}}},"responses":{"200":{"description":"Rescheduled","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","description":"The updated ride row."},"schedule":{"type":"object","properties":{"previous_scheduled_at":{"type":"string","format":"date-time","nullable":true},"scheduled_at":{"type":"string","format":"date-time"},"release_at":{"type":"string","format":"date-time","description":"When the ride rejoins the driver pool."},"status":{"type":"string","example":"scheduled"},"withdrawn_from_pool":{"type":"boolean","description":"True when this call took a live offer back out of the pool."}}},"quote":{"type":"object","properties":{"fare":{"type":"number","description":"What the ride costs, and will still cost."},"currency":{"type":"string"},"repriced":{"type":"boolean","example":false},"card_fare":{"type":"number","nullable":true,"description":"Today's card price for the recorded route. Informational — NOT what is charged."},"class_name":{"type":"string","nullable":true}}}}}}}},"400":{"description":"scheduled_at_required, invalid_scheduled_at, scheduled_at_in_seconds, scheduled_too_soon or scheduled_too_far"},"404":{"description":"Not the caller's ride, or no such ride"},"409":{"description":"ride_finished, not_a_ride, ride_claimed, trip_started, not_reschedulable, not_scheduled, or ride_changed (a driver accepted mid-request)"},"503":{"description":"trip_plan_unavailable — the deployment has not run migration 20260823140000"}}}},"/api/v1/rides":{"get":{"summary":"Current user's ride history","tags":["Ride"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Rides","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object"}}}}}}}}}},"/api/v1/me":{"get":{"summary":"Current user + profile","tags":["Account"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Me","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"profile":{"$ref":"#/components/schemas/Profile"}}}}}}}}}}},"/api/v1/merchant/me":{"get":{"summary":"Signed-in merchant record","tags":["Merchant Dashboard"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Merchant"}}}},"/api/v1/merchant/vouchers":{"get":{"summary":"Vouchers owned by signed-in merchant","tags":["Merchant Dashboard"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Vouchers"}}}},"/api/v1/admin/merchants":{"get":{"summary":"List all merchants (admin)","tags":["Admin"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Merchants"}}}},"/api/v1/admin/users":{"get":{"summary":"List all users (admin)","tags":["Admin"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Users + roles"}}}},"/api/v1/auth/signin/check":{"post":{"summary":"Check whether a phone has a Supabase account","description":"Native Supabase Auth. Responses use {code,message,data}. Success code=200; failures use HTTP 400/401/403/409/429/503. No legacy/Wonder consumer authentication.","tags":["Consumer Auth"],"security":[],"parameters":[{"name":"X-User-Scope","in":"header","schema":{"type":"string","enum":["jek","hk_taxi_go"],"default":"jek"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["phone"],"properties":{"phone":{"type":"string","description":"International phone, e.g. +852 91234567"}}}}}},"responses":{"200":{"description":"Success: {code:200,message:'OK',data:{...}}. Session data includes scope='user' and user_scope."},"400":{"description":"Invalid request or conflicting scopes"},"401":{"description":"Invalid/expired credential or proof"},"403":{"description":"Phone verification or product membership required"},"409":{"description":"Existing accounts need reconciliation"},"429":{"description":"Supabase rate limit"},"503":{"description":"Auth provider, configuration or storage unavailable"}}}},"/api/v1/auth/register":{"post":{"summary":"Send Supabase signup OTP","description":"Native Supabase Auth. Responses use {code,message,data}. Success code=200; failures use HTTP 400/401/403/409/429/503. No legacy/Wonder consumer authentication.","tags":["Consumer Auth"],"security":[],"parameters":[{"name":"X-User-Scope","in":"header","schema":{"type":"string","enum":["jek","hk_taxi_go"],"default":"jek"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["phone"],"properties":{"phone":{"type":"string","description":"International phone, e.g. +852 91234567"},"deviceId":{"type":"string","description":"Optional device identifier; if supplied, reuse it at every step."}}}}}},"responses":{"200":{"description":"Success: {code:200,message:'OK',data:{...}}. Session data includes scope='user' and user_scope."},"400":{"description":"Invalid request or conflicting scopes"},"401":{"description":"Invalid/expired credential or proof"},"403":{"description":"Phone verification or product membership required"},"409":{"description":"Existing accounts need reconciliation"},"429":{"description":"Supabase rate limit"},"503":{"description":"Auth provider, configuration or storage unavailable"}}}},"/api/v1/auth/login":{"post":{"summary":"Sign in with Supabase phone and password","description":"Native Supabase Auth. Responses use {code,message,data}. Success code=200; failures use HTTP 400/401/403/409/429/503. No legacy/Wonder consumer authentication.","tags":["Consumer Auth"],"security":[],"parameters":[{"name":"X-User-Scope","in":"header","schema":{"type":"string","enum":["jek","hk_taxi_go"],"default":"jek"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["phone","password"],"properties":{"phone":{"type":"string","description":"International phone, e.g. +852 91234567"},"password":{"type":"string","description":"New passwords require 8–128 characters and the Supabase password policy."}}}}}},"responses":{"200":{"description":"Success: {code:200,message:'OK',data:{...}}. Session data includes scope='user' and user_scope."},"400":{"description":"Invalid request or conflicting scopes"},"401":{"description":"Invalid/expired credential or proof"},"403":{"description":"Phone verification or product membership required"},"409":{"description":"Existing accounts need reconciliation"},"429":{"description":"Supabase rate limit"},"503":{"description":"Auth provider, configuration or storage unavailable"}}}},"/api/v1/auth/forget-password":{"post":{"summary":"Send password recovery SMS through Supabase","description":"Native Supabase Auth. Responses use {code,message,data}. Success code=200; failures use HTTP 400/401/403/409/429/503. No legacy/Wonder consumer authentication.","tags":["Consumer Auth"],"security":[],"parameters":[{"name":"X-User-Scope","in":"header","schema":{"type":"string","enum":["jek","hk_taxi_go"],"default":"jek"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["phone"],"properties":{"phone":{"type":"string","description":"International phone, e.g. +852 91234567"},"deviceId":{"type":"string","description":"Optional device identifier; if supplied, reuse it at every step."}}}}}},"responses":{"200":{"description":"Success: {code:200,message:'OK',data:{...}}. Session data includes scope='user' and user_scope."},"400":{"description":"Invalid request or conflicting scopes"},"401":{"description":"Invalid/expired credential or proof"},"403":{"description":"Phone verification or product membership required"},"409":{"description":"Existing accounts need reconciliation"},"429":{"description":"Supabase rate limit"},"503":{"description":"Auth provider, configuration or storage unavailable"}}}},"/api/v1/auth/forgot-password":{"post":{"summary":"Alias for forget-password","description":"Native Supabase Auth. Responses use {code,message,data}. Success code=200; failures use HTTP 400/401/403/409/429/503. No legacy/Wonder consumer authentication.","tags":["Consumer Auth"],"security":[],"parameters":[{"name":"X-User-Scope","in":"header","schema":{"type":"string","enum":["jek","hk_taxi_go"],"default":"jek"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["phone"],"properties":{"phone":{"type":"string","description":"International phone, e.g. +852 91234567"},"deviceId":{"type":"string","description":"Optional device identifier; if supplied, reuse it at every step."}}}}}},"responses":{"200":{"description":"Success: {code:200,message:'OK',data:{...}}. Session data includes scope='user' and user_scope."},"400":{"description":"Invalid request or conflicting scopes"},"401":{"description":"Invalid/expired credential or proof"},"403":{"description":"Phone verification or product membership required"},"409":{"description":"Existing accounts need reconciliation"},"429":{"description":"Supabase rate limit"},"503":{"description":"Auth provider, configuration or storage unavailable"}}}},"/api/v1/auth/otp/send":{"post":{"summary":"Send or resend a Supabase SMS OTP","description":"Native Supabase Auth. Responses use {code,message,data}. Success code=200; failures use HTTP 400/401/403/409/429/503. No legacy/Wonder consumer authentication.","tags":["Consumer Auth"],"security":[],"parameters":[{"name":"X-User-Scope","in":"header","schema":{"type":"string","enum":["jek","hk_taxi_go"],"default":"jek"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["phone","action"],"properties":{"phone":{"type":"string","description":"International phone, e.g. +852 91234567"},"action":{"type":"string","description":"signin, signup, or pwdreset"},"deviceId":{"type":"string","description":"Optional device identifier; if supplied, reuse it at every step."}}}}}},"responses":{"200":{"description":"Success: {code:200,message:'OK',data:{...}}. Session data includes scope='user' and user_scope."},"400":{"description":"Invalid request or conflicting scopes"},"401":{"description":"Invalid/expired credential or proof"},"403":{"description":"Phone verification or product membership required"},"409":{"description":"Existing accounts need reconciliation"},"429":{"description":"Supabase rate limit"},"503":{"description":"Auth provider, configuration or storage unavailable"}}}},"/api/v1/auth/otp/verify":{"post":{"summary":"Verify SMS; signin returns session, signup/pwdreset return completion token","description":"Native Supabase Auth. Responses use {code,message,data}. Success code=200; failures use HTTP 400/401/403/409/429/503. No legacy/Wonder consumer authentication.","tags":["Consumer Auth"],"security":[],"parameters":[{"name":"X-User-Scope","in":"header","schema":{"type":"string","enum":["jek","hk_taxi_go"],"default":"jek"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["phone","action","token","aut_code"],"properties":{"phone":{"type":"string","description":"International phone, e.g. +852 91234567"},"action":{"type":"string","description":"signin, signup, or pwdreset"},"token":{"type":"string","description":"Opaque proof from the previous step; bound to product, device and purpose, expires after 10 minutes."},"deviceId":{"type":"string","description":"Optional device identifier; if supplied, reuse it at every step."},"aut_code":{"type":"string"}}}}}},"responses":{"200":{"description":"Success: {code:200,message:'OK',data:{...}}. Session data includes scope='user' and user_scope."},"400":{"description":"Invalid request or conflicting scopes"},"401":{"description":"Invalid/expired credential or proof"},"403":{"description":"Phone verification or product membership required"},"409":{"description":"Existing accounts need reconciliation"},"429":{"description":"Supabase rate limit"},"503":{"description":"Auth provider, configuration or storage unavailable"}}}},"/api/v1/auth/verification":{"post":{"summary":"Alias for otp/verify","description":"Native Supabase Auth. Responses use {code,message,data}. Success code=200; failures use HTTP 400/401/403/409/429/503. No legacy/Wonder consumer authentication.","tags":["Consumer Auth"],"security":[],"parameters":[{"name":"X-User-Scope","in":"header","schema":{"type":"string","enum":["jek","hk_taxi_go"],"default":"jek"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["phone","action","token","aut_code"],"properties":{"phone":{"type":"string","description":"International phone, e.g. +852 91234567"},"action":{"type":"string","description":"signin, signup, or pwdreset"},"token":{"type":"string","description":"Opaque proof from the previous step; bound to product, device and purpose, expires after 10 minutes."},"deviceId":{"type":"string","description":"Optional device identifier; if supplied, reuse it at every step."},"aut_code":{"type":"string"}}}}}},"responses":{"200":{"description":"Success: {code:200,message:'OK',data:{...}}. Session data includes scope='user' and user_scope."},"400":{"description":"Invalid request or conflicting scopes"},"401":{"description":"Invalid/expired credential or proof"},"403":{"description":"Phone verification or product membership required"},"409":{"description":"Existing accounts need reconciliation"},"429":{"description":"Supabase rate limit"},"503":{"description":"Auth provider, configuration or storage unavailable"}}}},"/api/v1/auth/signup/completed":{"post":{"summary":"Set name after phone verification; return Supabase session","description":"Native Supabase Auth. Responses use {code,message,data}. Success code=200; failures use HTTP 400/401/403/409/429/503. No legacy/Wonder consumer authentication.","tags":["Consumer Auth"],"security":[],"parameters":[{"name":"X-User-Scope","in":"header","schema":{"type":"string","enum":["jek","hk_taxi_go"],"default":"jek"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token","first_name","last_name"],"properties":{"token":{"type":"string","description":"Opaque proof from the previous step; bound to product, device and purpose, expires after 10 minutes."},"deviceId":{"type":"string","description":"Optional device identifier; if supplied, reuse it at every step."},"first_name":{"type":"string"},"last_name":{"type":"string"},"password":{"type":"string","description":"New passwords require 8–128 characters and the Supabase password policy."}}}}}},"responses":{"200":{"description":"Success: {code:200,message:'OK',data:{...}}. Session data includes scope='user' and user_scope."},"400":{"description":"Invalid request or conflicting scopes"},"401":{"description":"Invalid/expired credential or proof"},"403":{"description":"Phone verification or product membership required"},"409":{"description":"Existing accounts need reconciliation"},"429":{"description":"Supabase rate limit"},"503":{"description":"Auth provider, configuration or storage unavailable"}}}},"/api/v1/auth/phone/change/start":{"post":{"summary":"Send a code to the number currently on the account","description":"Native Supabase Auth. Responses use {code,message,data}. Success code=200; failures use HTTP 400/401/403/409/429/503. No legacy/Wonder consumer authentication.","tags":["Consumer Auth"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"X-User-Scope","in":"header","schema":{"type":"string","enum":["jek","hk_taxi_go"],"default":"jek"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":[],"properties":{"deviceId":{"type":"string","description":"Optional device identifier; if supplied, reuse it at every step."}}}}}},"responses":{"200":{"description":"Success: {code:200,message:'OK',data:{...}}. Session data includes scope='user' and user_scope."},"400":{"description":"Invalid request or conflicting scopes"},"401":{"description":"Invalid/expired credential or proof"},"403":{"description":"Phone verification or product membership required"},"409":{"description":"Existing accounts need reconciliation"},"429":{"description":"Supabase rate limit"},"503":{"description":"Auth provider, configuration or storage unavailable"}}}},"/api/v1/auth/phone/change/verify":{"post":{"summary":"Verify the current number's code; returns the proof the next step needs","description":"Native Supabase Auth. Responses use {code,message,data}. Success code=200; failures use HTTP 400/401/403/409/429/503. No legacy/Wonder consumer authentication.","tags":["Consumer Auth"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"X-User-Scope","in":"header","schema":{"type":"string","enum":["jek","hk_taxi_go"],"default":"jek"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token","aut_code"],"properties":{"token":{"type":"string","description":"Opaque proof from the previous step; bound to product, device and purpose, expires after 10 minutes."},"aut_code":{"type":"string","description":"The digits from the SMS. Generated, checked and expired by Supabase."},"deviceId":{"type":"string","description":"Optional device identifier; if supplied, reuse it at every step."}}}}}},"responses":{"200":{"description":"Success: {code:200,message:'OK',data:{...}}. Session data includes scope='user' and user_scope."},"400":{"description":"Invalid request or conflicting scopes"},"401":{"description":"Invalid/expired credential or proof"},"403":{"description":"Phone verification or product membership required"},"409":{"description":"Existing accounts need reconciliation"},"429":{"description":"Supabase rate limit"},"503":{"description":"Auth provider, configuration or storage unavailable"}}}},"/api/v1/auth/phone/change/send":{"post":{"summary":"Send a code to the new number; repeatable to resend or correct it","description":"Native Supabase Auth. Responses use {code,message,data}. Success code=200; failures use HTTP 400/401/403/409/429/503. No legacy/Wonder consumer authentication.","tags":["Consumer Auth"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"X-User-Scope","in":"header","schema":{"type":"string","enum":["jek","hk_taxi_go"],"default":"jek"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token","new_phone"],"properties":{"token":{"type":"string","description":"Opaque proof from the previous step; bound to product, device and purpose, expires after 10 minutes."},"new_phone":{"type":"string","description":"The international number to move the account to. Must not already belong to an account."},"deviceId":{"type":"string","description":"Optional device identifier; if supplied, reuse it at every step."}}}}}},"responses":{"200":{"description":"Success: {code:200,message:'OK',data:{...}}. Session data includes scope='user' and user_scope."},"400":{"description":"Invalid request or conflicting scopes"},"401":{"description":"Invalid/expired credential or proof"},"403":{"description":"Phone verification or product membership required"},"409":{"description":"Existing accounts need reconciliation"},"429":{"description":"Supabase rate limit"},"503":{"description":"Auth provider, configuration or storage unavailable"}}}},"/api/v1/auth/phone/change/confirm":{"post":{"summary":"Verify the new number's code; applies the change and returns the session","description":"Native Supabase Auth. Responses use {code,message,data}. Success code=200; failures use HTTP 400/401/403/409/429/503. No legacy/Wonder consumer authentication.","tags":["Consumer Auth"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"X-User-Scope","in":"header","schema":{"type":"string","enum":["jek","hk_taxi_go"],"default":"jek"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token","aut_code"],"properties":{"token":{"type":"string","description":"Opaque proof from the previous step; bound to product, device and purpose, expires after 10 minutes."},"aut_code":{"type":"string","description":"The digits from the SMS. Generated, checked and expired by Supabase."},"deviceId":{"type":"string","description":"Optional device identifier; if supplied, reuse it at every step."}}}}}},"responses":{"200":{"description":"Success: {code:200,message:'OK',data:{...}}. Session data includes scope='user' and user_scope."},"400":{"description":"Invalid request or conflicting scopes"},"401":{"description":"Invalid/expired credential or proof"},"403":{"description":"Phone verification or product membership required"},"409":{"description":"Existing accounts need reconciliation"},"429":{"description":"Supabase rate limit"},"503":{"description":"Auth provider, configuration or storage unavailable"}}}},"/api/v1/auth/password/signin":{"post":{"summary":"Alias for login","description":"Native Supabase Auth. Responses use {code,message,data}. Success code=200; failures use HTTP 400/401/403/409/429/503. No legacy/Wonder consumer authentication.","tags":["Consumer Auth"],"security":[],"parameters":[{"name":"X-User-Scope","in":"header","schema":{"type":"string","enum":["jek","hk_taxi_go"],"default":"jek"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["phone","password"],"properties":{"phone":{"type":"string","description":"International phone, e.g. +852 91234567"},"password":{"type":"string","description":"New passwords require 8–128 characters and the Supabase password policy."}}}}}},"responses":{"200":{"description":"Success: {code:200,message:'OK',data:{...}}. Session data includes scope='user' and user_scope."},"400":{"description":"Invalid request or conflicting scopes"},"401":{"description":"Invalid/expired credential or proof"},"403":{"description":"Phone verification or product membership required"},"409":{"description":"Existing accounts need reconciliation"},"429":{"description":"Supabase rate limit"},"503":{"description":"Auth provider, configuration or storage unavailable"}}}},"/api/v1/auth/password/reset":{"post":{"summary":"Spend recovery proof and update Supabase password","description":"Native Supabase Auth. Responses use {code,message,data}. Success code=200; failures use HTTP 400/401/403/409/429/503. No legacy/Wonder consumer authentication.","tags":["Consumer Auth"],"security":[],"parameters":[{"name":"X-User-Scope","in":"header","schema":{"type":"string","enum":["jek","hk_taxi_go"],"default":"jek"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token","password"],"properties":{"token":{"type":"string","description":"Opaque proof from the previous step; bound to product, device and purpose, expires after 10 minutes."},"deviceId":{"type":"string","description":"Optional device identifier; if supplied, reuse it at every step."},"password":{"type":"string","description":"New passwords require 8–128 characters and the Supabase password policy."}}}}}},"responses":{"200":{"description":"Success: {code:200,message:'OK',data:{...}}. Session data includes scope='user' and user_scope."},"400":{"description":"Invalid request or conflicting scopes"},"401":{"description":"Invalid/expired credential or proof"},"403":{"description":"Phone verification or product membership required"},"409":{"description":"Existing accounts need reconciliation"},"429":{"description":"Supabase rate limit"},"503":{"description":"Auth provider, configuration or storage unavailable"}}}},"/api/v1/auth/session/refresh":{"post":{"summary":"Rotate the Supabase session","description":"Native Supabase Auth. Responses use {code,message,data}. Success code=200; failures use HTTP 400/401/403/409/429/503. No legacy/Wonder consumer authentication.","tags":["Consumer Auth"],"security":[],"parameters":[{"name":"X-User-Scope","in":"header","schema":{"type":"string","enum":["jek","hk_taxi_go"],"default":"jek"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["refresh_token"],"properties":{"refresh_token":{"type":"string"}}}}}},"responses":{"200":{"description":"Success: {code:200,message:'OK',data:{...}}. Session data includes scope='user' and user_scope."},"400":{"description":"Invalid request or conflicting scopes"},"401":{"description":"Invalid/expired credential or proof"},"403":{"description":"Phone verification or product membership required"},"409":{"description":"Existing accounts need reconciliation"},"429":{"description":"Supabase rate limit"},"503":{"description":"Auth provider, configuration or storage unavailable"}}}},"/api/v1/auth/session/logout":{"post":{"summary":"Revoke the current Supabase refresh session","description":"Native Supabase Auth. Responses use {code,message,data}. Success code=200; failures use HTTP 400/401/403/409/429/503. No legacy/Wonder consumer authentication.","tags":["Consumer Auth"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"X-User-Scope","in":"header","schema":{"type":"string","enum":["jek","hk_taxi_go"],"default":"jek"}}],"responses":{"200":{"description":"Success: {code:200,message:'OK',data:{...}}. Session data includes scope='user' and user_scope."},"400":{"description":"Invalid request or conflicting scopes"},"401":{"description":"Invalid/expired credential or proof"},"403":{"description":"Phone verification or product membership required"},"409":{"description":"Existing accounts need reconciliation"},"429":{"description":"Supabase rate limit"},"503":{"description":"Auth provider, configuration or storage unavailable"}}}},"/api/v1/auth/profile":{"get":{"summary":"Read authenticated consumer profile","description":"Native Supabase Auth. Responses use {code,message,data}. Success code=200; failures use HTTP 400/401/403/409/429/503. No legacy/Wonder consumer authentication.","tags":["Consumer Auth"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"X-User-Scope","in":"header","schema":{"type":"string","enum":["jek","hk_taxi_go"],"default":"jek"}}],"responses":{"200":{"description":"Success: {code:200,message:'OK',data:{...}}. Session data includes scope='user' and user_scope."},"400":{"description":"Invalid request or conflicting scopes"},"401":{"description":"Invalid/expired credential or proof"},"403":{"description":"Phone verification or product membership required"},"409":{"description":"Existing accounts need reconciliation"},"429":{"description":"Supabase rate limit"},"503":{"description":"Auth provider, configuration or storage unavailable"}}},"patch":{"summary":"Update consumer first and last name","description":"Native Supabase Auth. Responses use {code,message,data}. Success code=200; failures use HTTP 400/401/403/409/429/503. No legacy/Wonder consumer authentication.","tags":["Consumer Auth"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"X-User-Scope","in":"header","schema":{"type":"string","enum":["jek","hk_taxi_go"],"default":"jek"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["first_name","last_name"],"properties":{"first_name":{"type":"string"},"last_name":{"type":"string"}}}}}},"responses":{"200":{"description":"Success: {code:200,message:'OK',data:{...}}. Session data includes scope='user' and user_scope."},"400":{"description":"Invalid request or conflicting scopes"},"401":{"description":"Invalid/expired credential or proof"},"403":{"description":"Phone verification or product membership required"},"409":{"description":"Existing accounts need reconciliation"},"429":{"description":"Supabase rate limit"},"503":{"description":"Auth provider, configuration or storage unavailable"}}}},"/api/v1/auth/scopes":{"get":{"summary":"Read the consumer's product memberships","description":"Native Supabase Auth. Responses use {code,message,data}. Success code=200; failures use HTTP 400/401/403/409/429/503. No legacy/Wonder consumer authentication.","tags":["Consumer Auth"],"security":[{"bearerAuth":[]}],"parameters":[{"name":"X-User-Scope","in":"header","schema":{"type":"string","enum":["jek","hk_taxi_go"],"default":"jek"}}],"responses":{"200":{"description":"Success: {code:200,message:'OK',data:{...}}. Session data includes scope='user' and user_scope."},"400":{"description":"Invalid request or conflicting scopes"},"401":{"description":"Invalid/expired credential or proof"},"403":{"description":"Phone verification or product membership required"},"409":{"description":"Existing accounts need reconciliation"},"429":{"description":"Supabase rate limit"},"503":{"description":"Auth provider, configuration or storage unavailable"}}}}}}