MeetyyAPI
Documentation / API Reference / Booking

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 PAID transition 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, and followups are included only when loaded — the list, detail, create, and lifecycle responses each load a relevant subset (the detail endpoints load them all).

meetingUrl is gated on settlement. It is non-null only once this booking is live: a free booking at confirmed, a paid booking at paid, or either at completed. It is null at requested, at confirmed while a paid booking still owes payment, and after cancelled.

The room belongs to the slot, not to the booking, so every mentee on a group session (max_capacity > 1) gets the same meetingUrl. A mentee who has not settled sees null even 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 on status. A group slot with free seats reads available even 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 / priceSnapshot snapshotted 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 > 1 several mentees each get their own booking on the same slot and all of them join the same meetingUrl. One mentee cannot take two seats on the same slot — a second attempt returns 422 ("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 requested for 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 in requested so 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