Booking API
Endpoints for the session booking lifecycle (Phase 3). Covers a mentee discovering follow-up slots and requesting a booking, viewing a booking's detail, paying for a paid session through a checkout page, and a mentor managing the request through its lifecycle.
- Base URL:
/api/v1 - Auth: all endpoints require a Sanctum bearer token —
Authorization: Bearer {token} - Content type:
application/json
Scope note: both free and paid session types are bookable. Free sessions skip straight to booked on confirmation; paid sessions settle through a fake checkout page (see endpoint 5). A real payment gateway (Stripe escrow/capture/refunds) is still deferred — the
PAIDtransition performs no charge.
Response envelope
Every response uses the standard envelope.
Success
{
"status": "success",
"message": "…",
"data": { }
}
Error
{
"status": "error",
"message": "…",
"data": { }
}
Validation errors (422) return field errors in data:
{
"status": "error",
"message": "The primary slot id field is required.",
"data": { "primary_slot_id": ["The primary slot id field is required."] }
}
Status codes
| Code | Meaning |
|---|---|
| 200 | OK (read, lifecycle action) |
| 201 | Booking created |
| 401 | Missing/invalid token |
| 404 | Not found or not owned by the caller |
| 409 | No seat left on the slot (lost a race, or the session filled up before confirmation) |
| 422 | Validation error / invalid state transition / outside follow-up window |
| 500 | Server error |
Enums
Booking status
| Value | Label |
|---|---|
requested |
Requested |
confirmed |
Confirmed |
paid |
Paid (paid sessions, after checkout) |
completed |
Completed |
cancelled |
Cancelled |
refunded |
Refunded (reserved; used once real refunds land) |
Follow-up status: reserved · completed · cancelled · rescheduled
Slot status: available · held · booked · cancelled · vacation_blocked · override_blocked
Shared objects
Booking object
{
"id": "9b6f…",
"status": "requested",
"statusLabel": "Requested",
"isFree": true,
"priceSnapshot": null,
"mentorId": "8a1c…",
"menteeId": "7d2e…",
"mentor": { "…party object…" },
"mentee": { "…party object…" },
"sessionTypeId": "6c3f…",
"sessionType": { "…session-type object…" },
"meetingUrl": null,
"mentorNote": null,
"cancellationReason": null,
"cancelledAt": null,
"completedAt": null,
"primarySlot": { "…slot object…" },
"followups": [ { "…follow-up object…" } ],
"createdAt": "2026-07-01T09:00:00+00:00"
}
The mentor variant of this object also includes
"cancelledBy": "<userId|null>".mentor,mentee,sessionType,primarySlot, andfollowupsare included only when loaded — the list, detail, create, and lifecycle responses each load a relevant subset (the detail endpoints load them all).
meetingUrlis gated on settlement. It is non-null only once this booking is live: a free booking atconfirmed, a paid booking atpaid, or either atcompleted. It isnullatrequested, atconfirmedwhile a paid booking still owes payment, and aftercancelled.The room belongs to the slot, not to the booking, so every mentee on a group session (
max_capacity > 1) gets the samemeetingUrl. A mentee who has not settled seesnulleven when a co-tenant on the same slot already has the room open.
Party object
Compact identity summary for the mentor or mentee on a booking.
{
"profileId": "8a1c…",
"userId": "3f9a…",
"headline": "Senior Product Designer",
"fullName": "Jane Mentor",
"avatarUrl": "https://…",
"timezone": "Asia/Dhaka"
}
Session-type object
What was booked, embedded so a booking row needs no second request.
{
"id": "6c3f…",
"title": "Career Coaching",
"sessionModel": "regular_weekly",
"sessionModelLabel": "Regular Weekly",
"durationMinutes": 60,
"isFree": false,
"price": "90.00",
"currency": "USD",
"followUpCount": 2
}
Follow-up object
{
"id": "5f0a…",
"sequenceNumber": 1,
"status": "reserved",
"statusLabel": "Reserved",
"rescheduleCount": 0,
"slot": { "…slot object…" },
"completedAt": null
}
Slot object
{
"id": "1a2b…",
"mentor_id": "8a1c…",
"session_type_id": "6c3f…",
"availability_rule_id": "4d5e…",
"starts_at": "2026-07-08T09:00:00+00:00",
"ends_at": "2026-07-08T10:00:00+00:00",
"buffer_ends_at": "2026-07-08T10:15:00+00:00",
"held_until": null,
"status": "available",
"status_label": "Available",
"is_selectable": true,
"max_capacity": 1,
"current_capacity": 0,
"available_seats": 1
}
Gate the booking button on
is_selectable(seat-aware), not onstatus. A group slot with free seats readsavailableeven while other mentees hold or occupy seats on it. Full field notes in availability-api.md.
Mentee endpoints
1. List my bookings
GET /api/v1/mentee/bookings
Returns the authenticated mentee's own bookings, newest first.
Query parameters
| Param | Type | Required | Notes |
|---|---|---|---|
status |
string | no | One of the booking-status values above |
per_page |
integer | no | 1–100 (default 15) |
200 Response
{
"status": "success",
"message": "Bookings retrieved successfully.",
"data": {
"bookings": {
"data": [ { "…booking object…" } ],
"links": { "first": "…", "last": "…", "prev": null, "next": null },
"meta": { "current_page": 1, "per_page": 15, "total": 3, "last_page": 1 }
}
}
}
Errors: 401 unauthenticated · 404 no mentee profile · 422 invalid status
2. Get my booking detail
GET /api/v1/mentee/bookings/{id}
Returns one of the mentee's own bookings with everything a detail screen needs — the mentor party, session type, primary slot, and follow-ups all loaded. Owner-scoped: a booking that isn't yours returns 404 (not 403), so ids don't leak.
Path parameters
| Param | Type | Notes |
|---|---|---|
id |
uuid | Booking id — must belong to the authenticated mentee |
200 Response
{
"status": "success",
"message": "Booking retrieved successfully.",
"data": { "booking": { "…booking object (embeds mentor + sessionType)…" } }
}
Errors: 401 unauthenticated · 404 no mentee profile / not found / not the caller's booking
3. Get follow-up slots for a primary slot
GET /api/v1/mentee/bookings/follow-up-slots?primary_slot_id={slotId}
For a chosen primary slot, returns one window per required follow-up
(= session type follow_up_count) with the bookable slots inside each window.
Returns follow_up_count: 0 and an empty windows array when the session type has no
follow-ups.
Query parameters
| Param | Type | Required | Notes |
|---|---|---|---|
primary_slot_id |
uuid | yes | Must exist in availability_slots |
200 Response
{
"status": "success",
"message": "Follow-up slots retrieved successfully.",
"data": {
"follow_up_count": 2,
"windows": [
{
"sequence": 1,
"interval_days": 7,
"flexibility_days": 2,
"earliest_at": "2026-07-13T09:00:00+00:00",
"latest_at": "2026-07-17T09:00:00+00:00",
"slots": [ { "…slot object…" } ]
},
{
"sequence": 2,
"interval_days": 7,
"flexibility_days": 2,
"earliest_at": "2026-07-20T09:00:00+00:00",
"latest_at": "2026-07-24T09:00:00+00:00",
"slots": [ { "…slot object…" } ]
}
]
}
}
Errors: 401 · 422 missing/unknown primary_slot_id, or a one-time session type with
no follow-up interval configured.
4. Create a booking
POST /api/v1/mentee/bookings
Holds the primary slot plus all follow-up slots (10-minute hold) and creates a
REQUESTED booking with its reserved follow-ups. Exactly follow_up_count follow-up
slots must be supplied, and each must fall inside its sequence window (see endpoint 3).
Payload
| Field | Type | Required | Notes |
|---|---|---|---|
primary_slot_id |
uuid | yes | Must exist and be available |
follow_up_slot_ids |
uuid[] | conditionally | Exactly follow_up_count items, distinct, each available and in-window |
{
"primary_slot_id": "1a2b…",
"follow_up_slot_ids": ["3c4d…", "5e6f…"]
}
201 Response
{
"status": "success",
"message": "Booking request submitted successfully.",
"data": { "booking": { "…booking object with primarySlot + followups…" } }
}
Errors
| Code | When |
|---|---|
| 422 | Validation failure; wrong number of follow-ups; a follow-up outside its window; primary has no free seat; the mentee already has a booking on this slot; one-time session without a follow-up interval |
| 409 | One of the requested slots lost its last seat to someone else first |
| 401 | Unauthenticated |
Both free and paid session types are accepted. The booking is created as REQUESTED with
isFree/priceSnapshotsnapshotted from the session type. Paid bookings later settle through the checkout page (endpoint 5).
Group sessions. Booking claims one seat, so on a slot with
max_capacity > 1several mentees each get their own booking on the same slot and all of them join the samemeetingUrl. One mentee cannot take two seats on the same slot — a second attempt returns422("You already have a booking on this slot.") unless the first was cancelled. Cancelling frees exactly that mentee's seat and leaves co-tenants untouched.
5. Get a payment link (paid bookings)
GET /api/v1/mentee/bookings/{id}/payment-link
Issues a signed URL to the checkout page for one of the mentee's own confirmed, paid bookings. The link needs no login — the signature authorises the browser — and is valid for 24 hours. Open it in a browser to reach the Pay / Cancel page.
200 Response
{
"status": "success",
"message": "Payment link generated successfully.",
"data": {
"payment_url": "https://api.example.com/payments/checkout/9b6f…?expires=…&signature=…",
"expires_at": "2026-07-03T09:00:00+00:00"
}
}
The checkout page (GET /payments/checkout/{booking}, web, signed) shows the session,
mentor and price with two actions:
| Action | Route (signed, web) | Effect |
|---|---|---|
| Pay | POST /payments/checkout/{booking}/pay |
CONFIRMED → PAID; this booking's held seats become booked; BookingPaid event |
| Cancel | POST /payments/checkout/{booking}/cancel |
Booking → CANCELLED; this booking's held seats released |
If the session fills up while the checkout page sits open, Pay renders a "Session full" page instead of settling: the booking stays
confirmed, no seat is claimed and the mentee is told they have not been charged. Overbooking is never absorbed (§2.7).
Errors
| Code | When |
|---|---|
| 422 | Booking is free (This booking is free and requires no payment.) or not in confirmed state |
| 404 | Not found / not the caller's booking |
| 401 | Unauthenticated |
| 403 | Checkout/pay/cancel opened with a missing or invalid signature |
6. Cancel my booking
PATCH /api/v1/mentee/bookings/{id}/cancel
Cancels the mentee's own active booking (requested/confirmed). Releases every held/booked slot back to available and marks follow-ups cancelled.
Payload
| Field | Type | Required | Notes |
|---|---|---|---|
reason |
string | no | Max 1000 chars |
{ "reason": "Changed plans" }
200 Response
{
"status": "success",
"message": "Booking cancelled.",
"data": { "booking": { "…booking object…" } }
}
Errors: 401 · 404 not found / not the caller's booking · 422 booking is no longer
active (already completed/cancelled)
Mentor endpoints
7. Booking dashboard
GET /api/v1/mentor/bookings
Returns the authenticated mentor's bookings, newest first. Drive the
Pending / Upcoming / Completed / Cancelled tabs with the status filter
(requested / confirmed / completed / cancelled).
Query parameters
| Param | Type | Required | Notes |
|---|---|---|---|
status |
string | no | Booking-status value |
per_page |
integer | no | 1–100 (default 15) |
200 Response
{
"status": "success",
"message": "Bookings retrieved successfully.",
"data": {
"bookings": {
"data": [ { "…booking object (mentor variant, includes cancelledBy)…" } ],
"links": { "first": "…", "last": "…", "prev": null, "next": null },
"meta": { "current_page": 1, "per_page": 15, "total": 2, "last_page": 1 }
}
}
}
Errors: 401 · 404 no mentor profile · 422 invalid status
8. Get my booking detail
GET /api/v1/mentor/bookings/{id}
Returns one of the mentor's own bookings with everything a detail screen needs — the
mentee party, session type, primary slot, follow-ups, and cancelledBy all loaded.
Owner-scoped: a booking that isn't yours returns 404 (not 403).
Path parameters
| Param | Type | Notes |
|---|---|---|
id |
uuid | Booking id — must belong to the authenticated mentor |
200 Response
{
"status": "success",
"message": "Booking retrieved successfully.",
"data": { "booking": { "…booking object (mentor variant: embeds mentee + sessionType + cancelledBy)…" } }
}
Errors: 401 unauthenticated · 404 no mentor profile / not found / not the caller's booking
9. Confirm a booking
PATCH /api/v1/mentor/bookings/{id}/confirm
Accepts a REQUESTED booking → CONFIRMED. For free sessions the primary and
follow-up slots are booked immediately (claims one capacity unit each) and meetingUrl is
issued. For paid sessions the slots stay held and this booking's hold is extended to
the 24h payment deadline, so its seats remain reserved while the mentee completes checkout
(endpoint 5); the seats are claimed and the room issued at PAID.
Payload: none.
200 Response
{
"status": "success",
"message": "Booking confirmed.",
"data": { "booking": { "…booking object…" } }
}
409 Response — the session filled up while the request was pending:
{
"status": "error",
"message": "This session is now fully booked and can no longer be confirmed.",
"data": { }
}
Handle the 409. A booking sits in
requestedfor up to 24 hours while holds expire after 10 minutes, so another mentee can legitimately take the last seat before the mentor gets round to confirming. Overbooking is refused rather than absorbed (§2.7). Show the mentor that the session is full and leave the booking inrequestedso they can decline it.
Errors: 401 · 404 not the mentor's booking · 409 slot filled up · 422 booking is
not in requested state
10. Decline a booking
PATCH /api/v1/mentor/bookings/{id}/decline
Declines a REQUESTED booking → CANCELLED, releasing its held slots. Functionally a mentor cancel of a pending request.
Payload
| Field | Type | Required | Notes |
|---|---|---|---|
reason |
string | no | Max 1000 chars |
200 Response
{
"status": "success",
"message": "Booking declined.",
"data": { "booking": { "…booking object…" } }
}
Errors: 401 · 404 · 422 booking is no longer active
11. Cancel a booking
PATCH /api/v1/mentor/bookings/{id}/cancel
Cancels an active booking the mentor owns (e.g. after confirming). Releases every held/booked slot and cancels follow-ups.
Payload
| Field | Type | Required | Notes |
|---|---|---|---|
reason |
string | no | Max 1000 chars |
200 Response
{
"status": "success",
"message": "Booking cancelled.",
"data": { "booking": { "…booking object…" } }
}
Errors: 401 · 404 · 422 booking is no longer active
Lifecycle reference
Free session
REQUESTED ──(mentor confirm)──► CONFIRMED ──(session ends, auto)──► COMPLETED
│ │
│(mentor decline / │(mentor/mentee cancel)
│ mentee cancel / ▼
│ 24h auto-expire) ─────────► CANCELLED
Paid session
REQUESTED ─(mentor confirm)─► CONFIRMED ─(mentee pays)─► PAID ─(session ends, auto)─► COMPLETED
│ │ │
│(decline / cancel / │(cancel / checkout │(mentor/mentee cancel)
│ 24h auto-expire) │ cancel) ▼
▼ ▼ CANCELLED
CANCELLED ◄───────────────────────────────────────────────┘
At CONFIRMED a paid booking's slots stay held (hold extended to a 24h payment deadline);
they become booked only at PAID.
Automated transitions (scheduled jobs, not API):
| Job | Schedule | Effect |
|---|---|---|
bookings:auto-complete |
every 30 min | Free confirmed + paid paid bookings (and reserved follow-ups) past their end time → completed; credits total_sessions |
bookings:expire-unconfirmed |
hourly | Requested bookings older than 24h → cancelled (by system), slots released |
slots:release-expired-holds |
every minute | Expired holds → slot back to available |