Skip to content

HTTP API

The implemented API lives at /api/v1. Use the running instance’s interactive reference at /api/v1/docs and OpenAPI JSON at /api/v1/openapi.json for exact schemas. The checked-in contract is OpenAPI 3.1, API revision 6.15.0.

Sign in through /sign-in, or send JSON { "email": "…", "password": "…" } to POST /api/auth/sign-in/email. Auth routes live outside /api/v1. Public sign-up is closed; the first account comes from one-time setup.

The server issues an HTTP-only signed session cookie:

  • HTTPS: __Secure-guestcraft.session_token.
  • HTTP localhost: guestcraft.session_token.

Use same-origin browser requests with credentials: "include". Server-side fetch needs explicit cookie handling; it has no browser cookie jar. Native apps sign in through the browser and use a device token instead (Authorization: Bearer gcd_…); see discovery and device sessions. Integrations use a workspace API key (Authorization: Bearer gc_live_…); see API keys and webhooks. The Channex API key supplied during onboarding authorizes the server’s Channex calls, not access to GuestCraft.

For every POST, PUT, PATCH or DELETE, send the configured instance Origin. Browser requests send it automatically. A non-browser cookie client must set it explicitly; a missing Origin is accepted only with browser Fetch Metadata Sec-Fetch-Site: same-origin or none. An unconfigured address receives request.untrusted_origin (403), including during sign-in. See runtime configuration for aliases and previews.

For a custom same-origin sign-in form:

async function signIn(email: string, password: string) {
const response = await fetch("/api/auth/sign-in/email", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
if (!response.ok) throw new Error("Sign-in failed");
}

Send GuestCraft-Workspace-Id: ws_… to select a workspace your session may access. It may be omitted when the caller reaches exactly one workspace. Missing selection with zero or multiple workspaces, an invalid workspace ID, or an inaccessible workspace produces auth.forbidden; selecting a workspace never grants access.

const response = await fetch("/api/v1/channex/connection", {
credentials: "include",
headers: { "GuestCraft-Workspace-Id": workspaceId },
});

The API covers every capability of the product status: properties, room types and rooms, guests, reservations and stays, availability and planning, rates and restrictions, policies, folios, room blocks and maintenance, night audit, reports, team and roles, the Channex connection, booking import, ARI publication and Messages, API keys and webhooks, and device sessions. Each operation’s description names the permission it requires, the credentials it accepts and its exact problem codes; the reference is the complete list. The workflow guides explain how operations combine, starting with properties, reservations and inventory and the front desk.

  • GET /api/v1/health and GET /api/v1/instance (also at /.well-known/guestcraft) are public. Health answers { "status": "ok" } without checking the database or Channex.
  • GET /api/v1/roles is public too: it returns the permission matrix of the built-in roles, which is the same for every workspace.
  • Two operations need no session, device token or API key, because they carry their own credential: POST /api/v1/device_sessions exchanges the mobile app’s one-time sign-in code, with its PKCE verifier, for a device session; POST /api/v1/invitations/accept uses the invitation’s bearer token, and an existing account also needs a matching session.
  • Every other operation needs a session, a device token or a workspace API key, and acts in the workspace that GuestCraft-Workspace-Id selects. Administration operations (team, roles, API keys, webhooks and Channex configuration) accept only a browser session; their descriptions say so.
  • GET /api/v1/channex/connection reads the saved connection (connected, environment, connected_at, property_count, account_mode and version) without contacting Channex, and PUT validates a Channex API key against the workspace’s Channex environment (staging for a sandbox workspace, production otherwise) before saving it encrypted. Listing Channex properties creates no GuestCraft records or mappings; import and ARI publication start only through their own workflows.

JSON uses snake_case. The API returns Request-Id on v1 responses. Errors use application/problem+json with RFC 9457 members type, title, status, detail, and GuestCraft’s code and request_id; validation errors may also contain an errors array. Branch on stable code, and retain request_id for support. Human-readable messages follow Accept-Language and must not be parsed.

Common outcomes:

  • auth.unauthenticated (401): no valid session, device token or API key.
  • auth.forbidden (403): workspace selection or permissions do not allow the operation.
  • request.untrusted_origin (403): the request’s browser origin is not configured for the instance, or a write has no trusted origin evidence.
  • request.invalid (400): invalid input; for Channex, also a missing connection or one that is not used (environment_mismatch).
  • channex.key_refused (400): Channex refused the key entered in Channels in the workspace’s Channex environment, named by the environment member.
  • cursor.invalid (400): pagination context is malformed or belongs to another workspace.
  • property.invalid (422): invalid local settings, with safe field pointers.
  • property.not_found (404): no GuestCraft property visible in the selected workspace.
  • server.unavailable (503): upstream/network failure or a bounded Channex listing that cannot finish.
  • server.internal (500): an internal failure, including a saved credential that cannot be decrypted. Check secret rotation.

Individual operations and common HTTP errors are documented in the OpenAPI reference. Auth routes have their own response contract. The SDK wraps v1 failures in GuestCraftError; it does not retry requests.

See local property setup for the operator workflow and current concurrency semantics.

What each API revision adds or changes, from 3.2.0; ADR 0016 defines when a revision is a major, minor or patch.

API revision 3.2.0 adds the property business_date projection and five nightAudits operations with the night_audits.run permission and recoverable audit problem codes. See night audit and ADR 0043.

API revision 3.3.0 adds room moves and splits: POST …/rooms/{room_id}/move and …/split, the segment’s room_type_id and the calendar stay’s segments. See reservations and ADR 0042.

API revision 3.4.0 adds discovery (GET /instance), the device sessions of the mobile app with their sign-in codes, and the caller’s workspaces. See discovery and device sessions and ADR 0041.

API revision 3.5.0 adds the automatic replay state of Channex receipts, the held acknowledgement count, and the Channex mapping, reconnect, webhook removal and busy-fence refusals. See Channex booking import and ADR 0028.

API revision 4.1.0 declares three members that responses already carried. The rooms of ReservationSummary, in reservations.list and guests.reservations, have booking_room_id and ota_cancelled when they come from an OTA booking. The availability of a validate_only preview has out_of_order, as availability.get does. See Channex booking import and ADR 0028.

API revision 4.1.1 answers Channex’s price limits in ARI health, settings.values.min_price and max_price, only to a caller who holds rates.read for the property; any other caller receives the rest of health. The schema is unchanged. See Channex ARI publication and ADR 0036.

API revision 4.1.2 answers every deposit of the front desk’s deposits_due with its required, covered and outstanding amounts, as the schema states, to the callers who receive the list: those with policies.prepayments for the property. A caller without folios.read received them without the amounts, which the SDK refused as an invalid answer. See policies and ADR 0036.

API revision 4.2.0 gives five refusals their own problem code. Each keeps the status it had, and only the operations that can answer a code document it. policies.create and guests.create document none of them: a create has no existing policy or guest, no If-Match and no archive flag.

Operation Refusal Code before Code now
policies.update the policy is not in the property reservation.not_found (404) policy.not_found (404)
policies.update the If-Match version is stale reservation.version_mismatch (412) policy.version_mismatch (412), with current_version
policies.update archiving a policy that an active rate plan uses inventory.in_use (409) policy.in_use (409)
guests.update editing a guest that an OTA booking owns reservation.invalid_state (409) guest.ota_owned (409)
channex.ari.map, channex.ari.resync a send is still open, or booking intake has not settled inventory.in_use (409) channex.ari_busy (409)

A client that branched on the old code of one of these refusals must be updated to match its new code, from “Code before” to “Code now” in the table; a client that branches on the status alone needs no change. The 4.5.0 revision below gives the remaining ARI and booking-import mapping refusals their precise codes. See policies, Channex ARI push and ADR 0016.

API revision 4.3.0 documents the GuestCraft-Workspace-Id header on the six Channex ARI operations, channex.ari.health, plans, mappings, map, repair and resync. The server always read it there to choose the workspace, and their behaviour is unchanged. The rest of the revision changes descriptions and the spelling of one pattern only, and no constraint:

  • every optional GuestCraft-Workspace-Id header has one description, which says when it may be omitted and what a workspace that the credentials cannot reach answers;
  • every If-Match of a version within the safe integers has one description;
  • the decimal amounts of reservation room input, rates and reports are written ^\d+(\.\d+)?$, which accepts the same values as the former ^\d+(?:\.\d+)?$;
  • reservations.update states that an empty body at the current version changes nothing.

API revision 4.4.0 starts the per-operation documentation correction. It declares all 27 used tags, gives 409/422 responses code-directed meanings, documents optional Retry-After on server.unavailable (also alongside codes with fixed retry delays), and removes 16 impossible 412 responses from night audit, team, block and maintenance ticket operations that take no If-Match. Scoped tests pin summaries, distinct multi-sentence descriptions and the 412/If-Match rule. Problem lists other than these 412 corrections remain shared in several modules; exact per-operation problems, complete individual descriptions and Channex connection coverage follow separately. Generated error maps lose only the 16 impossible 412 keys listed in ADR 0016. No runtime response, schema, parameter or successful response changes.

API revision 4.4.1 continues the per-operation documentation correction, and canonicalizes fixed policy penalty amounts.

  • Night audits, team, blocks, maintenance tickets and properties: unreachable 404, 409 and 422 responses are removed, and the reachable cursor.invalid, maintenance-ticket and invitation-validation codes are restored; the generated *Errors maps lose 10 keys at 404, 15 at 409 and 12 at 422. The exact-list test in packages/api/src/openapi.unit.test.ts is the authoritative list.
  • Rates, booking import and ARI: 23 operations get individual summaries, permissions, preconditions and exact problems; 15 generated *Errors maps lose 30 impossible keys (404: 10, 409: 8, 412: 10, 422: 2); the exact status lists in packages/api/src/openapi.unit.test.ts and rate-problem lists in packages/api/src/routes/rates.unit.test.ts are the authoritative list. No runtime response, schema, parameter or successful response changes; ADR 0016’s correction criteria apply.
  • Room types, units, guests, reservations, reservation rooms, policies, availability, the calendar, the front desk and folios (56 operations, 13 tags) each get their own summary and description and exactly the problems their core use case declares; 412 stays only where If-Match is taken and on reservations.create’s changed quote (quote.changed); stay money members are documented as needing folios.read; their generated *Errors maps lose 81 impossible keys (412: 39, 409: 25, 422: 16, 404: 1). The authoritative lists are the problemsOf calls of these routes, which the typecheck holds equal to each use case’s codes.

Fixed policy penalty amounts are canonicalized to the decimals of their ISO 4217 currency. For example, an EUR amount sent as "50" is answered and persisted as "50.00", a KWD amount "1.5" as "1.500", and a JPY amount "50.0" as "50". Existing stored policy terms and reservation snapshots are canonicalized when they are read and on their next update; the numeric obligation is unchanged. Amounts that need precision beyond the currency’s minor unit remain invalid; extra trailing zeros are accepted.

API revision 4.4.2 corrects the problems that roomTypes.create and units.create document. Creating has no stored record to find, archive or protect, so roomTypes.create no longer lists room_type.not_found, inventory.archived or inventory.in_use, and units.create no longer lists unit.not_found, inventory.archived or inventory.in_use; the updates keep them. The core now has separate create and update use cases for room types, units and rate plans, and the problemsOf lists of the inventory routes, which the typecheck holds equal to each use case’s codes, are the authoritative list. Every status stays documented, so no generated *Errors key changes; only the code lists in the descriptions of RoomTypesCreateErrors and UnitsCreateErrors narrow. No runtime response, schema, parameter or successful response changes, and no client change is needed.

API revision 4.4.3 canonicalizes ota.revision_at in every answer that carries ota (the OtaBooking schema): reservations, their room actions, guests.reservations, calendar.get and frontDesk.get. For example, 2026-10-27T10:00:00.000000Z becomes 2026-10-27T10:00:00Z, .100000Z becomes .1Z, and 2026-10-27T12:00:00.123456789+02:00 becomes 2026-10-27T10:00:00.123456789Z. The instant is unchanged, including nanoseconds; offsets become Z, trailing fractional zeros are dropped, and seconds are always present (10:00Z becomes 10:00:00Z). Existing non-canonical reservation JSON is served canonically on read and rewritten on its next update. Encrypted replay archives keep the adapter-normalized text they held before this change, including the Z appended to offset-less Channex timestamps. Clients comparing timestamp text should compare instants instead; response shapes and permissions are unchanged. Web and mobile do not display or sort by this field.

API revision 4.5.0 adds precise Channex refusal codes and documents request body limits on every operation. GET operations document the malformed-length 400; methods that carry a body also document the oversized-body 413. The shared 415 answer covers a request body with an unsupported media type. A GET that declares a body (Content-Length above zero or Transfer-Encoding) without a JSON Content-Type also answers that shared 415. The pre-existing 413 entries on ratePlans.list and rates.get remain as a documented follow-up for a later revision; no other GET operation documents 413.

Operation Refusal Code before Code now
channex.ari.map remapping or removing a mapping while ARI publication is live inventory.in_use (409) channex.ari_mapping_live (409)
channex.ari.resync a second manual full sync within 24 hours inventory.in_use (409) channex.ari_full_sync_used (409)
channex.mapping.put a pending unlink blocks the save inventory.in_use (409) channex.mapping_unlink_pending (409)
channex.mapping.put an in-use import mapping would be remapped inventory.in_use (409) channex.mapping_in_use (409)

Clients that branch on these refusal codes must be updated to match the new values; status-only handling is unchanged. A pending unlink is not cleared by repeating the save: inspect pending_unlinks in channex.sync.get, repeat DELETE /channex/mappings/{property_id} for the listed property (or forget it with DELETE /channex/mappings/{property_id}/cleanup), then save again. Every operation documents 400 request.invalid for a malformed Content-Length header; every operation that can carry a body documents 413 request.too_large for a body that exceeds the configured limit. These answers happen before routing or the operation body is read, so clients should send a valid decimal byte length or omit the header and retry smaller requests.

API revision 5.0.0 (31 October 2026) adds property report history coverage, daily pickup comparisons, cash payments/refunds by method and service date, and gross folio revenue by category/tax category and service date, each with JSON and CSV under reports.view. Performance definition 2 replaces the current-inventory denominator with historical capacity: definition_version changes from 1 to 2 and capacity_basis from current_active_units to historical_active_units. These closed values break older SDK validators and TypeScript comparisons. This is an explicit third exception to /v1’s addition-only rule in ADR 0016: no release exists and the SDK is unpublished. Upgrade SDKs and definition checks together with the server. Existing CSV columns keep their order; definition/capacity/history metadata and pre_history_nights are appended. Performance still accepts older dates, using labelled baseline capacity before history; pickup alone refuses unsupported observation dates, with field errors. See reports and ADR 0031.

Deployment and recovery: migration 0030 is contract and requires roll-forward; previous builds cannot cold-start after it. No released deployment upgrades across 0030. Its baseline blocks writes to properties, reservations, reservation rooms/nights, units, room types and unit blocks for the whole scan and DDL. Each table acquisition can wait up to five seconds, with earlier locks already held. Deploy large instances in a quiet window; pooled connections held by blocked writes can also delay reads. The measured hold-time guidance is in ADR 0031. Expand migration 0031 adds the ledger service-date index in a separate transaction after the baseline commits. Its five-second timeout bounds lock acquisition; ledger writes wait through the index build, so large ledgers also require a quiet window.

API revision 5.1.0 adds QR pairing of the mobile app and a precondition for room status changes replayed from the app’s offline queue. See discovery and device sessions, reservations and inventory and ADR 0041.

  • POST /device_pairings (devicePairings.create), GET /device_pairings/{id} (devicePairings.get) and DELETE /device_pairings/{id} (devicePairings.cancel, with If-Match): a signed-in browser issues a one-time pairing code for a QR code (with expires_in, the whole seconds it has left when the answer is sent, so a client counts down whatever its own clock says), follows its status (pending, paired, refused, expired) and withdraws an unused code. Browser session only; new problems device_pairing.not_found (404), device_pairing.used (409) and device_pairing.version_mismatch (412).
  • POST /device_sessions (deviceSessions.create) also takes the pairing grant { "pairing_code": "gcp_…", "device_name": "…" }, exchanged only at the instance’s origin that issued it. A malformed device name answers device_session.invalid_request with field: device_name on this operation, and leaves the code unused. Both grants are now admitted in the budgets of API key and device token guesses before the code is looked up, and can answer 429 auth.rate_limited with Retry-After; the operation also documents the request.malformed it answers to a body that is not JSON.
  • Discovery lists the mobile_pairing capability.
  • Unit gains housekeeping_version, which every change of the status advances by one. PUT …/units/{id}/housekeeping (units.housekeeping) takes an optional expected_housekeeping_version and an optional expected_updated_at; with either, a unit already in the requested status is answered unchanged and a unit whose status changed since answers 409 unit.status_conflict. Without them, nothing changes.

API revision 5.2.0 adds property-local night-audit scheduling, explicit automatic no-show settings and durable execution health through GET/PUT /properties/{property_id}/night_audit_settings. Audit results list fee-free expired options and automatically processed no-show rooms. Direct booking create/edit accepts nullable option_expires_on; confirming clears it. Cash drawers have versioned settings plus current, list, create, report and close operations under cash_shift_settings and cash_shifts, with property-scoped cash_shifts.read/open/close/configure and night_audits.configure. Cash posting admission and nullable shift attribution are selected by the server. See night audit and cash shifts for operator recovery, DST/catch-up and hosting cadence limits. It also adds the scoped metadata reads nightAudits.context (time_zone) and cashShifts.context (currency, time_zone), without requiring general property read access. Reservation precheck checks may contain resolves_at_audit: "option_expiry". Audit responses include required released_options and no_show_rooms, live no_show_details and released_option_details, and actor_kind alongside the live actor name. Folio movements include the server-assigned nullable cash_shift_id; clients cannot select attribution.

Cash history now uses ADR 0016: cursor, default limit 20, and { data, next_cursor, has_more }, with cursor.invalid for malformed cursors. Cash reports include live opened_by_name/closed_by_name. Audit settings include next_run_at. nightAudits.warnings returns the last automatic run’s warning counts and durable review state; nightAudits.reviewWarnings marks a completed automatic run opened. Automatic batches may report night_audit.in_progress. Settings and pending-run ETags are write version guards, not representation validators. The unreachable cash_shift.currency_mismatch problem is removed; the domain and database enforce that invariant.

Reservation policy decisions add optional live undo_available, computed by the server from the property business date; clients can offer automatic no-show recovery without duplicating the date rule. Warning review requires a browser member session and returns 409 night_audit.review_unavailable for pending or manual runs; manual runs need no review.

API revision 5.3.0 completes integrations (ADR 0040, API keys and webhooks) and adds the organization ownership transfer (ADR 0030):

  • WebhookEventType gains the events of Channex imports, folios, rates and rate plans, and room housekeeping status, reservation.overbooked@1 and the same-day undo of check-in and check-out; event bodies may carry data.reservation_id, and data.object names the kind of data.id.
  • GET /events (events.list) and GET /events/{id} (events.get) read the catalog’s events of the last 30 days, with the new permission events.read, from a browser session, a device token or an API key; problems event.invalid (422, field) and event.not_found (404).
  • POST /webhook_endpoints/{id}/replays (webhookEndpoints.replay) and POST /webhook_endpoints/{id}/deliveries/{delivery_id}/redeliver (webhookDeliveries.redeliver), browser-only; problems webhook_replay.invalid (422, field, reason), webhook_delivery.not_found (404) and webhook_delivery.pending (409); WebhookDelivery gains origin. webhook_endpoint.disabled is also answered by them.
  • POST /api_keys/{id}/rotate (apiKeys.rotate, If-Match), browser-only, answers 201 with the replacement’s secret, the replacement and the rotated key; problem api_key.not_rotatable (409). ApiKey gains allowed_ips, replaced_by and creator_access, and ApiKeyInput accepts allowed_ips; api_key.invalid names allowed_ips or grace_period_hours too. A key used from an address outside its allow-list answers 401.
  • POST /members/{member_id}/ownership_transfer (team.transferOwnership, If-Match), browser-only. Workspace audit entries (team.audit) gain actor_type and api_key_id; the member list gains organization_owner and organization_name.

API revision 5.4.0 adds workspace duplicate candidates and audited dismissal, a two-version guest merge with per-field choices and survivor pointers, append-only purpose consent history, and owning-property reassignment. New grants are guests.merge, guests.consents and guests.reassign. See guests and ADR 0045.

Revision 5.4.0 also exposes the following guest review contracts:

  • guests.list excludes absorbed identities unless include_merged=true; direct reads retain the final visible merged_into pointer and display name. Every guest write refuses an absorbed identity with 409 guest.absorbed and survivor_id. A pair containing the same identity twice is 422 guest.pair_invalid.
  • Duplicate pages exclude dismissals before paging unless include_dismissed=true and order strong contact matches by strength. Name equality only adds context. DELETE /guests/{id}/duplicate_dismissals/{other_id} undoes a dismissal with the path guest’s If-Match and the other guest’s other_version query parameter. Both dismissal directions are audited and idempotent.
  • GET /guests/shared_contacts and /guests/{id}/shared_contacts page contact keys shared by more than five reachable live guests. Each group has a count and at most five previews; /guests/shared_contact_guests?reason=email&key=... pages all visible members.
  • GET /guests/{id}/merges pages preserved, visible absorbed ancestry. Consent history is newest first and includes actor_name; /guests/{id}/consents/current returns the latest decision per purpose independently of history pages. Merges never combine consent evidence.
  • GET /guests/{id}/contact_notices pages OTA snapshots newest first by revision time and reservation identifier descending. contact contains only differing fields whose revision is later than that field’s last contact edit, Apply or dismissal, excluding every value previously dismissed for the guest and field. Omitted fields are not actionable; explicit null proposes clearing a field. Notes are never offered. Reservations expose the separate ota_contact snapshot. A merged survivor remains editable. Guest e-mail validation follows the core, including international OTA addresses: selecting notice fields and replacing the guest through the normal version-checked PUT is an ordinary audited edit. Overlay only selected notice fields on the current profile and retain its notes. Only actually changed contact fields advance their review times; notes-only edits, consent and ownership changes do not clear notices. Moved reservations receive new versions and ordinary reservation update events.
  • PUT /guests/{id}/contact_notice_dismissals/{reservation_id}/{field} keeps the current profile value for one field (full_name, email, phone, country or language). It requires guests.manage, the guest’s strong If-Match, and a body with revision_id and expected_value exactly matching the stored snapshot field (including null). Success returns the unchanged contacts with the guest’s new version and ETag. The dismissal is audited without contact values and remembers the dismissed value permanently for that guest and field, across later revisions and other reservations. Repeating it with current If-Match and a matching snapshot creates no extra audit/event or version. A stale guest returns 412 guest.version_mismatch; a changed, missing or inaccessible snapshot returns 409 guest.contact_notice_changed. Refetch both before deciding again. Invalid domain input is 422 guest.invalid, hidden/missing guest is 404 guest.not_found, denied access is 403 auth.forbidden, and tombstones retain the 409 guest.absorbed final-survivor contract. Malformed JSON is 400 request.malformed; invalid headers, path fields or body shape are 400 request.invalid.

The SDK facade and generated React factories expose each added operation. The MIT @guestcraft/sdk/guests entry point exports GUEST_CONSENT_PURPOSES from the generated response enum, following the core catalog without a second handwritten purpose list.

API revision 5.5.0 adds split folios, transfers, per-room settlement and bulk re-rating (ADR 0029). New operations: reservationFolios.list, reservationFolios.get, reservationFolios.create (201 with ETag and Location), reservationFolios.update (PATCH), reservationFolios.close and reservationFolios.reopen (each change takes the folio’s version in If-Match and answers 412 folio.version_mismatch), folios.transfer and rerates.apply. New grants: folios.split, folios.transfer and reservations.rerate. New problem codes: folio.not_found (404), folio.closed, folio.not_settled, folio.invalid_state, folio.not_transferable (409), folio.version_mismatch and rerate.stale (412). Applying a re-rate requires each selection’s previewed expected_total and expected_fingerprint (the stay’s fingerprint in the preview).

Additions to existing answers, which clients must accept: Folio carries folios (each folio with its payer, room, routes, state, version and totals); each movement carries located_in, counterpart_folio_id and transferable, and kind may be transfer, a leg of a pair that nets to zero and is never revenue; CheckoutQuote carries scope (room when the amount is the room’s own folios); folioCreditsDue.list answers one line per folio, with folio_id, folio_number, folio_name and payer, so a reservation may appear more than once, and a folio whose room has left appears before the reservation finishes. folios.postExtra and folios.pay accept an optional folio_id; without it an extra follows the reservation’s routes and a payment goes to the first folio, as before for a reservation with one folio. A closed folio (closed_by: member, or check_out when the reservation’s last room left, which closes every open folio, the first included, and is reopened by undoing that check-out) refuses new extras and transfers with folio.closed; payments, refunds and reversals still post on it. This changes one existing answer: an extra posted after the last room’s check-out now answers 409 folio.closed until a member with folios.split reopens the folio; a reversal of a transfer answers folio.not_reversible. Posting keys starting with transfer: are reserved, like room:, rerate: and policy:, and answer folio.invalid.

The folio webhook events follow the split, each with the folio as its subject: folio.closed@1 is emitted once for each folio that closes, by a member’s close or by the last room’s check-out (which closes every open folio), and the new folio.reopened@1 once for each folio that reopens, by a member’s reopen or by the undo of that check-out (which reopens exactly the folios it closed). WebhookEventType gains folio.reopened@1.

API revision 6.0.0 (31 October 2026) is a major revision: it adds housekeeping and changes the maintenance ticket contract (ADR 0046), the fourth dated exception to /v1’s addition-only rule in ADR 0016, which lists the three removed operations and links the breaking-change paragraph of ADR 0046. Migration 0039 is its contract migration: it replaces the ticket status check and adds tables; deploy it by rolling forward.

  • New housekeeping operations: the settings (housekeepingSettings.get, update with If-Match), on-demand idempotent day generation (housekeepingDays.generate, 201 the first time and 200 again, each run giving the day its next version as ETag; housekeepingDays.get answers a generated day with its version, and 404 housekeeping.day_not_found for a day never generated), tasks (housekeepingTasks.list, create, get, update, status, assign) and housekeepingAssignees.list. The bulk changes of 1 to 100 tasks, housekeepingTasks.bulkStatus (POST …/housekeeping_task_status_changes) and bulkAssign (POST …/housekeeping_task_assignments), take each task’s version in the body instead of If-Match and answer a stale one with 412. Reads need inventory.read; generation, manual tasks and assignment need the new housekeeping.assign, the settings the new housekeeping.configure, and status changes the existing housekeeping.update.
  • Maintenance tickets gain a priority, an assignee, scheduled and due dates, the reporting member, the housekeeping task they were found during, photos and an activity history, with maintenanceTickets.get, status, activity, photos.create, photos.get, photos.remove and maintenanceAssignees.list. maintenanceTickets.create takes the new optional members, an optional client id (a repeated report answers 200) and an optional block that creates an out-of-order or out-of-service block with the ticket, so it can also answer ticket.assignee_invalid and the block refusals block.invalid, block.overlap, block.assigned and block.past; a report that creates a block also answers overbooked_dates, the nights it oversells, as blocks.create does. A client id that another property’s task, ticket or photo uses is refused (housekeeping.invalid or ticket.invalid with field id, photo.invalid), writing nothing. The photos are part of the ticket: adding one (a body of up to 3 MiB; photo.too_large (413) or request.too_large) or removing one gives the ticket its next version, answered as ETag, and removing one takes the ticket’s version in If-Match (ticket.version_mismatch, 412). A photo is answered as its bytes, with ETag: "1": it never changes.
  • Breaking for maintenance-ticket clients (pre-1.0): the collection is renamed from /properties/{property_id}/maintenance-tickets to /properties/{property_id}/maintenance_tickets for every ticket operation, as ADR 0016’s snake_case paths require; the operation IDs are unchanged, so SDK callers only regenerate. The status is open, in_progress or done, and is changed only through PUT …/maintenance_tickets/{id}/status, where closing names its housekeeping follow_up; maintenanceTickets.update no longer takes status, and its members are all optional, at least one. ticket.invalid now carries field.
  • New problem codes: housekeeping.task_not_found (404), housekeeping.day_not_found (404), housekeeping.invalid (422, with field), housekeeping.version_mismatch (412), housekeeping.invalid_state (409), housekeeping.assignee_invalid (422), ticket.assignee_invalid (422), photo.not_found (404), photo.invalid (422, with field: base64 for content that is not a whole image of its type, id for an identifier in use), photo.dimensions_too_large (422, with field, max_side 4096 and max_pixels 16,000,000), photo.too_large (413, with max_bytes) and photo.limit_reached (409). The permission lists of roles, API keys and workspaces name the two new grants, and discovery declares the housekeeping_tasks and maintenance_photos capabilities.

API revision 6.1.0 adds reservation-room resizing with validation-only previews, the permissioned Do not move sub-resource and confirmation member on move/date/assignment writes, and a version-checked inverse Undo using server-owned change receipts. Reservation rooms and calendar stays expose the flag; successful planning changes expose the receipt identifier. Multi-room direct booking screens use the existing atomic reservation creation contract. Omitted resize pricing quotes added planned nights while retaining booked nights; manual stays use booked edge prices. Shortening needs no quote or override. A refused added-night preview can include reasons on quote.not_bookable. Resize windows must overlap the stay. Undo receipts expire after ten minutes; inverse domain events describe the actual change and include undo_of. See reservations and ADR 0042.

Custom role bodies (team.createRole, team.updateRole) accept as many permissions as the catalogue holds (69 in this revision) instead of 64, so a role can still grant every permission.

API revision 6.2.0 adds the API platform (ADR 0047, API keys, tokens and webhooks, MCP server):

  • Two bearer credentials beside API keys, on every operation that lists apiKey: the organizationToken scheme (gc_org_…, the workspace chosen with GuestCraft-Workspace-Id) and the clientSession scheme (gc_cst_…).
  • GET/POST /organization_tokens, GET /organization_tokens/{id}, POST /organization_tokens/{id}/revoke and POST /organization_tokens/{id}/rotate (organizationTokens.*, browser-only, organization owners and administrators); problems organization_token.invalid (422, field), organization_token.not_found (404), organization_token.version_mismatch (412) and organization_token.not_rotatable (409).
  • GET/POST /client_sessions, GET /client_sessions/{id} and POST /client_sessions/{id}/revoke (clientSessions.*) with the new permission client_sessions.manage; only an API key or an organization token mints; problems client_session.invalid (422, field), client_session.not_found (404) and client_session.version_mismatch (412).
  • The API answers the CORS preflight of every /v1 operation (OPTIONS with Origin and Access-Control-Request-Method: 204, Access-Control-Allow-Origin: *, never credentials), and lets a page read the answers of requests carrying a client session’s token.
  • Every operation declares its effect on the instance’s state as x-guestcraft-effect: read, create, update, transition or delete; the MCP server marks tools by it.
  • Workspace audit entries (team.audit) gain the actor types organization_token and client_session, with organization_token_id and client_session_id. The access log names the credential as api_key_id, organization_token_id or client_session_id.

API revision 6.3.0 completes Channex ARI publication and booking import (ADR 0034, ADR 0028, publication, booking import):

  • AriHealth gains drift (the latest nightly drift check, the drift repairs not yet published again and those rejected past the retry limit); AriSend gains lookups and looked_up_at, and its code can be lookup_values_held (Channex holds the send’s values, which does not prove that the request ran).
  • GET /properties/{property_id}/channex/alerts (channex.alerts.list) lists a property’s open Channex alerts, with channex.view_health, from a browser session, a device token or an API key; GET /properties/{property_id}/channex/alert_transitions (channex.alerts.transitions) lists their openings and resolutions from since, with next for the following read, each to be shown once by its event id. Both, and channex.bookingHistory.list, can answer 429 auth.rate_limited.
  • GET /properties/{property_id}/channex/booking_history (channex.bookingHistory.preview) and POST /properties/{property_id}/channex/booking_history_imports (channex.bookingHistory.import, 201), browser-only with channex.configure, preview and queue the import of a linked property’s existing Channex bookings; GET /properties/{property_id}/channex/booking_history_imports (channex.bookingHistory.list) lists its latest imports with channex.view_health. Problems channex.history_read_refused (409) and channex.history_import_running (409); channex.mapping_inactive is also answered by them.
  • OtaBooking gains reinstated_at: a channel can reinstate rooms it cancelled.
  • WebhookEventType gains channex.alert_opened@1, channex.alert_resolved@1 (object channex_alert) and reservation.reinstated@1.

API revision 6.4.0 adds browser-session tenancy administration (ADR 0049, organizations and workspaces):

  • GET/POST /organizations (organizations.list, organizations.create) lists the member’s organizations and instance-administration capability, or creates an organization, first workspace and creator memberships atomically. Creation requires instance administration.
  • POST /organizations/{organization_id}/workspaces (workspaces.create) creates a production or sandbox workspace for an organization owner/admin.
  • GET/PUT /workspaces/{workspace_id} (workspaces.get, workspaces.update) reads/replaces workspace name settings with workspaces.configure and unrestricted property access.
  • PUT /workspaces/{workspace_id}/archival (workspaces.setArchival) archives/restores a workspace for its organization owner/admin, preserving all records. Unlink Channex properties and finish webhook cleanup first; saved credentials are retained. Operational access and business-table RLS stop while archived.
  • GET /organizations/{organization_id}/audit (organizations.audit) reads organization Activity for organization owners/admins, with exact action, actor, subject and instant filters. limit is 1–100 (default 20); opaque cursors use descending (occurred_at,id) keysets and bind the organization and filters. Responses are {data,next_cursor,has_more}; invalid or reused cursors return 400 cursor.invalid. Workspace Activity retains its documented temporary offset contract.
  • Workspace item reads return a strong numeric version ETag ("N"); replacement and archival require it in If-Match. Problems are tenancy.invalid (422, field-linked name failures), tenancy.precondition_failed (412), and tenancy.integration_connected (409). Schema/path/query/precondition validation still answers request.invalid (400). Missing and foreign tenancy resources both return 403 auth.forbidden. Restore leaves night audits paused and expires pending webhook deliveries older than 246,960 seconds; the delivery status enum includes expired.
  • Browser workspaces.list now includes reachable workspaces across organizations, with required organization identifiers/names and archival state, ordered by organization creation then workspace creation (IDs break ties). Archived recovery entries require configuration or restore authority. Browser workspace selection resolves the requested workspace’s live membership; device sessions remain bound to their issuing organization and API keys to their workspace.

API revision 6.5.0 integrates Messages with the wave-2 API platform, archived-workspace policy and effect declarations.

API revision 6.6.0 adds housekeeping planning, automatic assignment and checklists (ADR 0046, housekeeping). Every new operation has the housekeeping tag and accepts a browser session, a device token or an integration’s credential.

  • Rosters: GET/PUT /properties/{property_id}/housekeeping_rosters/{date} (housekeepingRosters.get, update) read with inventory.read and replace with housekeeping.assign the members who work on a day, from the business date to 30 days after it: up to 100 distinct members holding housekeeping.update, each with starts_at (HH:MM), capacity_credits (1–999) and a section (the rooms’ floor or section) or null. A day nobody planned answers no staff at version 1.
  • Credits: GET/PUT /properties/{property_id}/housekeeping_credits (housekeepingCredits.get, update) read with inventory.read and replace with housekeeping.configure up to 200 rules of 0–100 credits per task type and room type (null for any room type); the answer also lists the default credits of each task type.
  • Automatic assignment: GET …/housekeeping_assignment_plans/{date} (housekeepingAssignmentPlans.get, housekeeping.assign) previews the plan without writing, with the members’ kept and planned credits, the changes and the tasks left unassigned (no_staff, no_capacity), and a basis (also its ETag). POST …/housekeeping_assignment_runs (housekeepingAssignmentRuns.create, transition) applies it with {date, basis} and answers 201 with the run and its Location; a different plan is 409 housekeeping.plan_changed, a plan with no change 409 housekeeping.plan_empty. POST …/housekeeping_assignment_runs/{run_id}/undo (undo, transition) restores each task’s previous assignee exactly, or answers 409 housekeeping.undo_conflict. GET …/housekeeping_assignment_runs?date= (list) and GET …/housekeeping_assignment_runs/{run_id} (get) read runs with inventory.read; a run’s ETag is "1", or "2" once undone.
  • Checklists: GET/POST …/housekeeping_checklists and GET/PUT/DELETE …/housekeeping_checklists/{checklist_id} (housekeepingChecklists.list, create, get, update, remove; writes need housekeeping.configure, PUT and DELETE take If-Match) manage one template per task type and room type, with 1–50 check or inspection items. A new task gets a copy of the template that applies when it is created; later edits never change it. GET …/housekeeping_tasks/{id}/checks (housekeepingTaskChecks.list) answers the task with its items, and PUT …/housekeeping_tasks/{id}/checks/{key} (answer, housekeeping.update, the task’s If-Match) answers one item; the first failed of an inspection item requires a follow_up task for the room, or a fault report that opens a maintenance ticket and also needs maintenance.manage.
  • HousekeepingTask gains assigned_automatically, pinned, unit_section, room_type_id, the frozen checklist summary (required_open among it) and the origin checklist. housekeepingTasks.update accepts pinned (with housekeeping.assign); a pinned task keeps its assignee, or none, whatever automatic assignment plans. A manual assignment, single or bulk, is never moved by automatic assignment. Setting done, single or bulk, on a task with required items unanswered is 409 housekeeping.checklist_incomplete.
  • New problem codes: housekeeping.checklist_incomplete (409), housekeeping.checklist_not_found (404), housekeeping.check_not_found (404), housekeeping.checklist_duplicate (409), housekeeping.plan_changed (409), housekeeping.plan_empty (409), housekeeping.run_not_found (404) and housekeeping.undo_conflict (409); housekeeping.invalid names the new fields staff, rules, name, items, result and follow_up, and date in the path of a roster or a plan.
  • Events: housekeeping_task.created@1, housekeeping_task.assigned@1 and housekeeping_task.status_changed@1 (object housekeeping_task) are published for every task creation, assignment change (manual, bulk, automatic and undo) and status change.

API revision 6.7.0 adds occupancy-based pricing, child age bands and packages to rate plans (ADR 0027, rates, folios). Plan settings are integer minor units of the property’s currency, as derivation.amount is; every other amount stays a decimal-string money object.

  • RatePlanInput and RatePlan gain occupancy (null, or { base, max, curve } with curve { kind: "per_adult", extra_adult, fewer_adult } or { kind: "explicit", adjustments }), age_bands ([{ category, max_age, nightly_fee }], infant or child, ages 0–17) and package_items ([{ extra_id, quantity, basis }], per_night, per_person_night or per_stay, at most 10), and distribution ({ scope: "direct" }, never published, or { scope: "channels", channels } with booking_com, expedia, agoda, airbnb, hostelworld; empty when not declared). On ratePlans.create the four members are optional (no curve, bands or package; channels not declared). On ratePlans.update (RatePlanUpdate) they are optional without a default: a member left out keeps the stored value, so a client of an earlier revision changes nothing it does not know; an explicit null or [] clears it. sell_mode: per_person is now accepted and requires occupancy; derived plans follow their root.
  • ratePlans.create and ratePlans.update answer rate_plan.not_distributable (422, field) for a setting Channex or the channels cannot represent exactly: more than 18 adult counts, more than one infant or child band or an infant band above the child band, a curve, derivation or child fee a declared channel (or, undeclared, any channel) cannot carry (occupancy/curve, derivation, distribution, age_bands), or bands whose ages differ from the property’s Channex hotel policy or that GuestCraft has not read (age_bands); ratePlans.update answers rate_plan.channex_locked (409, field) when a plan published to Channex, or the root of a published derived plan, changes what its Channex plan holds: the sale basis, the highest adult count or the age bands (ages and fees), its sale through Channex (distribution), or moves a published derived plan to a parent whose root holds other settings (derivation/parent_id); changing the sale basis of a plan with derived plans answers rates.invalid on sell_mode. rate_plan.unsupported_sell_mode is no longer answered by any operation.
  • RateDay gains occupancy_prices ([{ occupancy, price }]) on per-person plans’ days of rates.get.
  • QuoteInput gains child_ages; QuoteReason gains child_ages_required, child_age_unpriced and package_exceeds_price. Quote nights and reservation room nights (with folios.read) gain package: the services (extra_id, name, tax_category, quantity, amount, the sold basis and the source of the night’s price, rate_plan or channel) the night’s price includes. RateQuote gains fingerprint, and the room of a validate_only move or resize that re-quotes it gains quoted_fingerprint. A room input and a quoted move or resize accept quoted_fingerprint, require it when the quoted or sold nights include a package, and refuse an offer that changed since the quote, or a package offer without it, with quote.changed (412) even at the same nightly amounts. A room with price_overridden keeps the price entered but takes the package of the offer quoted, so it follows the same rule with that offer’s quoted_fingerprint.
  • ReservationRoomInput and ReservationRoomEditInput gain child_ages; ReservationRoom gains child_ages (empty when none were given). On a plan priced by occupancy, an edit of an expected stay that changes the occupants without rate_plan_id or a price answers quote.required (409) on reservationRooms.update.
  • FolioMovement.source gains package: a service a room’s price includes, posted apart from its accommodation under the extra’s tax category, so the revenue report shows it as its own category. FolioPostExtra gains beyond_package; without it folios.postExtra answers folio.included_in_package (409) for an extra a room’s package includes.
  • OtaBooking gains package_review ([{ booking_room_id, reason }], absent when empty): the channel’s rooms whose price import kept whole as accommodation because it could not split out a package, with the reason unknown_rate_plan or package_not_allocated.
  • AriSettings (channex.ari.health) gains age_policy (the Channex hotel policy’s infant_max_age and children_max_age, null without a policy, absent until read) and plan_issues ([{ plan_id, room_type_id, code, field, withheld }]): the mappings a plan issue withholds, and how (stop_sell, not_sent, or room_type_closed for a Channex plan inheriting values). Only that mapping is withheld; every other plan and room type keeps publishing, and the drift check no longer waits with publication_closed for a plan issue. channex.ari.map answers rate_plan.not_distributable (422) with plan_id and field for a mapped plan’s setting that cannot be published exactly, its error pointing at the plan’s mapping. A sync_failure alert gains the cause plans_withheld.

API revision 6.8.0 selects the Channex environment per workspace. GET /channex/connection returns environment: staging for a sandbox workspace and production otherwise, or the environment of a key saved before the selection existed, and state: environment_mismatch for a sandbox key of production, which makes no Channex call except an unlink’s own cleanup. Sync health returns connection_state, and the alerts a sync_failure with the code environment_mismatch. PUT /channex/connection verifies the key in that environment only.

Like 4.2.0 and 4.5.0, this revision gives a refusal its own problem code, with the status it had. A client that branched on the old code must be updated; one that branches on the status alone needs no change.

Operation Refusal Code before Code now
channex.connection.put Channex refused the key (401, 403 or 404) request.invalid (400) channex.key_refused (400), with environment

request.invalid from that operation now means only a malformed key. See Channex certification and ADR 0050.

API revision 6.9.0 adds receivables (ADR 0051, guide), cancellation trends and consolidated reports (ADR 0031):

  • Counterparties, the organization’s directory reached from its active workspaces: counterparties.list|create|get|replace (strong ETag/If-Match on replace), the non-fiscal statement counterparties.statement with counterparties.statementCsv, and counterparties.exposure: what a counterparty owes per workspace and currency, every active workspace of the organization for its owners and administrators in a browser, else the caller’s.
  • Open items receivableItems.list|get|writeOff, receipts receivableReceipts.list|create|get|allocate|void, receivableEntries.release, and the aging receivables.aging with receivables.agingCsv. Writes that move money take a posting_key; item and receipt reads answer an ETag that changes with their append-only entries.
  • reservationFolios.linkCounterparty|unlinkCounterparty (PUT/DELETE …/folios/{folio_id}/counterparty).
  • reservationFolios.close moves a linked folio’s owed balance to its counterparty (receivables.transfer, when the counterparty allows direct billing, else 409 counterparty.direct_billing_not_allowed), with accept_credit_limit and the problems receivable.credit_limit (409, members exposure, limit, amount only for a member who reaches every property), counterparty.not_found and counterparty.archived; reservationFolios.reopen answers receivable.item_applied.
  • FolioMovement.kind gains receivable_transfer and receivable_item_id; ReservationFolio gains counterparty_id; CashShiftReport gains receipts.
  • Events receivable.*@1 (objects receivable_item, receivable_receipt) and permissions receivables.read, receivables.manage, receivables.write_off and receivables.transfer; Counterparty.direct_billing_allowed.
  • reports.cancellations with reports.cancellationsCsv: cancellation trends of a property by cancellation date, lead time, channel or cause, with cancelled nights and the frozen value lost. Cancellations before capture started count as unknown (reports).
  • reports.consolidated with reports.consolidatedCsv: the workspace’s properties together, per currency or converted to reporting_currency with stored exchange rates (422 exchange_rate.missing, members currencies and reporting_currency). reports.organizationConsolidated with reports.organizationConsolidatedCsv: the same across the organization’s active workspaces (sandbox only with include_sandbox), for its owners and administrators in a browser.
  • exchangeRates.list|create|delete: the organization’s stored rates, one per currency pair, day and source; create and delete need exchange_rates.manage in a browser (problems exchange_rate.duplicate, exchange_rate.not_found, exchange_rate.version_mismatch).

API revision 6.10.0 adds guest data rights (ADR 0052) under the privacy tag. Every operation takes a browser session only (an API key, organization token, client session or device token answers 401 auth.unauthenticated), and its grant is a workspace grant that members restricted to selected properties never hold:

  • GET/PUT /guest_data_retention (guestDataRetention.get, guestDataRetention.update, workspaces.configure) reads and replaces the data-protection jurisdiction and a period in days (1–3650, or null for the default) for guest_profile, stay_personal_data, booking_revision_archive, audit_pseudonymization, exports and maintenance_photos (photos of maintenance tickets; starts_at closure, the ticket’s closing). The answer adds the defaults with their source (workspace, pack or generic) and the legal sources of the pack’s rule, the effective periods, the Messages content period, the technical periods and a pack’s authority periods. Strong ETag/If-Match; a stale version answers 412 privacy.version_mismatch.
  • GET/POST /guests/{id}/legal_holds and POST /guests/{id}/legal_holds/{hold_id}/release (guestLegalHolds.list, .create, .release; guests.legal_hold, and for the list also guests.erase or guests.export) place and release a hold with a reason. One hold is active at a time (409 guest.legal_hold_active); releasing another answers 404 guest.legal_hold_not_found.
  • POST /guests/{id}/erase (guestErasure.create, guests.erase) takes {"confirm": true} and the guest’s If-Match, anonymizes the guest and its merged identities, and answers the completed request, guest_version (also the new ETag), retained and upstream codes. A stay that has not ended or an active legal hold answers 409 guest.erasure_blocked with reason (active_stay with reservation_ids, or legal_hold with hold_id). The request’s counts include quarantined_revisions, the stays’ quarantined Channex revisions the erasure resolved; their receipts become stale with reason personal_data_erased.
  • POST /guests/{id}/data_exports (guestDataExports.create, guests.export) answers 202 with the queued request and its Location. GET /guest_data_requests (cursor pages, optional guest_id) and GET /guest_data_requests/{id} (guests.erase or guests.export) read the requests; unknown ones answer 404 privacy.request_not_found. A request carries a version that every state change raises; the single read answers it as a strong ETag. A queued export whose job never completes fails after 24 hours with failure not_prepared.
  • POST /guest_data_requests/{id}/download (guestDataRequests.download, guests.export) answers a ready export’s application/zip archive once, as an attachment with Cache-Control: no-store; afterwards, or while it is not ready, expired, erased or failed, it answers 409 privacy.export_unavailable with that reason.
  • Guest gains erased_at (null while the profile holds personal data), and the permission catalog gains guests.erase, guests.export and guests.legal_hold.
  • messages.erase is no longer an API key or organization token permission: it leaves the ApiKeyPermission and OrganizationTokenPermission enums, like the three new grants. Its only operation, DELETE /message_guests/{guest_id}/content, already required a browser session; no version has been released, so no integration held it.

API revision 6.11.0 adds keyset paging and exports to Activity, and account security. Every change is additive: a client written for an earlier 6.x revision keeps working unchanged.

  • GET /audit (team.audit) adds the opaque keyset_cursor parameter and the answer members next_keyset_cursor (null on the last page) and has_more, optional in the contract so that a client still reads an earlier server, and always sent by this one. Pages hold 50 entries in descending (occurred_at,id) order; the keyset cursor binds the workspace, the exact filters and the order, keeps microseconds, and is decoded only after authentication, so an entry written between two requests no longer shifts later pages. A foreign, changed-filter or malformed keyset cursor answers 400 cursor.invalid.
  • Deprecated: the numeric cursor offset of GET /audit and the numeric next_cursor it answers keep their 6.6.0 meaning (the entries to skip, up to 100,000; next_cursor is given when no keyset cursor is sent, null otherwise and on the last page). Use keyset_cursor instead. A request with both cursor and keyset_cursor answers 400 request.invalid at /keyset_cursor.
  • GET /audit/export_csv and GET /audit/export_json (team.auditExportCsv, team.auditExportJson, audit.view, browser session only) and GET /organizations/{organization_id}/audit/export_csv and export_json (organizations.auditExportCsv, organizations.auditExportJson, organization owners and administrators) download the entries that match the list’s filters. from and until are required and at most 366 days apart (400 request.invalid at /from or /until). The selection is read in one statement; more than 10,000 entries, or more than 20 MiB counted on the stored entries before any is returned (each entry’s data with its quotes doubled for CSV, and 512 bytes for its other fields), answer the new 422 activity.export_too_large with max_entries and max_bytes members, never a truncated file. The file is streamed. CSV columns are occurred_at, id, action, actor_type, actor, api_key_id, organization_token_id, client_session_id, subject, data (RFC 4180, formula-guarded text, data as compact JSON); JSON is { "data": [...] } with the list’s entry shape. Both carry Content-Disposition: attachment; filename="activity-<from date>-<until date>.<csv|json>" and Cache-Control: no-store. Each export records audit.exported with its format, count and filters, in the transaction that read it.

Account security (ADR 0053, guide):

  • accountSecurity.getPolicy and accountSecurity.updatePolicy (GET/PUT /organizations/{organization_id}/security_policy, strong If-Match on the policy version, "0" before the first change): the organization’s requirement (none, second_factor, passkey), grace days, remembered browsers and session age. Owners and administrators. The answer also lists passkeys_unavailable_on (optional in the contract, always sent): the instance’s addresses trusted for sign-in where passkeys cannot work (other_domain, ip_address, not_secure), outside the policy’s version; a passkey ceremony from one of them answers 403 PASSKEY_ORIGIN_UNAVAILABLE on /api/auth.
  • accountSecurity.compliance (GET /organizations/{organization_id}/security_compliance): members’ factors and status, and the API keys and organization tokens of non-compliant creators.
  • accountSecurity.issuePasswordReset and accountSecurity.withdrawPasswordReset (POST/DELETE /organizations/{organization_id}/members/{user_id}/password_reset): a one-time link, 60 minutes, for a member of a lower role who belongs to no other organization; 5 per member and 30 per issuer an hour.
  • accountSecurity.redeemPasswordReset (POST /password_resets/redeem, public, instance Origin): sets the password and ends every session of the person; every unusable link answers password_reset.invalid; 10 attempts per address a minute.
  • Every operation of a browser session can answer 403 auth.step_up_required (members requirement, organization_id) in an organization whose policy the session does not meet; every operation of a device token can answer 401 auth.reauthentication_required. The Problem schema declares both members.
  • New problems: security.invalid, security.precondition_failed, security.member_not_found, password_reset.not_permitted, password_reset.operator_only, password_reset.not_found, password_reset.rate_limited (with Retry-After), password_reset.invalid.
  • SDK: GuestCraftError.retryAfter carries the seconds of Retry-After; the accountSecurity facade.

API revision 6.12.0 (2 November 2026) adds OAuth applications, organization permissions, narrower client sessions and event versions (ADR 0047 and ADR 0040, amendments of 1 November 2026). It is an addition-only revision: every operation, parameter, member and type of 6.11.0 keeps its shape and meaning, which packages/api/src/contract-compatibility.unit.test.ts checks against the 6.11.0 contract. As in 6.2.0 and 6.3.0, closed enums of answers gain values, which an older validator refuses until the SDK is regenerated: the permission code oauth_applications.manage (in the permissions of workspaces, roles and the role matrix) and the audit actor type oauth_application. The members added to existing answers (scope, oauth_grant_id, organization_permissions, grantable_organization_permissions, event_versions, deprecated_event_types and backoff_until) are optional in the contract, so this SDK still reads an older server’s answer; this revision always sends them.

  • OAuth applications. The oauthApplication bearer scheme (gc_oat_…) on every operation that lists apiKey, except minting client sessions. GET /authorized_applications (authorizedApplications.list), GET /authorized_applications/{id} (authorizedApplications.get) and POST /authorized_applications/{id}/revoke (authorizedApplications.revoke, If-Match), browser-only, with the new permission oauth_applications.manage; problems oauth_application.not_found (404) and oauth_application.version_mismatch (412), and oauth_application.invalid (422, field) for a refused consent. Audit entries gain actor_type oauth_application and the member oauth_grant_id (null for every other actor). The OAuth endpoints themselves are Better Auth’s under /api/auth/oauth2/…; the discovery documents are /.well-known/oauth-authorization-server/api/auth (RFC 8414) and /.well-known/oauth-protected-resource/api/v1 and …/api/mcp (RFC 9728). See OAuth applications.
  • Organization permissions. OrganizationToken.organization_permissions, the optional input member of the same name (default []), OrganizationTokenPage.grantable_organization_permissions and the OrganizationPermission component (workspaces.create, organization_audit.view). workspaces.create and organizations.audit also accept the organizationToken scheme, and answer it 429 auth.rate_limited when its budget is spent; a browser session calls them as before.
  • Client session scopes. clientSessions.create takes an optional scope (ClientSessionScope: properties, reservation, guest or property_public) beside property_ids, which stays required as in 6.6.0. Without scope, property_ids is 1 to 100 properties, as before; with it, property_ids must be the properties the scope names (the same list for properties, [property_id] for reservation and property_public, [] for guest), and any other list answers client_session.invalid with field property_ids. Sessions answer their scope; property_ids keeps its type and meaning, the properties the session reaches: those of its scope, and [] for a session of one guest, which names no property. A scoped session answers 403 auth.forbidden on every operation outside its scope.
  • Event versions. GET /event_types (eventTypes.list, public, cacheable for five minutes) lists every version of the catalog with its object and state (current, or deprecated with supported_until and successor). Webhook endpoints gain event_versions, an additive map of event family to the version delivered, which their creation and edits also accept; event_types keeps its 6.6.0 shape (families, by their first version’s name), and a family subscribed without a version is pinned to the version current when it was subscribed. The map is the WebhookEventVersions component; webhook_endpoint.invalid gains the reasons later_version (on event_types, a later version named instead of the family) and not_subscribed, unknown_version and deprecated_version (on event_versions). A replay sends each family in its pinned version; a queued delivery or a redelivery keeps the version it was queued with. Endpoints also gain deprecated_event_types and backoff_until.
  • Operation permissions. Every operation that an OAuth application may call declares the permissions it needs (any one of them) as the vendor extension x-guestcraft-permissions, which the built-in MCP server reads to answer insufficient_scope (RFC 6750) when a token’s scopes lack them.
  • Activity exports. The CSV export of workspace and organization Activity gains the column oauth_grant_id after client_session_id, and the JSON export the member, as in the list.

API revision 6.13.0 adds online payments through the workspace’s own Stripe account (ADR 0054). GuestCraft sends no e-mail or SMS: a payment link is GuestCraft’s own address with its own expiry, which staff copy or show as a QR code. Outcomes are settled only by the provider’s signed events or provider reads, never by the guest’s browser or by these calls, so the calls that ask the provider for money answer 202. Money is a decimal string with its currency; a card is only brand, last4, exp_month and exp_year.

  • GET/PUT/DELETE /payment_connection (paymentConnection.get, .update, .delete) and POST /payment_connection/verify (paymentConnection.verify), browser session only, with payments.configure and unrestricted property access. The ETag is the connection’s version, "0" before the first connection; the writes take it in If-Match (PUT accepts "0"). PUT takes a restricted secret key, the webhook signing secret (both write-only, never returned) and link_lifetime_days. Every answer carries webhook_url, the workspace’s address to register with the provider, also before connecting. DELETE answers the disconnected connection (200).
  • A connected PaymentConnection carries pending (PaymentPendingWork): the open links, the payments and holds and the refunds the provider has not settled, the signed deliveries received and not processed yet, and the failed_deliveries that failed every processing attempt. DELETE answers payments.connection_in_use and a key of another account payments.account_changed while links, payments, refunds or deliveries are pending; failed deliveries alone do not keep the connection.
  • POST /payment_connection/deliveries/retry (paymentConnection.retryDeliveries), browser session only, payments.configure, If-Match with the connection’s version: offers the failed deliveries to processing again with fresh attempts and answers the connection (200, ETag). Problems payments.not_connected (409), payments.version_mismatch (412) and auth.forbidden.
  • GET /properties/{property_id}/reservations/{id}/payments (reservationPayments.get, folios.read): links, provider payments with refunds, saved cards, off_platform and what each purpose may still collect.
  • POST /properties/{property_id}/reservations/{id}/payment_requests (paymentRequests.create, payments.collect, 201 with Location and ETag), GET /properties/{property_id}/payment_requests/{id} (paymentRequests.get, folios.read, ETag) and POST …/payment_requests/{id}/cancel (paymentRequests.cancel, payments.collect, If-Match). The guest’s button on the link’s page (POST /pay/{token}, outside the API) makes at most one provider page at a time: while another opening of the same link is asking the provider for it, the page answers 200 saying the payment page is being prepared, with the button again.
  • POST /properties/{property_id}/reservations/{id}/card_charges (cardCharges.create, payments.collect, 202 with Location and ETag). Its posting_key makes a resend answer the same charge; the same key for another charge answers 409 payments.key_conflict.
  • GET /properties/{property_id}/provider_payments/{id} (providerPayments.get, folios.read, ETag), POST …/provider_payments/{id}/capture (providerPayments.capture, payments.collect, If-Match, 202: posts the damage charge, the catalog extra times quantity, and captures exactly its amount, at most what the hold still holds) and POST …/provider_payments/{id}/release (providerPayments.release, payments.collect, If-Match, 202).
  • POST /properties/{property_id}/provider_payments/{id}/refunds (providerRefunds.create, payments.refund, 202, Location the provider payment), with a posting_key like charges.
  • GET /properties/{property_id}/payments_due?through=YYYY-MM-DD (paymentsDue.list, policies.prepayments, amounts included).
  • POST /properties/{property_id}/reservations/{id}/cash_settlements (cashSettlements.create, 201): settles a folio wholly in cash with the country pack’s cash rounding; folios.post_payment for kind: payment, folios.refund for kind: refund.
  • OTA booking cards through Channex’s Stripe Tokenization App (payments): ReservationPayments.ota_card (ReservationOtaCard, null for a direct booking) carries who collects (collect), the payment type, the booking’s card as safe facts (OtaBookingCard: brand, last4, exp_month, exp_year, virtual and, for an OTA’s virtual card, virtual_card with currency, its initial balance, effective_on and expires_on; unknown values are null), the card moved to the provider (id, card, moved_at), what a virtual card can still pay (remaining: its initial balance less GuestCraft’s own charges of it) and why it cannot be moved or charged now (blocked: a reason of payments.ota_card_unavailable, tokenization_inactive, tokenization_missing or off_platform).
  • POST /properties/{property_id}/reservations/{id}/ota_card/move (otaCards.move, payments.collect, 200 OtaCard) asks Channex for the Stripe payment method of the booking’s card and keeps it once it is a card of the workspace’s own account, in its mode, with the booking’s last four digits. Unknown or malformed last four digits refuse the move before Channex is asked (payments.ota_card_unavailable, reason unverifiable); Channex’s 403 (the app is not installed on the property) answers payments.tokenization_missing and is recorded as the tokenization’s missing_at.
  • POST /properties/{property_id}/reservations/{id}/ota_card/charges (otaCardCharges.create, payments.collect, 202 with Location and ETag) takes posting_key and amount like cardCharges.create and charges the moved card toward the folio’s balance.
  • ProviderPayment: origin can also be ota_card; the new method (card_online or ota_virtual_card) is how the folio names the money, and ota_card_id names the moved card a charge used (null otherwise).
  • GET /properties/{property_id}/card_tokenization (cardTokenization.get) and POST …/card_tokenization/acknowledge ({ "installed_and_cost_accepted": true }) and …/stop (cardTokenization.acknowledge, .stop): browser session only and payments.configure, like paymentConnection. The operator installs the Stripe Tokenization App in Channex’s interface; GuestCraft never installs or removes it. They answer CardTokenization (state inactive or active, linked, acknowledged_at, missing_at, version), with the version as ETag, "0" before the first acknowledgement; the writes take it in If-Match.
  • New problems: payments.cost_not_acknowledged (422), payments.channex_not_linked, payments.tokenization_inactive and payments.tokenization_missing (409), and payments.ota_card_unavailable (409, reason no_card, unverifiable, not_moved, foreign, mismatch, not_effective, expired, balance, currency or unknown).
  • Problems: payments.invalid (422, field-linked, with field and reason), payments.not_connected (409), payments.connection_in_use (409), payments.provider_unavailable (503, Retry-After: 5), payments.request_not_found, payments.payment_not_found and payments.card_not_found (404), payments.off_platform (409, reason airbnb, ota_collects or collector_unknown), payments.open_request_exists, payments.in_progress, payments.invalid_state, payments.nothing_due and payments.account_changed (409), payments.amount_exceeded (422) and payments.version_mismatch (412, with current_version and the current ETag).
  • folios.refund answers folio.provider_payment (409) for a payment the provider collected: it is refunded through providerRefunds.create.
  • FolioMovement: method can also read card_online, ota_virtual_card and cash_rounding (staff still record only the four manual methods), source can be provider or rounding, and a provider movement carries provider, provider_reference and, when it came from a link, payment_request_id.
  • PrepaymentStatus gains guarantee_source (provider or operator); a reservation room’s guarantee_tracking is now RoomGuaranteeTracking, with source. Both are optional, because servers before this revision omit them; a client reads a missing one as operator.
  • Property gains country (ISO 3166-1 alpha-2 or null), which PropertySettings takes optionally: omitted keeps it, null clears it, an unknown code answers property.invalid.
  • The new grants payments.configure (browser only, never held by API keys or organization tokens), payments.collect and payments.refund join the permission catalog.

API revision 6.14.0 adds enterprise identity (ADR 0055): verified domains, single sign-on per organization and SCIM 2.0 provisioning. Every change is additive: a client written for 6.13.0 keeps working unchanged.

Every operation below is an organization owner’s or administrator’s, from a browser session that meets the organization’s policy (API keys and device tokens are refused); writes need the instance Origin header. Each can answer request.invalid, auth.unauthenticated, auth.forbidden (also for an unknown organization) and auth.step_up_required, besides the problems listed.

  • Domains (/organizations/{organization_id}/domains):
    • enterpriseIdentity.listDomains (GET, read).
    • enterpriseIdentity.addDomain (POST { domain }, create; 201 with Location and ETag): a registrable domain, pending, with the TXT record to publish at _guestcraft-verification.<domain>. domain.invalid, enterprise_identity.invalid (already added, at /domain), enterprise_identity.limit_reached (20 domains).
    • enterpriseIdentity.getDomain (GET /{domain_id}, read, ETag): enterprise_identity.not_found.
    • enterpriseIdentity.verifyDomain (POST /{domain_id}/verify, transition): looks the record up now. enterprise_identity.not_found, domain.record_not_found (422; also when the lookup failed), domain.unavailable (409: another organization proved it).
    • enterpriseIdentity.releaseDomain (DELETE /{domain_id}, If-Match, delete, 204): ends the single sign-on sessions it served. enterprise_identity.not_found, enterprise_identity.precondition_failed.
    • A proved domain is checked again every 24 hours; a missing record makes it failing (with failing_since and lapses_at) for 7 days, then lapsed.
  • Single sign-on connections (/organizations/{organization_id}/sso_connections), body SsoConnectionSettings: name, enabled, protocol (oidc with oidc { issuer, client_id, client_secret? }, or saml with saml { entity_id, sso_url, certificates[] }), domain_ids[], mfa { trusted, amr[], acr[] }, default_grants[{ workspace_id, role, property_ids? }] (WorkspaceGrant: a built-in workspace role or a custom role’s rol_ identifier, at every property of the workspace when property_ids is absent or null, or at 1–100 of its prop_ properties, as a member’s access; answers always carry property_ids, null for every property). The client secret is write-only; answers say client_secret_set. Answers (SsoConnection) also carry service_provider (OIDC redirect URI; SAML entity ID, ACS and metadata URLs) and active.
    • enterpriseIdentity.listConnections (GET, read).
    • enterpriseIdentity.createConnection (POST, update: it assigns the listed domains, whose ETags change; 201 with Location and ETag): enterprise_identity.invalid, enterprise_identity.limit_reached (10 connections), sso.discovery_failed.
    • enterpriseIdentity.getConnection (GET /{connection_id}, read, ETag): enterprise_identity.not_found.
    • enterpriseIdentity.updateConnection (PUT /{connection_id}, If-Match, update): a secret left out keeps the stored one; the protocol and the identity provider never change. enterprise_identity.invalid, enterprise_identity.not_found, enterprise_identity.precondition_failed, sso.discovery_failed.
    • enterpriseIdentity.deleteConnection (DELETE /{connection_id}, If-Match, delete, 204): enterprise_identity.not_found, enterprise_identity.precondition_failed.
  • SCIM (/organizations/{organization_id}/...):
    • enterpriseIdentity.listScimTokens (GET /scim_tokens, read): the tokens and base_url.
    • enterpriseIdentity.createScimToken (POST /scim_tokens { name, expires_in_days, scopes }, create, 201): { token, scim_token }, the token shown once; 1 to 730 days, 365 by default. scopes is required: one or more of scim.users.read, scim.users.write, scim.groups.read, scim.groups.write, none twice; every token (ScimToken) answers its scopes. enterprise_identity.invalid (also at /scopes), enterprise_identity.limit_reached (5 live tokens).
    • enterpriseIdentity.revokeScimToken (POST /scim_tokens/{token_id}/revoke, transition, 200 with the revoked token, like API keys and organization tokens): enterprise_identity.not_found.
    • enterpriseIdentity.listScimGroups (GET /scim_groups?cursor=, read): pages with next_cursor. enterprise_identity.invalid at /cursor.
    • enterpriseIdentity.updateScimGroupAccess (PUT /scim_groups/{group_id}/access { grants } of WorkspaceGrant, If-Match with the group’s etag, update): reconciles its members’ SCIM-managed workspace roles and their properties (several groups granting the same role give the union of their properties, every property absorbing a list). enterprise_identity.invalid (also at /grants/{n}/property_ids/{m} for a property that is not the workspace’s, or listed twice), enterprise_identity.not_found, enterprise_identity.precondition_failed.
    • enterpriseIdentity.listScimUsers (GET /scim_users?cursor=, read): pages with next_cursor. enterprise_identity.invalid at /cursor.
  • New problems: enterprise_identity.invalid (422; the field is the pointer of its one errors entry), enterprise_identity.not_found (404), enterprise_identity.precondition_failed (412), enterprise_identity.limit_reached (409), domain.invalid (422), domain.unavailable (409), domain.record_not_found (422), sso.discovery_failed (422).
  • New routes outside /v1, under /api/auth:
    • POST /api/auth/sso/discover { email } answers { "sso": true, "url" }, the identity provider to send the browser to, for an address whose proved domain an enabled connection serves; every other address, known or not, gets the same { "sso": false }. Every answer takes at least 300 ms. A SAML hit also sets the __Host-guestcraft.saml_flow cookie (Secure, HttpOnly, SameSite=None), which binds the sign-in to this browser.
    • POST /api/auth/sso/link-preview { token } answers { organization_name, connection_name }, what a waiting link asks, for the page where its person confirms it; 400 SSO_LINK_INVALID once it cannot be confirmed.
    • POST /api/auth/sso/link { token }, with a fresh session of the account it names: links an existing account to its single sign-on identity after the person confirmed with their current credentials. The connection, its proved domain and the person’s SCIM state are checked again then; otherwise 400 SSO_LINK_INVALID.
    • The identity provider’s callbacks: GET /api/auth/sso/callback/{connection_id} (OIDC), POST /api/auth/sso/saml2/sp/acs/{connection_id} (SAML Assertion Consumer Service), and GET /api/auth/sso/saml2/sp/metadata?providerId={connection_id} (SAML service provider metadata). Identity-provider-initiated SAML is refused. A refused sign-in returns to /sign-in?error= with one of SSO_CONNECTION_UNAVAILABLE, SSO_DOMAIN_NOT_VERIFIED, SSO_NONCE_INVALID, SSO_ACCOUNT_DEACTIVATED, SSO_NOT_A_MEMBER (a linked person who is no longer a member: an administrator removed them), SSO_ACCOUNT_EXISTS, SSO_LINK_CONFIRMATION_REQUIRED, SSO_PROVISIONING_FAILED, SSO_UNSOLICITED_RESPONSE, SSO_STARTED_ELSEWHERE (a SAML response for a sign-in this browser did not start, or whose start expired or was spent) or SSO_RESPONSE_INVALID (a SAML response without this ACS as its Destination, or outside its validity window).
    • SCIM 2.0 (RFC 7643, RFC 7644) at /api/auth/scim/v2, with a SCIM token as bearer: Users and Groups (GET, POST; GET, PUT, PATCH, DELETE on an item), each needing the matching scope of the token (403 otherwise), and ServiceProviderConfig, Schemas, ResourceTypes, which need none. Requests are limited to 600 a minute per client address, per token and per organization, each on its own (429 with Retry-After).
  • GET /api/internal/domains/check, with the scheduler’s CRON_SECRET bearer like /api/internal/channex/drain, runs the due 24-hour domain checks ({ "checked": n }); Vercel calls it daily, and ordinary traffic wakes the same run at most once an hour per process.
  • SDK: the enterpriseIdentity facade.

API revision 6.15.0 adds the retirement of live Channex targets (ADR 0034, publication), rate-plan sales caps (ADR 0027, rates), reports of OTA bookings to their channel through Channex’s Booking Reporting API (ADR 0032, booking reports) and Messages history paging (ADR 0044, guest messaging). The members it adds to existing answers (RateDay.max_sell, RateWindow.sales, AriHealth.unattributed_nights and AriHealth.retirement, MessageThreadDetail.messages_next_cursor) are optional in the contract, so this SDK still reads an older server’s answer; this revision always sends them.

  • max_sell, a seventh restriction: the most rooms a base plan sells on a night through every channel together, null for no cap (0 sells none). It is in RateDay (calendar reads, the Undo payload), in the bulk restrictions of PUT /properties/{property_id}/rates (omitted, it stays as it is) and in restore cells (omitted, the cell restores no cap). Derived plans inherit it and count their own sales. RateWindow.sales lists each plan’s sold rooms per night and the OTA rooms whose Channex plan is unmapped (unattributed), which count against no cap.

  • Quotes answer the reason max_sell for a plan that sold its cap on a night. Saving a reservation change that sells a plan beyond its cap answers rate_plan.max_sell_reached (409) with max_sell_nights (plan_id, date, max_sell, sold), on every operation that saves a reservation. ReservationRoomInput.max_sell_override_reason sells beyond the cap with the new permission reservations.override_sales_cap (built-in: administrators), audited as reservation.max_sell_overridden.

  • Booking reports have the reservations tag and accept a browser session, a device token or an integration’s credential. GET /properties/{property_id}/reservations/{id}/ota_reports (reservations.listOtaReports, reservations.read) lists the three reports, invalid_card, cancel_due_invalid_card and no_show: available, the problem code a request would answer now (refusal), the channel’s window (opens_at, closes_at excluded) in the property’s time zone, the local no-show’s fee state (no_show) and the stored report (state sending, sent, failed or unknown, failure, attempts). Penalty amounts need folios.read.

  • POST …/ota_reports/{kind} (reservations.reportToOta, transition, reservations.report_to_ota) sends the report once; repeating it answers the stored report without a second call. A no-show takes optional waived_fees and, for Expedia only, penalty_amount, both defaulting to the local no-show’s fee state.

  • POST …/ota_reports/{kind}/reconcile (reservations.reconcileOtaReport, transition, reservations.report_to_ota) records { received } for a report whose outcome is unknown.

  • New permission reservations.report_to_ota, granted to the built-in administrator and front desk roles.

  • New problem codes: ota_report.invalid (422, field), and 409 ota_report.not_ota_booking, ota_report.channel_unsupported, ota_report.property_not_linked, ota_report.reservation_cancelled, ota_report.stay_status_not_allowed, ota_report.overbooked, ota_report.invalid_card_not_reported, ota_report.window_closed (opens_at, closes_at), ota_report.outcome_unknown, ota_report.refused_by_channel (reason) and ota_report.not_unknown.

  • Event: reservation.ota_reported@1 (object reservation) when the OTA received a report, as Channex confirmed it or an operator recorded it.

  • POST /properties/{property_id}/channex/ari/retirements (channex.ari.retirements.create), browser-only with channex.configure, retires one mapped rate plan (kind: rate_plan, plan_id) or a mapped room type with its plans (kind: room_type, room_type_id), unmapped (action: unmap) or replaced (action: replace, with replacement_rate_plan_id, or replacement_room_type_id and replacement_plans). validate_only: true answers 200 with an AriRetirementPreview (the nights, the Channex targets the closure stops, the open sends and the imported bookings still to depart) and needs no If-Match; otherwise the strong mapping version is required as If-Match (412 channex.mapping_version_mismatch with the current ETag) and the answer is 201 with an AriRetirement and its Location.

  • GET /properties/{property_id}/channex/ari/retirements/{retirement_id} (channex.ari.retirements.get, channex.view_health, with a weak ETag) reads one; POST …/retirements/{retirement_id}/cancel and …/resume (channex.ari.retirements.cancel, channex.ari.retirements.resume, browser-only with channex.configure) cancel it before its mapping switched, or resume a failed one.

  • AriHealth (channex.ari.health) gains retirement: the property’s active retirement, or else its latest, or null. Its state is stopping, draining, verifying, switching (a replacement waiting for its first verified values), completed, failed (with code readback_mismatch, upstream_rejected or target_missing; the target stays closed) or cancelled.

  • New problems: channex.ari_retirement_not_live (409), channex.ari_retirement_active (409), channex.ari_retirement_last_room_type (409), channex.ari_replacement_invalid (422, with reason not_found, mapped or incompatible, and plan_id), channex.ari_retirement_not_found (404) and channex.ari_retirement_state (409, with state). A replacement’s channex.ari_replacement_invalid and rate_plan.not_distributable point at the member naming it.

  • channex.ari.map also answers channex.ari_retirement_active (409) while a retirement is active; channex.ari_mapping_live now directs to the retirement.

  • A sync_failure alert gains the cause retirement_failed.

  • channex.revision.retry accepts apply_to_former_room_type (default false): it applies a revision quarantined with the reason retired_target (a Channex room type a retirement unmapped) to the local room type that room type was mapped to; for any other receipt it answers request.invalid.

  • GET /message_threads/{thread_id}/messages (messageThreads.messages.list, read, messages.read) lists a thread’s messages from the latest backwards: limit (1–100, 20 by default) and cursor, answering ThreadMessagesPage ({ data, next_cursor, has_more }) with each page oldest first. Problems: messages.thread_not_found (404) and cursor.invalid (400) for a cursor of another thread or call.

  • MessageThreadDetail gains messages_next_cursor: the cursor of the messages older than the detail’s latest 1,000, or null when the detail begins the conversation. The detail’s messages are unchanged.