Skip to content

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/ucp
json
{
  "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/acp

Send 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 & pathDoes
POST /checkout_sessionsCreate 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}/completePlace the held booking.
POST /checkout_sessions/{id}/cancelCancel an open session.

Session ids are opaque UUIDs.

The session object

FieldNotes
idPass it to the other four endpoints.
statusSee below.
currencyLowercase ISO code, e.g. "eur".
line_itemsid, item ({id, quantity}), name, and base_amount / discount / subtotal / tax / total in minor units.
buyerPresent once you've set one; absent before that.
totalsSession-level, itemized (see below).
fulfillment_optionsAlways []. Lodging has nothing to ship; fulfillment_address and fulfillment_option_id on requests are accepted and ignored.
messagesBusiness-level problems (see Errors).
linksThe storefront's policy pages (terms, privacy, etc.) when the host has them configured.
capabilitiesConstant in v1: payment.handlers is always [], interventions.supported is [], extensions is [].
orderOnly once status is completed (see payment posture).

Statuses:

StatusMeans
not_ready_for_paymentMissing or invalid buyer info, or a line item problem.
ready_for_paymentPriced and ready to complete.
complete_in_progressCompletion is being processed (transient).
completedBooking placed.
canceledCanceled explicitly, or after 24 h of inactivity.
requires_escalationReserved, 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-17
json
{
  "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-7a2d6e5f0b91
json
{
  "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-1f0b6a5d4c3b
json
{
  "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.)

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.

StatuscodeCause
401unauthorizedMissing/wrong bearer key, or the store hasn't generated one.
401invalid_signatureInbound signing is configured and the Signature/Timestamp check failed.
400unsupported_api_versionAPI-Version isn't one Stayblox accepts.
400missingNo line_items (or items) array on create.
404not_foundUnknown checkout session id.
405not_cancelableCancel 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.

codeTypical cause
missingBuyer fields absent or invalid (param points at $.buyer or a specific field).
invalidMixed dates across line items, or mutating a session that's already finished.
out_of_stockThe 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

WhatValue
Rate limit60 requests/min per IP + storefront domain
Checkout session lifetime24 h from last mutation, then canceled
Unpaid booking holdReleased 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.

© Stayblox — Developer Platform