Appearance
ACP checkout
Stayblox storefronts can also speak ACP (the Agentic Commerce Protocol behind OpenAI's Instant Checkout and the wider ChatGPT commerce ecosystem). If your platform is ACP-native rather than UCP-native, use this REST surface instead of the MCP checkout tools. Both sit on the same booking engine, so pricing, availability, and the held-booking behavior are identical either way; only the wire format differs.
Discovery and catalog search still happen over UCP/MCP; ACP only replaces the checkout half. Get your stay item ids from search_catalog on the storefront's MCP endpoint, then check out against the ACP endpoints below.
Unlike UCP, ACP is opt-in per storefront: a host has to switch it on and issue you an API key before you can check out against their site.
Discovery
Storefronts that enable ACP add an acp object to their UCP profile:
GET https://{storefront-domain}/.well-known/ucpjson
{
"ucp": { "...": "..." },
"acp": {
"version": "2026-04-17",
"checkout_url": "https://stay.example.com/api/acp/checkout_sessions"
}
}The acp key only appears when the host has ACP enabled and takes instant bookings. No acp key means this storefront doesn't support ACP checkout right now.
Base URL and versioning
All five endpoints hang off the same host as checkout_url:
https://{storefront-domain}/api/acpSend API-Version: 2026-04-17 (current) or 2026-01-30 on every request. Omit the header and Stayblox assumes the current version; send anything else and you get a 400 unsupported_api_version. Every response echoes the resolved version back in the API-Version header.
Auth
Access is a manual, per-store step, not a self-serve signup: the host enables ACP for their storefront and generates an API key from their dashboard settings, then hands it to you (typically during your platform's merchant onboarding). Send it as a bearer token:
Authorization: Bearer <key>A missing or wrong key returns 401 unauthorized.
Optional request signing
Hosts can additionally configure an inbound signing secret. When they have, every request must also carry:
Signature: sha256=<hex-encoded HMAC-SHA256>
Timestamp: <unix seconds>The signature covers "{timestamp}.{raw request body}", hashed with the shared secret. Stayblox rejects timestamps more than 5 minutes off and any signature that doesn't verify, both as 401 invalid_signature. If the host hasn't configured a secret, these headers are ignored and the bearer key alone gates access.
Rate limit
60 requests/min per IP and storefront domain, the same budget as the MCP surface.
Item ids
Line items reference the same stay item ids the storefront's MCP catalog returns from search_catalog (see stay item ids):
stay:{unit_type_id}:{check_in}:{check_out}:{adults}:{children}Treat them as opaque: take ids from search_catalog / lookup_catalog on /api/mcp and pass them to ACP unchanged.
The five endpoints
| Method & path | Does |
|---|---|
POST /checkout_sessions | Create a session: price line_items (+ optional buyer). |
GET /checkout_sessions/{id} | Fetch current session state. |
POST /checkout_sessions/{id} | Update line_items and/or buyer (per-key semantics); reprices. |
POST /checkout_sessions/{id}/complete | Place the held booking. |
POST /checkout_sessions/{id}/cancel | Cancel an open session. |
Session ids are opaque UUIDs.
The session object
| Field | Notes |
|---|---|
id | Pass it to the other four endpoints. |
status | See below. |
currency | Lowercase ISO code, e.g. "eur". |
line_items | id, item ({id, quantity}), name, and base_amount / discount / subtotal / tax / total in minor units. |
buyer | Present once you've set one; absent before that. |
totals | Session-level, itemized (see below). |
fulfillment_options | Always []. Lodging has nothing to ship; fulfillment_address and fulfillment_option_id on requests are accepted and ignored. |
messages | Business-level problems (see Errors). |
links | The storefront's policy pages (terms, privacy, etc.) when the host has them configured. |
capabilities | Constant in v1: payment.handlers is always [], interventions.supported is [], extensions is []. |
order | Only once status is completed (see payment posture). |
Statuses:
| Status | Means |
|---|---|
not_ready_for_payment | Missing or invalid buyer info, or a line item problem. |
ready_for_payment | Priced and ready to complete. |
complete_in_progress | Completion is being processed (transient). |
completed | Booking placed. |
canceled | Canceled explicitly, or after 24 h of inactivity. |
requires_escalation | Reserved, unused in v1. |
Totals
Every amount is an integer in the currency's minor unit (30000 = €300.00). Session totals[] are typed entries: items_base_amount, an optional discount, subtotal, one fee entry per mandatory fee, one tax entry per tax added on top of the rate, and total. They sum exactly; render them as given, don't recompute.
Walkthrough
Create a session with a stay item, no buyer yet:
POST /api/acp/checkout_sessions
Authorization: Bearer sk_live_...
API-Version: 2026-04-17json
{
"line_items": [
{ "item": { "id": "stay:42:2026-08-10:2026-08-13:2:0" }, "quantity": 1 }
]
}json
{
"id": "0b7e9a2c-4f3d-4e8a-9c1b-7a2d6e5f0b91",
"status": "not_ready_for_payment",
"currency": "eur",
"line_items": [
{
"id": "li_0",
"item": { "id": "stay:42:2026-08-10:2026-08-13:2:0", "quantity": 1 },
"name": "Seaside Resort — Deluxe Suite",
"base_amount": 30000,
"discount": 0,
"subtotal": 30000,
"tax": 0,
"total": 30000
}
],
"totals": [
{ "type": "items_base_amount", "display_text": "Items", "amount": 30000 },
{ "type": "subtotal", "display_text": "Subtotal", "amount": 30000 },
{ "type": "total", "display_text": "Total", "amount": 30000 }
],
"fulfillment_options": [],
"messages": [
{
"type": "error",
"code": "missing",
"content_type": "plain",
"content": "Buyer first_name, last_name and email are required.",
"param": "$.buyer"
}
],
"links": [],
"capabilities": {
"payment": { "handlers": [] },
"interventions": { "supported": [] },
"extensions": []
}
}Add the buyer with an update call, same session id:
POST /api/acp/checkout_sessions/0b7e9a2c-4f3d-4e8a-9c1b-7a2d6e5f0b91json
{
"buyer": { "first_name": "Ada", "last_name": "Lovelace", "email": "[email protected]" }
}The response repeats the session with status: "ready_for_payment", a buyer object, and no more missing message. Complete it:
POST /api/acp/checkout_sessions/0b7e9a2c-4f3d-4e8a-9c1b-7a2d6e5f0b91/complete
Idempotency-Key: 6d5a9c1e-8b2f-4a3d-9e7c-1f0b6a5d4c3bjson
{
"payment_data": { "provider": "stripe", "token": "vt_test_ignored" }
}json
{
"id": "0b7e9a2c-4f3d-4e8a-9c1b-7a2d6e5f0b91",
"status": "completed",
"order": {
"id": "9G4T-X2LK",
"checkout_session_id": "0b7e9a2c-4f3d-4e8a-9c1b-7a2d6e5f0b91",
"permalink_url": "https://stay.example.com/pay/status/eyJpdiI6…"
},
"messages": [
{
"type": "info",
"code": "payment_required",
"content_type": "plain",
"content": "Booking is held. Payment must be completed at the permalink to confirm the stay."
}
]
}(Trimmed: the real response still carries line_items, totals, links, and capabilities.)
Payment: pay by link, not delegated checkout
capabilities.payment.handlers is always empty, so Stayblox never processes payment inside an ACP checkout. If a payment_data token still arrives on complete, Stayblox accepts the request and ignores the token: no card is charged.
What complete actually does is place a held, unpaid booking and return order.permalink_url. Hand that link to the guest; they pay through the host's own payment page to confirm the stay. If nobody pays, the hold releases automatically after a couple of hours, the same as an abandoned checkout on the storefront itself.
Send an Idempotency-Key header on complete. Retrying complete on a session that's already completed returns the same order again, never a duplicate booking.
Delegated, in-chat payment is on the roadmap, not part of v1.
Errors
Two layers. Don't confuse them.
Protocol errors are HTTP 4xx with a {type, code, message, param} body: the request itself was malformed.
| Status | code | Cause |
|---|---|---|
| 401 | unauthorized | Missing/wrong bearer key, or the store hasn't generated one. |
| 401 | invalid_signature | Inbound signing is configured and the Signature/Timestamp check failed. |
| 400 | unsupported_api_version | API-Version isn't one Stayblox accepts. |
| 400 | missing | No line_items (or items) array on create. |
| 404 | not_found | Unknown checkout session id. |
| 405 | not_cancelable | Cancel called on a session that's already completed or completing. |
Business errors ride inside a normal 2xx session as messages[]: the session itself is fine, something about its contents isn't.
code | Typical cause |
|---|---|
missing | Buyer fields absent or invalid (param points at $.buyer or a specific field). |
invalid | Mixed dates across line items, or mutating a session that's already finished. |
out_of_stock | The requested dates are no longer available, including a capacity race caught at complete. |
Business errors are recoverable: fix the input (new dates, buyer info) and call create or update again.
Limits and lifecycle
| What | Value |
|---|---|
| Rate limit | 60 requests/min per IP + storefront domain |
| Checkout session lifetime | 24 h from last mutation, then canceled |
| Unpaid booking hold | Released by the storefront's abandoned-booking sweep (~2 h) |
Not in v1
Delegated/in-chat payment, a lodging product feed and listing on ChatGPT itself, and order-update webhooks back to your platform.