MeetyyAPI
Documentation / API Reference / Availability

Availability API

Endpoints for the availability engine (Phase 2). Covers a mentor declaring when they are bookable (availability rules), blocking time out (off-days, vacation mode), the public discovery of precomputed slots, and the short-lived hold a mentee places on slots before committing to a booking.

  • Base URL: /api/v1
  • Auth: Sanctum bearer token — Authorization: Bearer {token}except the two /public/mentors/… endpoints, which are unauthenticated
  • Content type: application/json

Scope note: slots are precomputed, not calculated on read. Rules describe intent; a background job (GenerateMentorSlotsJob) expands them into concrete rows in availability_slots over a rolling 90-day horizon. Every mutation below dispatches a regeneration — so a slot list may lag a rule edit by the queue's latency, not by a request.

Key-casing warning: this module is inconsistent and it is load-bearing for clients. Availability rules and off-days return camelCase keys; slots and session types return snake_case keys. Both shapes are documented below verbatim.


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": "Day of week is required for weekly rules.",
  "data": { "day_of_week": ["Day of week is required for weekly rules."] }
}

Status codes

Code Meaning
200 OK (read, update, activate, deactivate, hold)
201 Rule or off-day created
401 Missing/invalid token
404 No mentor profile, or resource not found / not owned by the caller
409 One or more slots are no longer holdable (lost a race)
422 Validation error
500 Server error

Enums

Availability rule type — also the slot-invalidation priority (lower wins).

Value Label Priority
regular_weekly Regular Weekly 4
recurring Recurring 3
one_time One Time 2

Vacation blocking outranks all three (priority 1).

Slot status

Value Label Selectable
available Available
held Being Booked
booked Booked
cancelled Cancelled
vacation_blocked Vacation Blocked
override_blocked Unavailable
overwrite_blocked Overwrite Blocked

Slot reads return every status except cancelled. Clients must gate selection on is_selectable, not on presence in the list — held and booked slots are returned deliberately so a calendar can render them as taken.

available / held / booked are derived from seat counts, not authored. They are recomputed on every seat change: booked once all seats are claimed, held once claimed plus in-checkout seats fill the slot, available otherwise. The other four statuses are authored by the mentor (or by vacation mode) and always win.

For a group slot (max_capacity > 1) this means a slot with seats left reads available even while other mentees hold or occupy seats on it. Never infer "taken" from the status alone on a group slot — use is_selectable and available_seats.

Day of week: 0 = Sunday … 6 = Saturday.


Shared objects

Availability rule object

{
  "id": "9b6f…",
  "mentorId": "8a1c…",
  "sessionTypeId": null,
  "ruleType": "regular_weekly",
  "ruleTypeLabel": "Regular Weekly",
  "dayOfWeek": 1,
  "timeBlocks": [ { "start_time": "09:00", "end_time": "12:00" } ],
  "rrule": null,
  "specificDate": null,
  "validFrom": "2026-07-01",
  "validUntil": null,
  "isActive": true,
  "createdAt": "2026-07-01T09:00:00+00:00",
  "updatedAt": "2026-07-01T09:00:00+00:00"
}

sessionTypeId: null means the rule applies to every session type the mentor offers. dayOfWeek is set only for regular_weekly, rrule only for recurring, and specificDate only for one_time — the other two are null in each case. timeBlocks is populated for all three rule types.

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
}

Seats. max_capacity is the seat count (1 = one-on-one, >1 = group session), current_capacity the seats claimed by settled bookings, and available_seats what is left after claimed and in-checkout seats are subtracted. is_selectable is available_seats > 0 on an unblocked slot — gate the booking button on it, and show available_seats on any slot where max_capacity > 1.

held_until is the latest hold expiry on the slot and is a display hint only. On a group slot several mentees can be in checkout at once, so it does not tell you how many seats are reserved — available_seats does.

All timestamps are UTC. buffer_ends_at is ends_at plus the session type's buffer_after_minutes — the mentor is occupied until then, so overlap checks use the buffer, not ends_at. This is the same slot object the Booking and Messaging APIs embed.

Off-day object

{
  "id": "3c7d…",
  "mentorId": "8a1c…",
  "offFrom": "2026-08-01",
  "offUntil": "2026-08-05",
  "reason": "Conference",
  "isFullDay": true,
  "offTimeBlocks": null,
  "isVacation": false,
  "createdAt": "2026-07-01T09:00:00+00:00",
  "updatedAt": "2026-07-01T09:00:00+00:00"
}

Session-type object

{
  "id": "6c3f…",
  "mentor_id": "8a1c…",
  "title": "Career Coaching",
  "description": "…",
  "session_model": "regular_weekly",
  "session_model_label": "Regular Weekly",
  "duration_minutes": 60,
  "buffer_after_minutes": 15,
  "is_free": false,
  "price": "90.00",
  "currency": "USD",
  "max_capacity": 1,
  "follow_up_count": 2,
  "follow_up_interval_days": 7,
  "follow_up_flexibility_days": 2,
  "is_active": true,
  "created_at": "2026-07-01T09:00:00+00:00",
  "updated_at": "2026-07-01T09:00:00+00:00"
}

Mentor endpoints — availability rules

All require auth:sanctum and a mentor profile. A caller without one gets 404 "Mentor profile not found."

Rules are owner-scoped: a rule id that does not exist, and one belonging to another mentor, are indistinguishable — both return 404 "Availability rule not found." on every handler (show, update, activate, deactivate), so ids don't leak. A genuine server fault is still a 500.

1. List availability rules

GET /api/v1/mentor/availability-rules

Returns the authenticated mentor's rules, newest first.

Query parameters

Param Type Required Notes
is_active boolean no Filter by active state — see caveat
session_type uuid no Only rules bound to this session type

Caveat: the is_active filter is applied only when the value is truthy. Passing is_active=0 or is_active=false does not return inactive rules — it returns all of them. To list deactivated rules, fetch unfiltered and filter client-side.

200 Response

{
  "status": "success",
  "message": "Availability rules retrieved successfully.",
  "data": { "availability_rules": [ { "…availability rule object…" } ] }
}

Errors: 401 unauthenticated · 404 no mentor profile · 500 server error


2. Create an availability rule

POST /api/v1/mentor/availability-rules

Creates one rule and dispatches slot regeneration.

Body

Field Type Required Rules
session_type_id uuid no Must exist in session_types. Omit or null → applies to all session types
rule_type string yes regular_weekly · recurring · one_time
day_of_week integer conditional Required when rule_type=regular_weekly. 06
time_blocks array yes At least 1 block, for every rule type
time_blocks.*.start_time string yes H:i (24-hour, e.g. 09:00)
time_blocks.*.end_time string yes H:i, must be after its own start_time
rrule string conditional Required when rule_type=recurring. iCal RRULE, max 500 chars
specific_date date conditional Required when rule_type=one_time. Today or later
valid_from date no Defaults to today when omitted
valid_until date no Must be on/after valid_from. null = open-ended

Times in time_blocks are interpreted in the mentor's own timezone (mentor_profiles.timezone, falling back to UTC) and stored as UTC on the generated slots.

Example request

{
  "rule_type": "regular_weekly",
  "day_of_week": 1,
  "time_blocks": [
    { "start_time": "09:00", "end_time": "12:00" },
    { "start_time": "14:00", "end_time": "17:00" }
  ],
  "valid_from": "2026-08-01"
}

Recurring example

{
  "rule_type": "recurring",
  "rrule": "FREQ=WEEKLY;BYDAY=TU,TH;INTERVAL=2",
  "time_blocks": [ { "start_time": "10:00", "end_time": "13:00" } ]
}

201 Response

{
  "status": "success",
  "message": "Availability rule created successfully.",
  "data": { "availability_rule": { "…availability rule object…" } }
}

Errors: 401 unauthenticated · 404 no mentor profile · 422 validation · 500 server error


3. Create availability rules in bulk

POST /api/v1/mentor/availability-rules/bulk

Creates many rules in one request — the natural shape for a "set my weekly schedule" screen that submits seven days at once. Each element is validated with the same rules as endpoint 2, with the conditional requirements resolved per element from that element's own rule_type.

Body

{
  "availability_rules": [
    {
      "rule_type": "regular_weekly",
      "day_of_week": 1,
      "time_blocks": [ { "start_time": "09:00", "end_time": "17:00" } ]
    },
    {
      "rule_type": "regular_weekly",
      "day_of_week": 3,
      "time_blocks": [ { "start_time": "09:00", "end_time": "12:00" } ]
    }
  ]
}

A bare top-level JSON array is also accepted and normalized into availability_rules before validation, so [ {…}, {…} ] works identically.

Not transactional. Rules are created in a loop. If element 5 fails at the database level, elements 1–4 are already persisted and the response is 500. Validation failures are caught before any write (422, nothing persisted) — only a mid-loop write failure leaves a partial result. Re-listing after a 500 is the safe recovery.

201 Response

{
  "status": "success",
  "message": "Availability rules created successfully.",
  "data": { "availability_rules": [ { "…availability rule object…" } ] }
}

Errors: 401 unauthenticated · 404 no mentor profile · 422 validation · 500 partial write


4. Get an availability rule

GET /api/v1/mentor/availability-rules/{id}

Path parameters

Param Type Notes
id uuid Must belong to the authenticated mentor

200 Response

{
  "status": "success",
  "message": "Availability rule retrieved successfully.",
  "data": { "availability_rule": { "…availability rule object…" } }
}

Errors: 401 unauthenticated · 404 no mentor profile / not found / not the caller's rule


5. Update an availability rule

PUT|PATCH /api/v1/mentor/availability-rules/{id}

Every field is optional (sometimes) — send only what changes. Dispatches slot regeneration.

Body

Field Type Rules
session_type_id uuid Nullable, must exist
rule_type string regular_weekly · recurring · one_time
day_of_week integer Nullable, 06
time_blocks array Min 1 block; start_time / end_time required with the block
rrule string Nullable, max 500
specific_date date Nullable, today or later
valid_from date
valid_until date Nullable, on/after valid_from
is_active boolean

Unlike create, update enforces no conditional requirements — changing rule_type to recurring without supplying an rrule passes validation and yields a rule that generates no slots. Send the discriminating field alongside any rule_type change.

200 Response

{
  "status": "success",
  "message": "Availability rule updated successfully.",
  "data": { "availability_rule": { "…availability rule object…" } }
}

Errors: 401 unauthenticated · 404 no mentor profile / not found / not the caller's rule · 422 validation · 500 server error


6. Activate an availability rule

PATCH /api/v1/mentor/availability-rules/{id}/activate

Sets is_active = true and dispatches regeneration. No request body.

200 Response

{
  "status": "success",
  "message": "Availability rule activated successfully.",
  "data": { "availability_rule": { "…availability rule object…" } }
}

Errors: 401 unauthenticated · 404 no mentor profile / not found / not the caller's rule · 500 server error


7. Deactivate an availability rule

DELETE /api/v1/mentor/availability-rules/{id}

Soft deactivation, not deletion — sets is_active = false and keeps the row, so it can be revived through endpoint 6. Regeneration then cancels the future available slots the rule produced; booked and held slots are preserved.

200 Response

{
  "status": "success",
  "message": "Availability rule deactivated successfully.",
  "data": { }
}

Errors: 401 unauthenticated · 404 no mentor profile / not found / not the caller's rule · 500 server error


Mentor endpoints — off-days & vacation

Time blocked out of an otherwise valid rule. Both dispatch regeneration; only available slots are mutated, so an off-day never silently destroys a booking.

8. List off-days

GET /api/v1/mentor/off-days

Returns the mentor's off-days, most recent off_from first.

200 Response

{
  "status": "success",
  "message": "Off days retrieved successfully.",
  "data": { "off_days": [ { "…off-day object…" } ] }
}

Errors: 401 unauthenticated · 404 no mentor profile


9. Create an off-day

POST /api/v1/mentor/off-days

Body

Field Type Required Rules
off_from date yes Today or later
off_until date yes On/after off_from
reason string no Max 100 chars
is_full_day boolean no Defaults true
is_vacation boolean no Marks the range as vacation
off_time_blocks array conditional Required when is_full_day=false. Min 1 block
off_time_blocks.*.start_time string with block H:i
off_time_blocks.*.end_time string with block H:i, after its own start_time

Partial-hour off-days must be a single date. When is_full_day=false, off_from and off_until must be equal — otherwise validation fails on off_until. To block the same hours across several days, create one off-day per day.

201 Response

{
  "status": "success",
  "message": "Off day created successfully.",
  "data": { "off_day": { "…off-day object…" } }
}

Errors: 401 unauthenticated · 404 no mentor profile · 422 validation


10. Get, update, delete an off-day

GET    /api/v1/mentor/off-days/{id}
PUT    /api/v1/mentor/off-days/{id}
PATCH  /api/v1/mentor/off-days/{id}
DELETE /api/v1/mentor/off-days/{id}

Update takes the same fields as endpoint 9, all optional; the single-date constraint is re-checked only when is_full_day is present and false. Delete is a hard delete — unlike availability rules, the row is removed. All three dispatch regeneration; deleting an off-day restores the slots it had suppressed.

200 Response{ "off_day": { … } } for get/update; { } for delete.

Errors: 401 unauthenticated · 404 no mentor profile / not found · 422 validation


11. Enable vacation mode

POST /api/v1/mentor/vacation-mode

Body

Field Type Required Rules
vacation_ends_at date no Must be after today. Omit or null for indefinite

Two different behaviours by design. An indefinite vacation (vacation_ends_at null) blocks every future available slot immediately, in-request. A dated vacation injects a synthetic off-day range and blocks on the next generation run, so slots stay available until the queued job lands.

200 Response

{
  "status": "success",
  "message": "Vacation mode enabled successfully.",
  "data": { "mentor": { "…mentor profile object…" } }
}

Errors: 401 unauthenticated · 404 no mentor profile · 422 end date not in the future


12. Disable vacation mode

DELETE /api/v1/mentor/vacation-mode

Clears vacation_mode and vacation_ends_at, and restores future vacation_blocked slots back to available.

200 Response

{
  "status": "success",
  "message": "Vacation mode disabled successfully.",
  "data": { "mentor": { "…mentor profile object…" } }
}

Errors: 401 unauthenticated · 404 no mentor profile


Slot endpoints

13. Public — list slots for a mentor's session type

GET /api/v1/public/mentors/{mentorId}/session-types/{sessionTypeId}/slots

Unauthenticated. The primary discovery endpoint — what a public mentor profile renders its calendar from. Ordered by starts_at ascending.

Path parameters

Param Type Notes
mentorId uuid Mentor profile id
sessionTypeId uuid Session type id

Query parameters

Param Type Required Notes
from datetime no Window start. Defaults to now
until datetime no Window end. Defaults to now + 90 days

from / until are not validated — they are parsed directly. A malformed value returns 500, not 422. Send ISO-8601.

An unknown mentorId or sessionTypeId returns 200 with an empty slots array, not a 404 — the endpoint is a filtered query, not a resource lookup.

200 Response

{
  "status": "success",
  "message": "Availability slots retrieved successfully.",
  "data": { "slots": [ { "…slot object…" } ] }
}

Errors: 500 unparseable from / until


14. Public — list a mentor's session types

GET /api/v1/public/mentors/{mentorId}/session-types

Unauthenticated. The companion to endpoint 13 — pick a session type, then load its slots.

Returns inactive session types too. The is_active filter is currently disabled in the service, so clients that must not offer retired session types should filter on is_active themselves.

200 Response

{
  "status": "success",
  "message": "Available session types retrieved successfully.",
  "data": { "session_types": [ { "…session-type object…" } ] }
}

15. Mentor — list my slots for a session type

GET /api/v1/mentor/slots-by-session-type/{sessionTypeId}

Authenticated equivalent of endpoint 13, scoped to the caller's own mentor profile — the mentor's view of their own generated calendar. Same from / until query parameters and the same defaults.

200 Response

{
  "status": "success",
  "message": "Available slots retrieved successfully.",
  "data": { "slots": [ { "…slot object…" } ] }
}

Errors: 401 unauthenticated · 404 no mentor profile · 500 unparseable dates


16. Hold slots

POST /api/v1/mentor/slots/hold

Reserves slots for 10 minutes while the caller completes a booking, preventing two mentees from claiming the same time. Runs inside a transaction with SELECT … FOR UPDATE, so concurrent callers cannot both win.

Despite the mentor prefix, this is a mentee-facing endpoint. It authorizes on the authenticated user, not on a mentor profile — any authenticated user may hold slots. The path is a routing artifact, not an access rule.

Body

Field Type Required Rules
slot_ids array yes Min 1
slot_ids.* uuid yes Must exist in availability_slots

A hold reserves one seat, not the whole slot. A slot is holdable when it is not blocked (cancelled / vacation_blocked / override_blocked / overwrite_blocked) and it still has a free seat — that is, claimed seats plus seats already in checkout are fewer than max_capacity. Equivalently: available_seats > 0.

On a one-on-one slot (max_capacity: 1) the first hold consumes the slot and it flips to status: held, exactly as before. On a group slot several mentees hold seats concurrently and the slot stays available until the seats run out.

All slots in one request share a single booking_attempt_id — pass that id to the booking endpoint to convert the hold into a booking.

Example request

{ "slot_ids": ["1a2b…", "3c4d…"] }

200 Response — note this endpoint returns a raw payload, not a resource:

{
  "status": "success",
  "message": "Slots held successfully.",
  "data": {
    "booking_attempt_id": "7f8e…",
    "held_slots": ["1a2b…", "3c4d…"],
    "expires_at": "2026-07-20T09:10:00+00:00"
  }
}

409 Response — the whole request fails atomically; no partial holds are taken:

{
  "status": "error",
  "message": "Slots no longer available: 1a2b…, 3c4d…",
  "data": { }
}

The 409 message is one of "One or more slots not found." (an id resolved to nothing) or "Slots no longer available: <ids>" (someone else got there first). Both mean re-fetch the slot list and let the user pick again.

Errors: 401 unauthenticated · 409 slots unavailable · 422 validation · 500 server error


Expiry

Holds are released by the slots:release-expired-holds command, which runs every minute. It stamps released_at on every slot_holds row whose expires_at has passed, then recomputes the status of the affected slots.

slot_holds rows are the source of truth for reserved seats, not audit records: a hold is live while released_at IS NULL AND expires_at > NOW(). Rows are never deleted, but a released or expired one reserves nothing. Because seats are counted per row, expiring one mentee's hold on a group slot frees exactly their seat and leaves every co-tenant's hold alone.

Clients should treat expires_at as a soft deadline and re-hold if the user stalls, rather than assuming a hold survives to the moment of booking.


Related endpoints

Slots surface in two other modules, documented alongside their own flows:

Endpoint Doc
GET /api/v1/mentee/bookings/follow-up-slots booking-api.md
GET /api/v1/chat/shareable-slots messaging-api.md
POST /api/v1/chat/threads/{thread}/share-availability messaging-api.md

Generation semantics

Useful context for anyone reasoning about why a slot did or did not appear.

  • Horizon: 90 days from now. Candidates outside (now, horizon] are skipped.
  • Timezone: rule times are read in the mentor's timezone and stored UTC.
  • Identity: a slot is keyed (mentor_id, session_type_id, starts_at) — a unique index, so regeneration is idempotent.
  • Preservation: booked and held slots inside the horizon are loaded first and never mutated; their starts_at … buffer_ends_at ranges are treated as occupied.
  • Scope: only active session types and rules valid within the horizon are expanded. A rule with session_type_id: null expands against every session type.
  • Recurrence: recurring rules expand via the simshaun/recurr library.
  • Indefinite vacation short-circuits everything — all available slots become vacation_blocked and no expansion runs.
  • Cron: slots:generate runs daily; every rule / off-day / vacation / session-type mutation additionally dispatches GenerateMentorSlotsJob (3 tries, 300s timeout).