Download OpenAPI specification:Download
Build booking, membership and community applications on top of a Tatiya space.
The Tatiya Platform API lets an application read a space's public offering, take bookings on its behalf, and react to what happens inside it.
Public (/v1/public/...) needs no credential. It serves exactly what a
visitor to the space's own website can already see: profile, booking types,
availability, published posts. Cacheable, and safe to call from a browser.
Authenticated (everything else) needs an API key. A key belongs to one space and carries a fixed set of scopes, so the space is never a parameter — the key decides which tenant a call reaches.
No cross-space endpoints. A key that could name a tenant is a key that can name the wrong one; an integration spanning several spaces holds several keys. OAuth for "an app acting on behalf of any host" is a later phase.
| spaceHandle required | string^[a-z0-9][a-z0-9-]{1,62}$ Example: sunrise The space's handle — the |
| locale | string Default: "en" Example: locale=th BCP-47 tag. Exact match — content absent in this locale is absent, not substituted from another. |
{- "handle": "sunrise",
- "name": "Sunrise Studio",
- "tagline": "string",
- "about": "string",
- "timezone": "Asia/Bangkok",
- "currency": "THB",
- "locales": [
- "string"
], - "location": {
- "address": "string",
- "city": "string",
- "country": "string",
- "latitude": 0,
- "longitude": 0
}, - "open_hours": [
- {
- "weekday": "string",
- "opens_at": "string",
- "closes_at": "string",
- "closed": true
}
], - "accepts_bookings": true
}| spaceHandle required | string^[a-z0-9][a-z0-9-]{1,62}$ Example: sunrise The space's handle — the |
| locale | string Default: "en" Example: locale=th BCP-47 tag. Exact match — content absent in this locale is absent, not substituted from another. |
| limit | integer [ 1 .. 100 ] Default: 25 |
| cursor | string Opaque. Take it from the previous page's |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "slug": "string",
- "title": "Vinyasa Flow",
- "description": "string",
- "duration_minutes": 60,
- "price": {
- "amount_minor": 0,
- "currency": "THB"
}, - "capacity": 0,
- "categories": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string"
}
], - "intro_offer": {
- "eligibility": "first_time_only",
- "price": {
- "amount_minor": 0,
- "currency": "THB"
}
}
}
], - "has_more": true,
- "next_cursor": "string"
}Wraps computeSlots (@tatiya/shared/availability), which today has no
HTTP surface at all. Times are wall-clock in the space's own timezone;
the zone is returned alongside so a client never has to guess it.
A slot's presence means the window is open and unbooked at compute time. It is not a hold — the authoritative capacity check happens inside the booking transaction, and a slot can fill between this call and that one.
| spaceHandle required | string^[a-z0-9][a-z0-9-]{1,62}$ Example: sunrise The space's handle — the |
| bookingTypeId required | string <uuid> |
| from required | string <date> Inclusive start date, |
| to required | string <date> Inclusive end date. At most 62 days after |
{- "timezone": "Asia/Bangkok",
- "slots": [
- {
- "date": "2019-08-24",
- "weekday": "monday",
- "start_time": "string",
- "end_time": "string",
- "capacity": 0,
- "spots_left": 0
}
]
}The customer-facing write path. Distinct from POST /bookings, which is
the host acting on someone's behalf and can carry policy overrides —
this one cannot, by construction.
Identity resolves the way the rest of the platform resolves a guest: email lowercased, then digits-only phone. A returning customer is recognised without an account; someone who gives neither is a first-timer every visit.
Requires the space to have opted this integration in. An unauthenticated
caller may create a booking only when the space allows public booking;
otherwise present an API key with bookings:write.
| spaceHandle required | string^[a-z0-9][a-z0-9-]{1,62}$ Example: sunrise The space's handle — the |
| Idempotency-Key | string <= 255 characters A client-generated key, unique per logical operation. Replaying a request with the same key returns the original response instead of acting twice, for 24 hours. Send one on every write from a client that might retry — which is every client. |
| booking_type_id required | string <uuid> |
| scheduled_date required | string <date> |
| scheduled_start_time required | string |
required | object |
| source | string Default: "external_partner" Enum: "native_app" "discovery_app" "classpass" "external_partner" "admin_walk_in" "admin_line" "admin_whatsapp" "admin_messenger" "admin_phone" "admin_instagram" "admin_other" "website_enquiry" Where the booking came from. |
| marketing_consent | boolean Default: false Whether the customer agreed to marketing contact at the point of booking. Recorded with its own timestamp and the consent text shown. Never default this to true. |
| notes | string <= 1000 characters |
{- "booking_type_id": "7012cefa-e2da-4a08-9700-305b62e5cdd2",
- "scheduled_date": "2019-08-24",
- "scheduled_start_time": "07:00",
- "source": "external_partner",
- "marketing_consent": false,
- "notes": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "booking_type_id": "7012cefa-e2da-4a08-9700-305b62e5cdd2",
- "booking_type_title": "string",
- "scheduled_date": "2019-08-24",
- "scheduled_start_time": "07:00",
- "scheduled_end_time": "string",
- "status": "pending",
- "source": "native_app",
- "customer": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string",
- "phone": "string"
}, - "price": {
- "amount_minor": 0,
- "currency": "THB"
}, - "paid_at": "string",
- "checked_in_at": "string",
- "cancelled_at": "string",
- "was_intro_offer": true,
- "covered_by_membership": {
- "membership_id": "a4a28f8e-1ae6-49f7-beed-4e090aa324f4",
- "plan_name": "string"
}, - "notes": "string",
- "created_at": "string"
}Locale is an exact match, not a negotiated fallback — a post absent in the requested locale is absent. Post slugs are per-locale, so a URL built for one locale is not valid in another.
| spaceHandle required | string^[a-z0-9][a-z0-9-]{1,62}$ Example: sunrise The space's handle — the |
| locale | string Default: "en" Example: locale=th BCP-47 tag. Exact match — content absent in this locale is absent, not substituted from another. |
| limit | integer [ 1 .. 100 ] Default: 25 |
| cursor | string Opaque. Take it from the previous page's |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "slug": "string",
- "locale": "string",
- "title": "string",
- "excerpt": "string",
- "body_html": "string",
- "published_at": "string",
}
], - "has_more": true,
- "next_cursor": "string"
}Scoped to the key's space. Always paginated — there is no unbounded form, deliberately: the tRPC procedure this replaces reads the whole table twice per call and that is not a contract worth publishing.
| status | string (BookingStatus) Enum: "pending" "confirmed" "paid" "completed" "cancelled" "no_show" |
| source | string (BookingSource) Enum: "native_app" "discovery_app" "classpass" "external_partner" "admin_walk_in" "admin_line" "admin_whatsapp" "admin_messenger" "admin_phone" "admin_instagram" "admin_other" "website_enquiry" Where the booking came from. |
| from | string <date> Inclusive lower bound on |
| to | string <date> |
| awaiting_payment | boolean Priced, unpaid, and neither cancelled nor no-showed. |
| limit | integer [ 1 .. 100 ] Default: 25 |
| cursor | string Opaque. Take it from the previous page's |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "booking_type_id": "7012cefa-e2da-4a08-9700-305b62e5cdd2",
- "booking_type_title": "string",
- "scheduled_date": "2019-08-24",
- "scheduled_start_time": "07:00",
- "scheduled_end_time": "string",
- "status": "pending",
- "source": "native_app",
- "customer": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string",
- "phone": "string"
}, - "price": {
- "amount_minor": 0,
- "currency": "THB"
}, - "paid_at": "string",
- "checked_in_at": "string",
- "cancelled_at": "string",
- "was_intro_offer": true,
- "covered_by_membership": {
- "membership_id": "a4a28f8e-1ae6-49f7-beed-4e090aa324f4",
- "plan_name": "string"
}, - "notes": "string",
- "created_at": "string"
}
], - "has_more": true,
- "next_cursor": "string"
}The host-side write path — a booking taken over LINE, WhatsApp, the
phone, or through a partner's own checkout. Requires bookings:write.
allow_over_capacity and allow_policy_bypass are deliberate
overrides, not conveniences: send them only after a human has been told
what they are overriding. They are refused unless the key carries
bookings:override.
| Idempotency-Key | string <= 255 characters A client-generated key, unique per logical operation. Replaying a request with the same key returns the original response instead of acting twice, for 24 hours. Send one on every write from a client that might retry — which is every client. |
| booking_type_id required | string <uuid> |
| scheduled_date required | string <date> |
| scheduled_start_time required | string |
required | object |
| source | string Default: "external_partner" Enum: "native_app" "discovery_app" "classpass" "external_partner" "admin_walk_in" "admin_line" "admin_whatsapp" "admin_messenger" "admin_phone" "admin_instagram" "admin_other" "website_enquiry" Where the booking came from. |
| marketing_consent | boolean Default: false Whether the customer agreed to marketing contact at the point of booking. Recorded with its own timestamp and the consent text shown. Never default this to true. |
| notes | string <= 1000 characters |
| payment_status | string Default: "pay_later" Enum: "paid" "pay_later" "free" |
| check_in_now | boolean Default: false |
| allow_over_capacity | boolean Default: false Requires |
| allow_policy_bypass | boolean Default: false Requires |
{- "booking_type_id": "7012cefa-e2da-4a08-9700-305b62e5cdd2",
- "scheduled_date": "2019-08-24",
- "scheduled_start_time": "07:00",
- "source": "external_partner",
- "marketing_consent": false,
- "notes": "string",
- "payment_status": "paid",
- "check_in_now": false,
- "allow_over_capacity": false,
- "allow_policy_bypass": false
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "booking_type_id": "7012cefa-e2da-4a08-9700-305b62e5cdd2",
- "booking_type_title": "string",
- "scheduled_date": "2019-08-24",
- "scheduled_start_time": "07:00",
- "scheduled_end_time": "string",
- "status": "pending",
- "source": "native_app",
- "customer": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string",
- "phone": "string"
}, - "price": {
- "amount_minor": 0,
- "currency": "THB"
}, - "paid_at": "string",
- "checked_in_at": "string",
- "cancelled_at": "string",
- "was_intro_offer": true,
- "covered_by_membership": {
- "membership_id": "a4a28f8e-1ae6-49f7-beed-4e090aa324f4",
- "plan_name": "string"
}, - "notes": "string",
- "created_at": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "booking_type_id": "7012cefa-e2da-4a08-9700-305b62e5cdd2",
- "booking_type_title": "string",
- "scheduled_date": "2019-08-24",
- "scheduled_start_time": "07:00",
- "scheduled_end_time": "string",
- "status": "pending",
- "source": "native_app",
- "customer": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string",
- "phone": "string"
}, - "price": {
- "amount_minor": 0,
- "currency": "THB"
}, - "paid_at": "string",
- "checked_in_at": "string",
- "cancelled_at": "string",
- "was_intro_offer": true,
- "covered_by_membership": {
- "membership_id": "a4a28f8e-1ae6-49f7-beed-4e090aa324f4",
- "plan_name": "string"
}, - "notes": "string",
- "created_at": "string"
}Status is not settable here. A booking's lifecycle moves through the named transitions below, each of which publishes its own domain event — a generic status write would let a caller skip the event and leave the analytics, notification and community subscribers unfired.
| bookingId required | string <uuid> |
| notes | string or null <= 1000 characters |
| source | string (BookingSource) Enum: "native_app" "discovery_app" "classpass" "external_partner" "admin_walk_in" "admin_line" "admin_whatsapp" "admin_messenger" "admin_phone" "admin_instagram" "admin_other" "website_enquiry" Where the booking came from. |
{- "notes": "string",
- "source": "native_app"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "booking_type_id": "7012cefa-e2da-4a08-9700-305b62e5cdd2",
- "booking_type_title": "string",
- "scheduled_date": "2019-08-24",
- "scheduled_start_time": "07:00",
- "scheduled_end_time": "string",
- "status": "pending",
- "source": "native_app",
- "customer": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string",
- "phone": "string"
}, - "price": {
- "amount_minor": 0,
- "currency": "THB"
}, - "paid_at": "string",
- "checked_in_at": "string",
- "cancelled_at": "string",
- "was_intro_offer": true,
- "covered_by_membership": {
- "membership_id": "a4a28f8e-1ae6-49f7-beed-4e090aa324f4",
- "plan_name": "string"
}, - "notes": "string",
- "created_at": "string"
}Publishes booking.cancelled. A cancelled booking still counts against
an intro offer — forgiving them would make cancel-and-rebook an
unlimited free-session loop.
| bookingId required | string <uuid> |
| Idempotency-Key | string <= 255 characters A client-generated key, unique per logical operation. Replaying a request with the same key returns the original response instead of acting twice, for 24 hours. Send one on every write from a client that might retry — which is every client. |
| reason | string <= 500 characters |
{- "reason": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "booking_type_id": "7012cefa-e2da-4a08-9700-305b62e5cdd2",
- "booking_type_title": "string",
- "scheduled_date": "2019-08-24",
- "scheduled_start_time": "07:00",
- "scheduled_end_time": "string",
- "status": "pending",
- "source": "native_app",
- "customer": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string",
- "phone": "string"
}, - "price": {
- "amount_minor": 0,
- "currency": "THB"
}, - "paid_at": "string",
- "checked_in_at": "string",
- "cancelled_at": "string",
- "was_intro_offer": true,
- "covered_by_membership": {
- "membership_id": "a4a28f8e-1ae6-49f7-beed-4e090aa324f4",
- "plan_name": "string"
}, - "notes": "string",
- "created_at": "string"
}Requires the space's tier to include attendance tracking; a Free-tier
space gets 403 feature_not_available. Publishes booking.checked_in.
| bookingId required | string <uuid> |
| Idempotency-Key | string <= 255 characters A client-generated key, unique per logical operation. Replaying a request with the same key returns the original response instead of acting twice, for 24 hours. Send one on every write from a client that might retry — which is every client. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "booking_type_id": "7012cefa-e2da-4a08-9700-305b62e5cdd2",
- "booking_type_title": "string",
- "scheduled_date": "2019-08-24",
- "scheduled_start_time": "07:00",
- "scheduled_end_time": "string",
- "status": "pending",
- "source": "native_app",
- "customer": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string",
- "phone": "string"
}, - "price": {
- "amount_minor": 0,
- "currency": "THB"
}, - "paid_at": "string",
- "checked_in_at": "string",
- "cancelled_at": "string",
- "was_intro_offer": true,
- "covered_by_membership": {
- "membership_id": "a4a28f8e-1ae6-49f7-beed-4e090aa324f4",
- "plan_name": "string"
}, - "notes": "string",
- "created_at": "string"
}Publishes booking.paid. Requires bookings:write.
| bookingId required | string <uuid> |
| Idempotency-Key | string <= 255 characters A client-generated key, unique per logical operation. Replaying a request with the same key returns the original response instead of acting twice, for 24 hours. Send one on every write from a client that might retry — which is every client. |
| method required | string Enum: "cash" "bank_transfer" "card" "promptpay" "other" |
| amount_minor | integer >= 0 Minor units. Omit to record the booking's own price. |
| reference | string <= 200 characters |
{- "method": "cash",
- "amount_minor": 0,
- "reference": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "booking_type_id": "7012cefa-e2da-4a08-9700-305b62e5cdd2",
- "booking_type_title": "string",
- "scheduled_date": "2019-08-24",
- "scheduled_start_time": "07:00",
- "scheduled_end_time": "string",
- "status": "pending",
- "source": "native_app",
- "customer": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string",
- "phone": "string"
}, - "price": {
- "amount_minor": 0,
- "currency": "THB"
}, - "paid_at": "string",
- "checked_in_at": "string",
- "cancelled_at": "string",
- "was_intro_offer": true,
- "covered_by_membership": {
- "membership_id": "a4a28f8e-1ae6-49f7-beed-4e090aa324f4",
- "plan_name": "string"
}, - "notes": "string",
- "created_at": "string"
}| include_archived | boolean Default: false |
| limit | integer [ 1 .. 100 ] Default: 25 |
| cursor | string Opaque. Take it from the previous page's |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "slug": "string",
- "title": "Vinyasa Flow",
- "description": "string",
- "duration_minutes": 60,
- "price_amount": 0,
- "capacity": 0,
- "categories": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string"
}
], - "intro_offer": {
- "eligibility": "first_time_only",
- "intro_price_amount": 0,
- "applies_to_member": true
}, - "archived": true,
- "created_at": "string",
- "updated_at": "string"
}
], - "has_more": true,
- "next_cursor": "string"
}| bookingTypeId required | string <uuid> |
| member_id | string <uuid> Ask whether this member still has the booking type's intro offer, returned as |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "slug": "string",
- "title": "Vinyasa Flow",
- "description": "string",
- "duration_minutes": 60,
- "price_amount": 0,
- "capacity": 0,
- "categories": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string"
}
], - "intro_offer": {
- "eligibility": "first_time_only",
- "intro_price_amount": 0,
- "applies_to_member": true
}, - "archived": true,
- "created_at": "string",
- "updated_at": "string"
}Requires members:read. Never returns a credential field of any kind.
| lifecycle_stage | string Enum: "lead" "first_timer" "returning" "regular" "at_risk" "lapsed" |
| limit | integer [ 1 .. 100 ] Default: 25 |
| cursor | string Opaque. Take it from the previous page's |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string",
- "phone": "string",
- "joined_at": "string",
- "lifecycle_stage": "string",
- "last_visit_at": "string",
- "total_bookings": 0
}
], - "has_more": true,
- "next_cursor": "string"
}Requires members:write. Always creates a plain roster entry — the
same identity resolution the admin UI's own "add member" flow uses.
409s if the email is already registered anywhere on the platform,
not only in this space.
| Idempotency-Key | string <= 255 characters A client-generated key, unique per logical operation. Replaying a request with the same key returns the original response instead of acting twice, for 24 hours. Send one on every write from a client that might retry — which is every client. |
| email required | string <email> |
| name | string <= 200 characters |
{- "name": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string",
- "phone": "string",
- "joined_at": "string",
- "lifecycle_stage": "string",
- "last_visit_at": "string",
- "total_bookings": 0,
- "consent": {
- "marketing": true,
- "updated_at": "string"
}, - "recent_visits": [
- {
- "booking_id": "b0ae0641-0cd4-4f7f-8550-dcd550941f4a",
- "date": "2019-08-24",
- "booking_type_title": "string"
}
], - "active_memberships": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "plan_id": "00713021-9aea-41da-9a88-87760c08fa72",
- "plan_name": "string",
- "member_id": "435a4844-006a-4cfc-a644-e8eb2dd2ca43",
- "status": "active",
- "started_at": "string",
- "current_period_end": "string",
- "cancelled_at": "string"
}
], - "roles": [
- {
- "role": "participant",
- "bio": "string",
- "progression_level": "string",
- "certifications": [
- "string"
], - "specialties": [
- "string"
], - "created_at": "string"
}
]
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string",
- "phone": "string",
- "joined_at": "string",
- "lifecycle_stage": "string",
- "last_visit_at": "string",
- "total_bookings": 0,
- "consent": {
- "marketing": true,
- "updated_at": "string"
}, - "recent_visits": [
- {
- "booking_id": "b0ae0641-0cd4-4f7f-8550-dcd550941f4a",
- "date": "2019-08-24",
- "booking_type_title": "string"
}
], - "active_memberships": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "plan_id": "00713021-9aea-41da-9a88-87760c08fa72",
- "plan_name": "string",
- "member_id": "435a4844-006a-4cfc-a644-e8eb2dd2ca43",
- "status": "active",
- "started_at": "string",
- "current_period_end": "string",
- "cancelled_at": "string"
}
], - "roles": [
- {
- "role": "participant",
- "bio": "string",
- "progression_level": "string",
- "certifications": [
- "string"
], - "specialties": [
- "string"
], - "created_at": "string"
}
]
}Requires members:write. role is required, not inferred — a
member can hold several roles (participant AND facilitator, say),
each with its own bio/progression/certifications/specialties, so
there is no single "the" profile to patch without naming which
role's row it is.
| memberId required | string <uuid> |
| role required | string Enum: "participant" "facilitator" "guide" "artist" "host" |
| bio | string |
| progression_level | string |
| certifications | Array of strings |
| specialties | Array of strings |
{- "role": "participant",
- "bio": "string",
- "progression_level": "string",
- "certifications": [
- "string"
], - "specialties": [
- "string"
]
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "email": "string",
- "phone": "string",
- "joined_at": "string",
- "lifecycle_stage": "string",
- "last_visit_at": "string",
- "total_bookings": 0,
- "consent": {
- "marketing": true,
- "updated_at": "string"
}, - "recent_visits": [
- {
- "booking_id": "b0ae0641-0cd4-4f7f-8550-dcd550941f4a",
- "date": "2019-08-24",
- "booking_type_title": "string"
}
], - "active_memberships": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "plan_id": "00713021-9aea-41da-9a88-87760c08fa72",
- "plan_name": "string",
- "member_id": "435a4844-006a-4cfc-a644-e8eb2dd2ca43",
- "status": "active",
- "started_at": "string",
- "current_period_end": "string",
- "cancelled_at": "string"
}
], - "roles": [
- {
- "role": "participant",
- "bio": "string",
- "progression_level": "string",
- "certifications": [
- "string"
], - "specialties": [
- "string"
], - "created_at": "string"
}
]
}| limit | integer [ 1 .. 100 ] Default: 25 |
| cursor | string Opaque. Take it from the previous page's |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "description": "string",
- "price": {
- "amount_minor": 0,
- "currency": "THB"
}, - "billing_interval": "monthly",
- "archived": true,
- "benefits": [
- {
- "type": "unlimited_access",
- "scope": "all",
- "target_ids": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
]
}
]
}
], - "has_more": true,
- "next_cursor": "string"
}| status | string Enum: "active" "cancelled" "expired" "paused" |
| member_id | string <uuid> |
| limit | integer [ 1 .. 100 ] Default: 25 |
| cursor | string Opaque. Take it from the previous page's |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "plan_id": "00713021-9aea-41da-9a88-87760c08fa72",
- "plan_name": "string",
- "member_id": "435a4844-006a-4cfc-a644-e8eb2dd2ca43",
- "status": "active",
- "started_at": "string",
- "current_period_end": "string",
- "cancelled_at": "string"
}
], - "has_more": true,
- "next_cursor": "string"
}Requires memberships:write. Refuses a plan that is inactive,
archived, or belongs to a different space than the key's.
| Idempotency-Key | string <= 255 characters A client-generated key, unique per logical operation. Replaying a request with the same key returns the original response instead of acting twice, for 24 hours. Send one on every write from a client that might retry — which is every client. |
| member_id required | string <uuid> |
| plan_id required | string <uuid> |
| start_date | string <date> Defaults to today. |
| notes | string <= 2000 characters |
{- "member_id": "435a4844-006a-4cfc-a644-e8eb2dd2ca43",
- "plan_id": "00713021-9aea-41da-9a88-87760c08fa72",
- "start_date": "2019-08-24",
- "notes": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "plan_id": "00713021-9aea-41da-9a88-87760c08fa72",
- "plan_name": "string",
- "member_id": "435a4844-006a-4cfc-a644-e8eb2dd2ca43",
- "status": "active",
- "started_at": "string",
- "current_period_end": "string",
- "cancelled_at": "string"
}| membershipId required | string <uuid> |
| notes required | string <= 2000 characters |
{- "notes": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "plan_id": "00713021-9aea-41da-9a88-87760c08fa72",
- "plan_name": "string",
- "member_id": "435a4844-006a-4cfc-a644-e8eb2dd2ca43",
- "status": "active",
- "started_at": "string",
- "current_period_end": "string",
- "cancelled_at": "string"
}Publishes membership.cancelled. Requires memberships:write.
| membershipId required | string <uuid> |
| Idempotency-Key | string <= 255 characters A client-generated key, unique per logical operation. Replaying a request with the same key returns the original response instead of acting twice, for 24 hours. Send one on every write from a client that might retry — which is every client. |
| reason | string <= 1000 characters |
| immediate | boolean Default: false End access now instead of at the current period's end. |
{- "reason": "string",
- "immediate": false
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "plan_id": "00713021-9aea-41da-9a88-87760c08fa72",
- "plan_name": "string",
- "member_id": "435a4844-006a-4cfc-a644-e8eb2dd2ca43",
- "status": "active",
- "started_at": "string",
- "current_period_end": "string",
- "cancelled_at": "string"
}Only an active membership can be paused. Publishes
membership.paused. Requires memberships:write.
| membershipId required | string <uuid> |
| Idempotency-Key | string <= 255 characters A client-generated key, unique per logical operation. Replaying a request with the same key returns the original response instead of acting twice, for 24 hours. Send one on every write from a client that might retry — which is every client. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "plan_id": "00713021-9aea-41da-9a88-87760c08fa72",
- "plan_name": "string",
- "member_id": "435a4844-006a-4cfc-a644-e8eb2dd2ca43",
- "status": "active",
- "started_at": "string",
- "current_period_end": "string",
- "cancelled_at": "string"
}Only a paused membership can be resumed. Publishes
membership.resumed. Requires memberships:write.
| membershipId required | string <uuid> |
| Idempotency-Key | string <= 255 characters A client-generated key, unique per logical operation. Replaying a request with the same key returns the original response instead of acting twice, for 24 hours. Send one on every write from a client that might retry — which is every client. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "plan_id": "00713021-9aea-41da-9a88-87760c08fa72",
- "plan_name": "string",
- "member_id": "435a4844-006a-4cfc-a644-e8eb2dd2ca43",
- "status": "active",
- "started_at": "string",
- "current_period_end": "string",
- "cancelled_at": "string"
}Publishes membership.paid, and reactivates an expired membership.
Requires memberships:write. method is narrower than a booking
payment's — promptpay, bank_transfer, cash or other only.
| membershipId required | string <uuid> |
| Idempotency-Key | string <= 255 characters A client-generated key, unique per logical operation. Replaying a request with the same key returns the original response instead of acting twice, for 24 hours. Send one on every write from a client that might retry — which is every client. |
| method required | string Enum: "promptpay" "bank_transfer" "cash" "other" |
| amount_minor required | integer >= 0 |
| reference | string <= 200 characters |
{- "method": "promptpay",
- "amount_minor": 0,
- "reference": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "plan_id": "00713021-9aea-41da-9a88-87760c08fa72",
- "plan_name": "string",
- "member_id": "435a4844-006a-4cfc-a644-e8eb2dd2ca43",
- "status": "active",
- "started_at": "string",
- "current_period_end": "string",
- "cancelled_at": "string"
}🚧 Not built yet. Every operation under this tag is a design, not a live route — calling any of them today 404s. See docs/api/README.md's "What exists right now" for the current state, and docs/api/README.md's "Webhooks" section for the design itself.
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "event_types": [
- "booking.created"
], - "description": "string",
- "status": "active",
- "created_at": "2019-08-24T14:15:22Z"
}
]
}🚧 Not built yet — this operation does not exist in apps/api today;
calling it 404s. Documented here as the design.
Once shipped: requires the space's tier to include webhooks. The signing secret is in this response and nowhere else — the same rule the agent grant follows. Lose it and you rotate rather than look it up.
| url required | string <uri> Must be |
| event_types required | Array of strings (EventType) non-empty Items Enum: "booking.created" "booking.confirmed" "booking.paid" "booking.cancelled" "booking.checked_in" "booking.completed" "booking.no_show" "membership.assigned" "membership.paid" "membership.cancelled" "membership.paused" "membership.resumed" |
| description | string <= 200 characters |
{- "event_types": [
- "booking.created"
], - "description": "string"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "event_types": [
- "booking.created"
], - "description": "string",
- "status": "active",
- "created_at": "2019-08-24T14:15:22Z",
- "signing_secret": "string"
}The last 30 days. This is the debugging surface — without it, a failing integration is a black box.
| endpoint_id | string <uuid> |
| status | string Enum: "pending" "succeeded" "failed" "exhausted" |
| limit | integer [ 1 .. 100 ] Default: 25 |
| cursor | string Opaque. Take it from the previous page's |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "endpoint_id": "e9ce0d4f-d433-423d-9497-4c000544106c",
- "event_id": "a7a26ff2-e851-45b6-9634-d595f45458b7",
- "event_type": "booking.created",
- "status": "pending",
- "attempts": 0,
- "response_status": 0,
- "last_attempted_at": "2019-08-24T14:15:22Z",
- "next_attempt_at": "2019-08-24T14:15:22Z"
}
], - "has_more": true,
- "next_cursor": "string"
}What this key is and what it may do. Call it first — it is how a client discovers its own scopes rather than discovering them from a 403.
{- "key_id": "1e779c8a-6786-4c89-b7c3-a6666f5fd6b5",
- "label": "string",
- "space": {
- "handle": "string",
- "name": "string",
- "timezone": "string",
- "currency": "string"
}, - "scopes": [
- "bookings:read"
], - "expires_at": "string"
}The real route is unversioned — GET /health, not GET /v1/health.
It's documented under this spec's /v1 server for discoverability
alongside the rest of the API, but this path segment is the one
exception to that base URL: strip /v1 when actually calling it.
Static today ({"status":"ok"}, no dependency probe) — apps/api/src/index.ts.
{- "status": "ok",
- "database": "ok",
- "timestamp": "2019-08-24T14:15:22Z"
}| id required | string <uuid> The |
| type required | string (EventType) Enum: "booking.created" "booking.confirmed" "booking.paid" "booking.cancelled" "booking.checked_in" "booking.completed" "booking.no_show" "membership.assigned" "membership.paid" "membership.cancelled" "membership.paused" "membership.resumed" |
| created_at required | string <date-time> |
| space_handle required | string |
required | object The event payload. Carries the facts that cannot be cheaply re-derived; anything else (a space's current name, a booking type's current title) should be read fresh rather than trusted from an event that may be minutes old. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "booking.created",
- "created_at": "2019-08-24T14:15:22Z",
- "space_handle": "string",
- "data": { }
}| id required | string <uuid> The |
| type required | string (EventType) Enum: "booking.created" "booking.confirmed" "booking.paid" "booking.cancelled" "booking.checked_in" "booking.completed" "booking.no_show" "membership.assigned" "membership.paid" "membership.cancelled" "membership.paused" "membership.resumed" |
| created_at required | string <date-time> |
| space_handle required | string |
required | object The event payload. Carries the facts that cannot be cheaply re-derived; anything else (a space's current name, a booking type's current title) should be read fresh rather than trusted from an event that may be minutes old. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "booking.created",
- "created_at": "2019-08-24T14:15:22Z",
- "space_handle": "string",
- "data": { }
}| id required | string <uuid> The |
| type required | string (EventType) Enum: "booking.created" "booking.confirmed" "booking.paid" "booking.cancelled" "booking.checked_in" "booking.completed" "booking.no_show" "membership.assigned" "membership.paid" "membership.cancelled" "membership.paused" "membership.resumed" |
| created_at required | string <date-time> |
| space_handle required | string |
required | object The event payload. Carries the facts that cannot be cheaply re-derived; anything else (a space's current name, a booking type's current title) should be read fresh rather than trusted from an event that may be minutes old. |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "type": "booking.created",
- "created_at": "2019-08-24T14:15:22Z",
- "space_handle": "string",
- "data": { }
}