HTTP API
Questi contenuti non sono ancora disponibili nella tua lingua.
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.
Session authentication
Section titled “Session authentication”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");}Choose a workspace
Section titled “Choose a workspace”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 },});Operations
Section titled “Operations”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/healthandGET /api/v1/instance(also at/.well-known/guestcraft) are public. Health answers{ "status": "ok" }without checking the database or Channex.GET /api/v1/rolesis 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_sessionsexchanges the mobile app’s one-time sign-in code, with its PKCE verifier, for a device session;POST /api/v1/invitations/acceptuses 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-Idselects. Administration operations (team, roles, API keys, webhooks and Channex configuration) accept only a browser session; their descriptions say so. GET /api/v1/channex/connectionreads the saved connection (connected,environment,connected_at,property_count,account_modeandversion) without contacting Channex, andPUTvalidates 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.
Responses and errors
Section titled “Responses and errors”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 theenvironmentmember.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.
Changelog
Section titled “Changelog”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-Idheader 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.updatestates 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*Errorsmaps lose 10 keys at 404, 15 at 409 and 12 at 422. The exact-list test inpackages/api/src/openapi.unit.test.tsis the authoritative list. - Rates, booking import and ARI: 23 operations get individual summaries, permissions,
preconditions and exact problems; 15 generated
*Errorsmaps lose 30 impossible keys (404: 10, 409: 8, 412: 10, 422: 2); the exact status lists inpackages/api/src/openapi.unit.test.tsand rate-problem lists inpackages/api/src/routes/rates.unit.test.tsare 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 needingfolios.read; their generated*Errorsmaps lose 81 impossible keys (412: 39, 409: 25, 422: 16, 404: 1). The authoritative lists are theproblemsOfcalls 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) andDELETE /device_pairings/{id}(devicePairings.cancel, with If-Match): a signed-in browser issues a one-time pairing code for a QR code (withexpires_in, the whole seconds it has left when the answer is sent, so a client counts down whatever its own clock says), follows itsstatus(pending,paired,refused,expired) and withdraws an unused code. Browser session only; new problemsdevice_pairing.not_found(404),device_pairing.used(409) anddevice_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 answersdevice_session.invalid_requestwithfield: device_nameon 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 429auth.rate_limitedwithRetry-After; the operation also documents therequest.malformedit answers to a body that is not JSON.- Discovery lists the
mobile_pairingcapability. Unitgainshousekeeping_version, which every change of the status advances by one.PUT …/units/{id}/housekeeping(units.housekeeping) takes an optionalexpected_housekeeping_versionand an optionalexpected_updated_at; with either, a unit already in the requested status is answered unchanged and a unit whose status changed since answers 409unit.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):
WebhookEventTypegains the events of Channex imports, folios, rates and rate plans, and room housekeeping status,reservation.overbooked@1and the same-day undo of check-in and check-out; event bodies may carrydata.reservation_id, anddata.objectnames the kind ofdata.id.GET /events(events.list) andGET /events/{id}(events.get) read the catalog’s events of the last 30 days, with the new permissionevents.read, from a browser session, a device token or an API key; problemsevent.invalid(422,field) andevent.not_found(404).POST /webhook_endpoints/{id}/replays(webhookEndpoints.replay) andPOST /webhook_endpoints/{id}/deliveries/{delivery_id}/redeliver(webhookDeliveries.redeliver), browser-only; problemswebhook_replay.invalid(422,field,reason),webhook_delivery.not_found(404) andwebhook_delivery.pending(409);WebhookDeliverygainsorigin.webhook_endpoint.disabledis 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; problemapi_key.not_rotatable(409).ApiKeygainsallowed_ips,replaced_byandcreator_access, andApiKeyInputacceptsallowed_ips;api_key.invalidnamesallowed_ipsorgrace_period_hourstoo. 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) gainactor_typeandapi_key_id; the member list gainsorganization_ownerandorganization_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.listexcludes absorbed identities unlessinclude_merged=true; direct reads retain the final visiblemerged_intopointer and display name. Every guest write refuses an absorbed identity with 409guest.absorbedandsurvivor_id. A pair containing the same identity twice is 422guest.pair_invalid.- Duplicate pages exclude dismissals before paging unless
include_dismissed=trueand 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’sIf-Matchand the other guest’sother_versionquery parameter. Both dismissal directions are audited and idempotent. GET /guests/shared_contactsand/guests/{id}/shared_contactspage 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}/mergespages preserved, visible absorbed ancestry. Consent history is newest first and includesactor_name;/guests/{id}/consents/currentreturns the latest decision per purpose independently of history pages. Merges never combine consent evidence.GET /guests/{id}/contact_noticespages OTA snapshots newest first by revision time and reservation identifier descending.contactcontains 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; explicitnullproposes clearing a field. Notes are never offered. Reservations expose the separateota_contactsnapshot. 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-checkedPUTis 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,countryorlanguage). It requiresguests.manage, the guest’s strongIf-Match, and a body withrevision_idandexpected_valueexactly matching the stored snapshot field (includingnull). 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 412guest.version_mismatch; a changed, missing or inaccessible snapshot returns 409guest.contact_notice_changed. Refetch both before deciding again. Invalid domain input is 422guest.invalid, hidden/missing guest is 404guest.not_found, denied access is 403auth.forbidden, and tombstones retain the 409guest.absorbedfinal-survivor contract. Malformed JSON is 400request.malformed; invalid headers, path fields or body shape are 400request.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
housekeepingoperations: the settings (housekeepingSettings.get,updatewith If-Match), on-demand idempotent day generation (housekeepingDays.generate, 201 the first time and 200 again, each run giving the day its next version asETag;housekeepingDays.getanswers a generated day with its version, and 404housekeeping.day_not_foundfor a day never generated), tasks (housekeepingTasks.list,create,get,update,status,assign) andhousekeepingAssignees.list. The bulk changes of 1 to 100 tasks,housekeepingTasks.bulkStatus(POST …/housekeeping_task_status_changes) andbulkAssign(POST …/housekeeping_task_assignments), take each task’s version in the body instead of If-Match and answer a stale one with 412. Reads needinventory.read; generation, manual tasks and assignment need the newhousekeeping.assign, the settings the newhousekeeping.configure, and status changes the existinghousekeeping.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.removeandmaintenanceAssignees.list.maintenanceTickets.createtakes the new optional members, an optional clientid(a repeated report answers 200) and an optionalblockthat creates an out-of-order or out-of-service block with the ticket, so it can also answerticket.assignee_invalidand the block refusalsblock.invalid,block.overlap,block.assignedandblock.past; a report that creates a block also answersoverbooked_dates, the nights it oversells, asblocks.createdoes. A clientidthat another property’s task, ticket or photo uses is refused (housekeeping.invalidorticket.invalidwith fieldid,photo.invalid), writing nothing. The photos are part of the ticket: adding one (a body of up to 3 MiB;photo.too_large(413) orrequest.too_large) or removing one gives the ticket its next version, answered asETag, and removing one takes the ticket’s version in If-Match (ticket.version_mismatch, 412). A photo is answered as its bytes, withETag: "1": it never changes. - Breaking for maintenance-ticket clients (pre-1.0): the collection is renamed from
/properties/{property_id}/maintenance-ticketsto/properties/{property_id}/maintenance_ticketsfor every ticket operation, as ADR 0016’s snake_case paths require; the operation IDs are unchanged, so SDK callers only regenerate. The status isopen,in_progressordone, and is changed only throughPUT …/maintenance_tickets/{id}/status, where closing names its housekeepingfollow_up;maintenanceTickets.updateno longer takesstatus, and its members are all optional, at least one.ticket.invalidnow carriesfield. - New problem codes:
housekeeping.task_not_found(404),housekeeping.day_not_found(404),housekeeping.invalid(422, withfield),housekeeping.version_mismatch(412),housekeeping.invalid_state(409),housekeeping.assignee_invalid(422),ticket.assignee_invalid(422),photo.not_found(404),photo.invalid(422, withfield:base64for content that is not a whole image of its type,idfor an identifier in use),photo.dimensions_too_large(422, withfield,max_side4096 andmax_pixels16,000,000),photo.too_large(413, withmax_bytes) andphoto.limit_reached(409). The permission lists of roles, API keys and workspaces name the two new grants, and discovery declares thehousekeeping_tasksandmaintenance_photoscapabilities.
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: theorganizationTokenscheme (gc_org_…, the workspace chosen withGuestCraft-Workspace-Id) and theclientSessionscheme (gc_cst_…). GET/POST /organization_tokens,GET /organization_tokens/{id},POST /organization_tokens/{id}/revokeandPOST /organization_tokens/{id}/rotate(organizationTokens.*, browser-only, organization owners and administrators); problemsorganization_token.invalid(422,field),organization_token.not_found(404),organization_token.version_mismatch(412) andorganization_token.not_rotatable(409).GET/POST /client_sessions,GET /client_sessions/{id}andPOST /client_sessions/{id}/revoke(clientSessions.*) with the new permissionclient_sessions.manage; only an API key or an organization token mints; problemsclient_session.invalid(422,field),client_session.not_found(404) andclient_session.version_mismatch(412).- The API answers the CORS preflight of every
/v1operation (OPTIONSwithOriginandAccess-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,transitionordelete; the MCP server marks tools by it. - Workspace audit entries (
team.audit) gain the actor typesorganization_tokenandclient_session, withorganization_token_idandclient_session_id. The access log names the credential asapi_key_id,organization_token_idorclient_session_id.
API revision 6.3.0 completes Channex ARI publication and booking import (ADR 0034, ADR 0028, publication, booking import):
AriHealthgainsdrift(the latest nightly drift check, the drift repairs not yet published again and those rejected past the retry limit);AriSendgainslookupsandlooked_up_at, and itscodecan belookup_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, withchannex.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 fromsince, withnextfor the following read, each to be shown once by its eventid. Both, andchannex.bookingHistory.list, can answer 429auth.rate_limited.GET /properties/{property_id}/channex/booking_history(channex.bookingHistory.preview) andPOST /properties/{property_id}/channex/booking_history_imports(channex.bookingHistory.import, 201), browser-only withchannex.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 withchannex.view_health. Problemschannex.history_read_refused(409) andchannex.history_import_running(409);channex.mapping_inactiveis also answered by them.OtaBookinggainsreinstated_at: a channel can reinstate rooms it cancelled.WebhookEventTypegainschannex.alert_opened@1,channex.alert_resolved@1(objectchannex_alert) andreservation.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 withworkspaces.configureand 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.limitis 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 400cursor.invalid. Workspace Activity retains its documented temporary offset contract.- Workspace item reads return a strong numeric version
ETag("N"); replacement and archival require it inIf-Match. Problems aretenancy.invalid(422, field-linked name failures),tenancy.precondition_failed(412), andtenancy.integration_connected(409). Schema/path/query/precondition validation still answersrequest.invalid(400). Missing and foreign tenancy resources both return 403auth.forbidden. Restore leaves night audits paused and expires pending webhook deliveries older than 246,960 seconds; the delivery status enum includesexpired. - Browser
workspaces.listnow 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 withinventory.readand replace withhousekeeping.assignthe members who work on a day, from the business date to 30 days after it: up to 100 distinct members holdinghousekeeping.update, each withstarts_at(HH:MM),capacity_credits(1–999) and asection(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 withinventory.readand replace withhousekeeping.configureup 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 abasis(also itsETag).POST …/housekeeping_assignment_runs(housekeepingAssignmentRuns.create, transition) applies it with{date, basis}and answers 201 with the run and itsLocation; a different plan is 409housekeeping.plan_changed, a plan with no change 409housekeeping.plan_empty.POST …/housekeeping_assignment_runs/{run_id}/undo(undo, transition) restores each task’s previous assignee exactly, or answers 409housekeeping.undo_conflict.GET …/housekeeping_assignment_runs?date=(list) andGET …/housekeeping_assignment_runs/{run_id}(get) read runs withinventory.read; a run’sETagis"1", or"2"once undone. - Checklists:
GET/POST …/housekeeping_checklistsandGET/PUT/DELETE …/housekeeping_checklists/{checklist_id}(housekeepingChecklists.list,create,get,update,remove; writes needhousekeeping.configure, PUT and DELETE take If-Match) manage one template per task type and room type, with 1–50checkorinspectionitems. 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, andPUT …/housekeeping_tasks/{id}/checks/{key}(answer,housekeeping.update, the task’s If-Match) answers one item; the firstfailedof an inspection item requires afollow_uptask for the room, or a fault report that opens a maintenance ticket and also needsmaintenance.manage. HousekeepingTaskgainsassigned_automatically,pinned,unit_section,room_type_id, the frozenchecklistsummary (required_openamong it) and the originchecklist.housekeepingTasks.updateacceptspinned(withhousekeeping.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. Settingdone, single or bulk, on a task with required items unanswered is 409housekeeping.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) andhousekeeping.undo_conflict(409);housekeeping.invalidnames the new fieldsstaff,rules,name,items,resultandfollow_up, anddatein the path of a roster or a plan. - Events:
housekeeping_task.created@1,housekeeping_task.assigned@1andhousekeeping_task.status_changed@1(objecthousekeeping_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.
RatePlanInputandRatePlangainoccupancy(null, or{ base, max, curve }withcurve{ kind: "per_adult", extra_adult, fewer_adult }or{ kind: "explicit", adjustments }),age_bands([{ category, max_age, nightly_fee }],infantorchild, ages 0–17) andpackage_items([{ extra_id, quantity, basis }],per_night,per_person_nightorper_stay, at most 10), anddistribution({ scope: "direct" }, never published, or{ scope: "channels", channels }withbooking_com,expedia,agoda,airbnb,hostelworld; empty when not declared). OnratePlans.createthe four members are optional (no curve, bands or package; channels not declared). OnratePlans.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 explicitnullor[]clears it.sell_mode: per_personis now accepted and requiresoccupancy; derived plans follow their root.ratePlans.createandratePlans.updateanswerrate_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.updateanswersrate_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 answersrates.invalidonsell_mode.rate_plan.unsupported_sell_modeis no longer answered by any operation.RateDaygainsoccupancy_prices([{ occupancy, price }]) on per-person plans’ days ofrates.get.QuoteInputgainschild_ages;QuoteReasongainschild_ages_required,child_age_unpricedandpackage_exceeds_price. Quote nights and reservation room nights (withfolios.read) gainpackage: the services (extra_id,name,tax_category,quantity,amount, the soldbasisand thesourceof the night’s price,rate_planorchannel) the night’s price includes.RateQuotegainsfingerprint, and the room of avalidate_onlymove or resize that re-quotes it gainsquoted_fingerprint. A room input and a quoted move or resize acceptquoted_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, withquote.changed(412) even at the same nightly amounts. A room withprice_overriddenkeeps the price entered but takes the package of the offer quoted, so it follows the same rule with that offer’squoted_fingerprint.ReservationRoomInputandReservationRoomEditInputgainchild_ages;ReservationRoomgainschild_ages(empty when none were given). On a plan priced by occupancy, an edit of an expected stay that changes the occupants withoutrate_plan_idor a price answersquote.required(409) onreservationRooms.update.FolioMovement.sourcegainspackage: 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.FolioPostExtragainsbeyond_package; without itfolios.postExtraanswersfolio.included_in_package(409) for an extra a room’s package includes.OtaBookinggainspackage_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 reasonunknown_rate_planorpackage_not_allocated.AriSettings(channex.ari.health) gainsage_policy(the Channex hotel policy’sinfant_max_ageandchildren_max_age, null without a policy, absent until read) andplan_issues([{ plan_id, room_type_id, code, field, withheld }]): the mappings a plan issue withholds, and how (stop_sell,not_sent, orroom_type_closedfor 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 withpublication_closedfor a plan issue.channex.ari.mapanswersrate_plan.not_distributable(422) withplan_idandfieldfor a mapped plan’s setting that cannot be published exactly, its error pointing at the plan’s mapping. Async_failurealert gains the causeplans_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(strongETag/If-Matchon replace), the non-fiscal statementcounterparties.statementwithcounterparties.statementCsv, andcounterparties.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, receiptsreceivableReceipts.list|create|get|allocate|void,receivableEntries.release, and the agingreceivables.agingwithreceivables.agingCsv. Writes that move money take aposting_key; item and receipt reads answer anETagthat changes with their append-only entries. reservationFolios.linkCounterparty|unlinkCounterparty(PUT/DELETE …/folios/{folio_id}/counterparty).reservationFolios.closemoves a linked folio’s owed balance to its counterparty (receivables.transfer, when the counterparty allows direct billing, else 409counterparty.direct_billing_not_allowed), withaccept_credit_limitand the problemsreceivable.credit_limit(409, membersexposure,limit,amountonly for a member who reaches every property),counterparty.not_foundandcounterparty.archived;reservationFolios.reopenanswersreceivable.item_applied.FolioMovement.kindgainsreceivable_transferandreceivable_item_id;ReservationFoliogainscounterparty_id;CashShiftReportgainsreceipts.- Events
receivable.*@1(objectsreceivable_item,receivable_receipt) and permissionsreceivables.read,receivables.manage,receivables.write_offandreceivables.transfer;Counterparty.direct_billing_allowed. reports.cancellationswithreports.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.consolidatedwithreports.consolidatedCsv: the workspace’s properties together, per currency or converted toreporting_currencywith stored exchange rates (422exchange_rate.missing, memberscurrenciesandreporting_currency).reports.organizationConsolidatedwithreports.organizationConsolidatedCsv: the same across the organization’s active workspaces (sandbox only withinclude_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;createanddeleteneedexchange_rates.managein a browser (problemsexchange_rate.duplicate,exchange_rate.not_found,exchange_rate.version_mismatch).
6.10.0
Section titled “6.10.0”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-protectionjurisdictionand a period in days (1–3650, or null for the default) forguest_profile,stay_personal_data,booking_revision_archive,audit_pseudonymization,exportsandmaintenance_photos(photos of maintenance tickets;starts_atclosure, the ticket’s closing). The answer adds the defaults with their source (workspace,packorgeneric) 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. StrongETag/If-Match; a stale version answers 412privacy.version_mismatch.GET/POST /guests/{id}/legal_holdsandPOST /guests/{id}/legal_holds/{hold_id}/release(guestLegalHolds.list,.create,.release;guests.legal_hold, and for the list alsoguests.eraseorguests.export) place and release a hold with a reason. One hold is active at a time (409guest.legal_hold_active); releasing another answers 404guest.legal_hold_not_found.POST /guests/{id}/erase(guestErasure.create,guests.erase) takes{"confirm": true}and the guest’sIf-Match, anonymizes the guest and its merged identities, and answers the completed request,guest_version(also the newETag),retainedandupstreamcodes. A stay that has not ended or an active legal hold answers 409guest.erasure_blockedwithreason(active_staywithreservation_ids, orlegal_holdwithhold_id). The request’scountsincludequarantined_revisions, the stays’ quarantined Channex revisions the erasure resolved; their receipts become stale with reasonpersonal_data_erased.POST /guests/{id}/data_exports(guestDataExports.create,guests.export) answers 202 with the queued request and itsLocation.GET /guest_data_requests(cursor pages, optionalguest_id) andGET /guest_data_requests/{id}(guests.eraseorguests.export) read the requests; unknown ones answer 404privacy.request_not_found. A request carries aversionthat every state change raises; the single read answers it as a strongETag. A queued export whose job never completes fails after 24 hours withfailurenot_prepared.POST /guest_data_requests/{id}/download(guestDataRequests.download,guests.export) answers a ready export’sapplication/ziparchive once, as an attachment withCache-Control: no-store; afterwards, or while it is not ready, expired, erased or failed, it answers 409privacy.export_unavailablewith thatreason.Guestgainserased_at(null while the profile holds personal data), and the permission catalog gainsguests.erase,guests.exportandguests.legal_hold.messages.eraseis no longer an API key or organization token permission: it leaves theApiKeyPermissionandOrganizationTokenPermissionenums, 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.
6.11.0
Section titled “6.11.0”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 opaquekeyset_cursorparameter and the answer membersnext_keyset_cursor(null on the last page) andhas_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 400cursor.invalid.- Deprecated: the numeric
cursoroffset ofGET /auditand the numericnext_cursorit answers keep their 6.6.0 meaning (the entries to skip, up to 100,000;next_cursoris given when no keyset cursor is sent, null otherwise and on the last page). Usekeyset_cursorinstead. A request with bothcursorandkeyset_cursoranswers 400request.invalidat/keyset_cursor. GET /audit/export_csvandGET /audit/export_json(team.auditExportCsv,team.auditExportJson,audit.view, browser session only) andGET /organizations/{organization_id}/audit/export_csvandexport_json(organizations.auditExportCsv,organizations.auditExportJson, organization owners and administrators) download the entries that match the list’s filters.fromanduntilare required and at most 366 days apart (400request.invalidat/fromor/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’sdatawith its quotes doubled for CSV, and 512 bytes for its other fields), answer the new 422activity.export_too_largewithmax_entriesandmax_bytesmembers, never a truncated file. The file is streamed. CSV columns areoccurred_at, id, action, actor_type, actor, api_key_id, organization_token_id, client_session_id, subject, data(RFC 4180, formula-guarded text,dataas compact JSON); JSON is{ "data": [...] }with the list’s entry shape. Both carryContent-Disposition: attachment; filename="activity-<from date>-<until date>.<csv|json>"andCache-Control: no-store. Each export recordsaudit.exportedwith its format, count and filters, in the transaction that read it.
Account security (ADR 0053, guide):
accountSecurity.getPolicyandaccountSecurity.updatePolicy(GET/PUT /organizations/{organization_id}/security_policy, strongIf-Matchon 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 listspasskeys_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 403PASSKEY_ORIGIN_UNAVAILABLEon/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.issuePasswordResetandaccountSecurity.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 answerspassword_reset.invalid; 10 attempts per address a minute.- Every operation of a browser session can answer 403
auth.step_up_required(membersrequirement,organization_id) in an organization whose policy the session does not meet; every operation of a device token can answer 401auth.reauthentication_required. TheProblemschema 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(withRetry-After),password_reset.invalid. - SDK:
GuestCraftError.retryAftercarries the seconds ofRetry-After; theaccountSecurityfacade.
6.12.0
Section titled “6.12.0”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
oauthApplicationbearer scheme (gc_oat_…) on every operation that listsapiKey, except minting client sessions.GET /authorized_applications(authorizedApplications.list),GET /authorized_applications/{id}(authorizedApplications.get) andPOST /authorized_applications/{id}/revoke(authorizedApplications.revoke, If-Match), browser-only, with the new permissionoauth_applications.manage; problemsoauth_application.not_found(404) andoauth_application.version_mismatch(412), andoauth_application.invalid(422,field) for a refused consent. Audit entries gainactor_typeoauth_applicationand the memberoauth_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/v1and…/api/mcp(RFC 9728). See OAuth applications. - Organization permissions.
OrganizationToken.organization_permissions, the optional input member of the same name (default[]),OrganizationTokenPage.grantable_organization_permissionsand theOrganizationPermissioncomponent (workspaces.create,organization_audit.view).workspaces.createandorganizations.auditalso accept theorganizationTokenscheme, and answer it429 auth.rate_limitedwhen its budget is spent; a browser session calls them as before. - Client session scopes.
clientSessions.createtakes an optionalscope(ClientSessionScope:properties,reservation,guestorproperty_public) besideproperty_ids, which stays required as in 6.6.0. Withoutscope,property_idsis 1 to 100 properties, as before; with it,property_idsmust be the properties the scope names (the same list forproperties,[property_id]forreservationandproperty_public,[]forguest), and any other list answersclient_session.invalidwith fieldproperty_ids. Sessions answer theirscope;property_idskeeps 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 403auth.forbiddenon 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, ordeprecatedwithsupported_untilandsuccessor). Webhook endpoints gainevent_versions, an additive map of event family to the version delivered, which their creation and edits also accept;event_typeskeeps 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 theWebhookEventVersionscomponent;webhook_endpoint.invalidgains the reasonslater_version(onevent_types, a later version named instead of the family) andnot_subscribed,unknown_versionanddeprecated_version(onevent_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 gaindeprecated_event_typesandbackoff_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 answerinsufficient_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_idafterclient_session_id, and the JSON export the member, as in the list.
6.13.0
Section titled “6.13.0”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) andPOST /payment_connection/verify(paymentConnection.verify), browser session only, withpayments.configureand unrestricted property access. TheETagis the connection’s version,"0"before the first connection; the writes take it inIf-Match(PUTaccepts"0").PUTtakes a restricted secret key, the webhook signing secret (both write-only, never returned) andlink_lifetime_days. Every answer carrieswebhook_url, the workspace’s address to register with the provider, also before connecting.DELETEanswers the disconnected connection (200).- A connected
PaymentConnectioncarriespending(PaymentPendingWork): the openlinks, thepaymentsand holds and therefundsthe provider has not settled, the signeddeliveriesreceived and not processed yet, and thefailed_deliveriesthat failed every processing attempt.DELETEanswerspayments.connection_in_useand a key of another accountpayments.account_changedwhile 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-Matchwith the connection’s version: offers the failed deliveries to processing again with fresh attempts and answers the connection (200,ETag). Problemspayments.not_connected(409),payments.version_mismatch(412) andauth.forbidden.GET /properties/{property_id}/reservations/{id}/payments(reservationPayments.get,folios.read): links, provider payments with refunds, saved cards,off_platformand what each purpose may still collect.POST /properties/{property_id}/reservations/{id}/payment_requests(paymentRequests.create,payments.collect, 201 withLocationandETag),GET /properties/{property_id}/payment_requests/{id}(paymentRequests.get,folios.read,ETag) andPOST …/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 withLocationandETag). Itsposting_keymakes a resend answer the same charge; the same key for another charge answers 409payments.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 timesquantity, and captures exactly its amount, at most what the hold still holds) andPOST …/provider_payments/{id}/release(providerPayments.release,payments.collect,If-Match, 202).POST /properties/{property_id}/provider_payments/{id}/refunds(providerRefunds.create,payments.refund, 202,Locationthe provider payment), with aposting_keylike 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_paymentforkind: payment,folios.refundforkind: 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 paymenttype, the booking’s card as safe facts (OtaBookingCard:brand,last4,exp_month,exp_year,virtualand, for an OTA’s virtual card,virtual_cardwithcurrency, its initialbalance,effective_onandexpires_on; unknown values are null), the cardmovedto 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: areasonofpayments.ota_card_unavailable,tokenization_inactive,tokenization_missingoroff_platform). POST /properties/{property_id}/reservations/{id}/ota_card/move(otaCards.move,payments.collect, 200OtaCard) 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,reasonunverifiable); Channex’s 403 (the app is not installed on the property) answerspayments.tokenization_missingand is recorded as the tokenization’smissing_at.POST /properties/{property_id}/reservations/{id}/ota_card/charges(otaCardCharges.create,payments.collect, 202 withLocationandETag) takesposting_keyandamountlikecardCharges.createand charges the moved card toward the folio’s balance.ProviderPayment:origincan also beota_card; the newmethod(card_onlineorota_virtual_card) is how the folio names the money, andota_card_idnames the moved card a charge used (null otherwise).GET /properties/{property_id}/card_tokenization(cardTokenization.get) andPOST …/card_tokenization/acknowledge({ "installed_and_cost_accepted": true }) and…/stop(cardTokenization.acknowledge,.stop): browser session only andpayments.configure, likepaymentConnection. The operator installs the Stripe Tokenization App in Channex’s interface; GuestCraft never installs or removes it. They answerCardTokenization(stateinactiveoractive,linked,acknowledged_at,missing_at,version), with the version asETag,"0"before the first acknowledgement; the writes take it inIf-Match.- New problems:
payments.cost_not_acknowledged(422),payments.channex_not_linked,payments.tokenization_inactiveandpayments.tokenization_missing(409), andpayments.ota_card_unavailable(409,reasonno_card,unverifiable,not_moved,foreign,mismatch,not_effective,expired,balance,currencyorunknown). - Problems:
payments.invalid(422, field-linked, withfieldandreason),payments.not_connected(409),payments.connection_in_use(409),payments.provider_unavailable(503,Retry-After: 5),payments.request_not_found,payments.payment_not_foundandpayments.card_not_found(404),payments.off_platform(409,reasonairbnb,ota_collectsorcollector_unknown),payments.open_request_exists,payments.in_progress,payments.invalid_state,payments.nothing_dueandpayments.account_changed(409),payments.amount_exceeded(422) andpayments.version_mismatch(412, withcurrent_versionand the currentETag). folios.refundanswersfolio.provider_payment(409) for a payment the provider collected: it is refunded throughproviderRefunds.create.FolioMovement:methodcan also readcard_online,ota_virtual_cardandcash_rounding(staff still record only the four manual methods),sourcecan beproviderorrounding, and a provider movement carriesprovider,provider_referenceand, when it came from a link,payment_request_id.PrepaymentStatusgainsguarantee_source(provideroroperator); a reservation room’sguarantee_trackingis nowRoomGuaranteeTracking, withsource. Both are optional, because servers before this revision omit them; a client reads a missing one asoperator.Propertygainscountry(ISO 3166-1 alpha-2 or null), whichPropertySettingstakes optionally: omitted keeps it, null clears it, an unknown code answersproperty.invalid.- The new grants
payments.configure(browser only, never held by API keys or organization tokens),payments.collectandpayments.refundjoin the permission catalog.
6.14.0
Section titled “6.14.0”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 withLocationandETag): 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(withfailing_sinceandlapses_at) for 7 days, thenlapsed.
- Single sign-on connections (
/organizations/{organization_id}/sso_connections), bodySsoConnectionSettings:name,enabled,protocol(oidcwithoidc { issuer, client_id, client_secret? }, orsamlwithsaml { 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’srol_identifier, at every property of the workspace whenproperty_idsis absent or null, or at 1–100 of itsprop_properties, as a member’s access; answers always carryproperty_ids, null for every property). The client secret is write-only; answers sayclient_secret_set. Answers (SsoConnection) also carryservice_provider(OIDC redirect URI; SAML entity ID, ACS and metadata URLs) andactive.enterpriseIdentity.listConnections(GET, read).enterpriseIdentity.createConnection(POST, update: it assigns the listed domains, whose ETags change; 201 withLocationandETag):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 andbase_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.scopesis required: one or more ofscim.users.read,scim.users.write,scim.groups.read,scim.groups.write, none twice; every token (ScimToken) answers itsscopes.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 withnext_cursor.enterprise_identity.invalidat/cursor.enterpriseIdentity.updateScimGroupAccess(PUT /scim_groups/{group_id}/access{ grants }ofWorkspaceGrant, If-Match with the group’setag, 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 withnext_cursor.enterprise_identity.invalidat/cursor.
- New problems:
enterprise_identity.invalid(422; the field is thepointerof its oneerrorsentry),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_flowcookie (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_INVALIDonce 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; otherwise400 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), andGET /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 ofSSO_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) orSSO_RESPONSE_INVALID(a SAML response without this ACS as itsDestination, or outside its validity window). - SCIM 2.0 (RFC 7643, RFC 7644) at
/api/auth/scim/v2, with a SCIM token as bearer:UsersandGroups(GET,POST;GET,PUT,PATCH,DELETEon an item), each needing the matching scope of the token (403 otherwise), andServiceProviderConfig,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 withRetry-After).
GET /api/internal/domains/check, with the scheduler’sCRON_SECRETbearer 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
enterpriseIdentityfacade.
6.15.0
Section titled “6.15.0”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 inRateDay(calendar reads, the Undo payload), in the bulkrestrictionsofPUT /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.saleslists 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_sellfor a plan that sold its cap on a night. Saving a reservation change that sells a plan beyond its cap answersrate_plan.max_sell_reached(409) withmax_sell_nights(plan_id,date,max_sell,sold), on every operation that saves a reservation.ReservationRoomInput.max_sell_override_reasonsells beyond the cap with the new permissionreservations.override_sales_cap(built-in: administrators), audited asreservation.max_sell_overridden. -
Booking reports have the
reservationstag 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_cardandno_show:available, the problem code a request would answer now (refusal), the channel’s window (opens_at,closes_atexcluded) in the property’s time zone, the local no-show’s fee state (no_show) and the storedreport(statesending,sent,failedorunknown,failure,attempts). Penalty amounts needfolios.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 optionalwaived_feesand, 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 409ota_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) andota_report.not_unknown. -
Event:
reservation.ota_reported@1(objectreservation) 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 withchannex.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, withreplacement_rate_plan_id, orreplacement_room_type_idandreplacement_plans).validate_only: trueanswers 200 with anAriRetirementPreview(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 (412channex.mapping_version_mismatchwith the currentETag) and the answer is 201 with anAriRetirementand itsLocation. -
GET /properties/{property_id}/channex/ari/retirements/{retirement_id}(channex.ari.retirements.get,channex.view_health, with a weakETag) reads one;POST …/retirements/{retirement_id}/canceland…/resume(channex.ari.retirements.cancel,channex.ari.retirements.resume, browser-only withchannex.configure) cancel it before its mapping switched, or resume a failed one. -
AriHealth(channex.ari.health) gainsretirement: the property’s active retirement, or else its latest, or null. Itsstateisstopping,draining,verifying,switching(a replacement waiting for its first verified values),completed,failed(withcodereadback_mismatch,upstream_rejectedortarget_missing; the target stays closed) orcancelled. -
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, withreasonnot_found,mappedorincompatible, andplan_id),channex.ari_retirement_not_found(404) andchannex.ari_retirement_state(409, withstate). A replacement’schannex.ari_replacement_invalidandrate_plan.not_distributablepoint at the member naming it. -
channex.ari.mapalso answerschannex.ari_retirement_active(409) while a retirement is active;channex.ari_mapping_livenow directs to the retirement. -
A
sync_failurealert gains the causeretirement_failed. -
channex.revision.retryacceptsapply_to_former_room_type(default false): it applies a revision quarantined with the reasonretired_target(a Channex room type a retirement unmapped) to the local room type that room type was mapped to; for any other receipt it answersrequest.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) andcursor, answeringThreadMessagesPage({ data, next_cursor, has_more }) with each page oldest first. Problems:messages.thread_not_found(404) andcursor.invalid(400) for a cursor of another thread or call. -
MessageThreadDetailgainsmessages_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.