Skip to content
العربية All documentation Sign in

The channel connectivity API

This is for booking platforms. If you run a marketplace, a regional OTA or a corporate travel tool, and hosts who manage their units in Suitiee want to sell through you, this is how the two systems talk: you read their listings and calendars, you build the listing on your own site, and you tell us when a guest books.

If you are a host wanting to connect a channel, this is not your page — see Choosing a path.

Suitiee carries no channel-specific code. Everything on this page is the same for every platform, which is what lets you integrate without waiting for us to build anything for you.

What you need

Ask us for an application. We create it and send you four things: a client id, a client secret (once — it is stored as a hash and cannot be shown again), the redirect URI we will accept, and a consent link for a sandbox host whose account already has sample units, a priced calendar and stays on it.

The sandbox host matters more than it sounds: every authenticated call in this API needs a host's consent, so without one you would be waiting for a real customer to volunteer before you could make your first request. Connect the sandbox exactly as you will connect a real host — same screen, same tokens, same endpoints — and everything you create in it is marked as test data that can never reach an operator, a cleaner or a real calendar.

Your application starts in test mode. Everything behaves exactly as it does live — consent, tokens, calendars, bookings — but every row it creates is marked as test data, nothing is billed to a host, and no cleaner is dispatched. Going live keeps the same client id, the same secret and the same URLs, so your staging configuration is your production configuration.

The shape of an integration

  1. A host on your platform presses Connect Suitiee. You send them to /channel/authorize.
  2. They sign in, tick which of their units you may see, and approve. Their browser comes back to you with a code.
  3. You exchange the code for an access token and a refresh token.
  4. GET /units → for each unit GET /units/{unit}/listing → build your listing → tell us its id with PUT /units/{unit}/listing-link.
  5. GET /units/{unit}/calendar?from=&to= → seed your availability, prices and minimum stays.
  6. From then on we POST you a signed event whenever something changes, and you re-read the endpoint the event points at.
  7. A guest books on your platform → POST /bookings. The host sees the stay on their Suitiee calendar within the request, and the night closes on every other channel they sell through.

Everything this API can do

The whole surface, in one table, so you can see what you are integrating against before you start. Every row is scoped: a host grants each permission separately and can refuse any of them, so build for the case where you hold some and not others.

What you want to do How Scope
Listings
See the units a host granted you GET /units units:read
Read a unit's full listing document — names, description, capacity, beds, amenities, address, photos, licence, headline price GET /units/{unit}/listing units:read
Record that you have listed a unit, and under which id PUT /units/{unit}/listing-link listings:write
Propose unit details for the host to review POST /units/import units:write
Prices and availability
Read per-night availability, price and stay rules GET /units/{unit}/calendar calendar:read
Set prices, minimum stays, arrival/departure rules and blocks PUT /units/{unit}/calendar calendar:write
Bookings
Record a stay a guest booked with you POST /bookings bookings:write
Move, cancel or check out one of your stays PATCH/cancel/checkout on /bookings/{id} bookings:write
Reconcile your stays against ours GET /bookings?updated_since= bookings:read
Guest conversation
Put your guests' messages in front of the host POST /messages messages:write
Receive the host's replies message.sent event messages:read
Catch up after your receiver was down GET /messages?since= messages:read
Operations
Ask whether the flat is ready for your guest GET /bookings/{id}/turnover operations:read
See the cleaning and maintenance booked on a unit GET /units/{unit}/cleanings, /maintenance operations:read
See what can be ordered and what it costs this host GET /services?unit_id= operations:read
Report a problem your guest found POST /units/{unit}/maintenance maintenance:write
Order a cleaning visit POST /units/{unit}/cleanings cleaning:write
Call off a clean you ordered POST /cleanings/{id}/cancel cleaning:write
Housekeeping
See who is connected to you, and disconnect GET/DELETE /connection, GET /connections units:read / none
Find out what became of a write you sent GET /events/{id} bookings:read
Get a real body for every event we send GET /webhooks/samples, POST /webhooks/test none

What is deliberately not exposed, and why

A complete list, so you do not go looking. Each of these is something this API can see and will not hand over.

Not available Why
Other channels' bookings on the same unit They appear as closed nights and nothing more. A stay the host took on Airbnb is Airbnb's guest, not yours — you need the night to be unsellable, which it is.
Other channels' prices Same reason, and it is commercially somebody else's.
The guest's email or phone on a booking you did not make You get back exactly the contact details you supplied.
The cleaner: name, phone, photo, company You have no relationship with the person who cleans the flat, and their identity is not ours to put on a guest-facing screen.
The host's billing with Suitiee — wallet, packages, invoices, statements, commissions Between them and us. You see the price of a visit you are about to order and one you did, and nothing else.
Proof-of-clean photographs and operators' internal notes Photographs of the inside of somebody's flat, and private notes about it.
Applying unit content directly POST /units/import creates a draft the host accepts or discards. A marketplace silently rewriting somebody's address or photos is a change nobody agreed to.
Access codes and door locks Not built yet — not withheld. When it exists it will be here.
The host's other units You see the ones they ticked. Re-consent is how that changes.

Three things that will otherwise cost you a day

Each of these is a decision a reasonable engineer would guess the other way.

An unpriced night is published closed

rate: null always comes with available: false and reason: "unpriced". Nobody has said what that night costs, so there is no honest price to sell it at. Do not fall back to a default of your own — the host finds out on a payout, and it is not recoverable.

Our events are pointers, not values

availability.changed says which nights of which unit moved. It never says what they moved to. Re-read the calendar for the ranges named.

This is deliberate. Delivery is retried for about a day and a half and is not ordered, so an event carrying a value could arrive after a newer one and write an old price over a new one on your live listing — silently. A pointer cannot: applying it twice, or late, or out of order, ends in the same place.

Every calendar and listing event carries sync_seq, a number that only goes up per connection. If you batch, ignore any event whose sync_seq is below one you have already acted on.

An overlapping booking is accepted, not refused

By default, if you tell us about a stay on nights that are already taken, we record it and answer 201 with meta.warnings naming the nights. The host is shown an oversell to resolve.

That is because you have usually already sold the stay and taken the guest's money. Refusing it would not un-sell it — it would only mean the host never finds out until two parties arrive at one door.

If your platform calls us before confirming to the guest, tell us and we will switch your application to strict mode. You then get 409 NIGHTS_UNAVAILABLE with the offending nights, which is an answer you can act on.

Authentication

Two credentials, used for different things.

Your application's client id and secret, over HTTP Basic, authenticate the token endpoint, the revocation endpoint, GET /connections and POST /webhooks/test. These are questions about your application rather than about one host.

A connection's access token, as Authorization: Bearer …, authenticates everything else. It resolves to exactly one host and exactly the units they ticked.

There is no account-wide key, by design. A host's consent is the only way to reach a host's data, and they can end it in one click from their own panel.

The consent link

https://suitiee.com/channel/authorize
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=YOUR_REGISTERED_URI      ← matched exactly, never by prefix
  &scope=units:read calendar:read bookings:read bookings:write listings:write
  &state=YOUR_CSRF_VALUE                 ← echoed back to you
  &code_challenge=BASE64URL(SHA256(verifier))
  &code_challenge_method=S256            ← required; `plain` is refused

The host signs into their own Suitiee account (or their operator does, for units they manage), sees what you are asking for in plain language, ticks their units, and approves.

Exchanging the code

POST /api/v1/channel/oauth/token
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=…&redirect_uri=…&code_verifier=…

You get an access token (30 days), a refresh token, and a connection_id. Store the connection_id against the host account on your side: it is stable across re-consent and it is what every event names.

Refreshing

POST /api/v1/channel/oauth/token
Authorization: Basic base64(client_id:client_secret)

grant_type=refresh_token&refresh_token=…

Refresh tokens are single use. Each exchange returns a new one; store it and discard the old. Presenting a spent refresh token revokes the whole connection and requires the host to consent again — because from our side that is what a stolen token looks like, and we cannot tell which of the two holders is you.

Scopes

Shown to the host in their own language and enforced on every request against both the token and the host's current grant — the narrower wins. A host who re-consents with fewer permissions is obeyed on your next call, not in thirty days.

Scope What it lets you do
units:read The connection, the granted units, and each unit's listing document
calendar:read Per-night availability, price and stay restrictions
bookings:read The bookings you made. Never another channel's
bookings:write Create, modify, cancel and check out your own bookings
listings:write Record and remove the "this unit is listed with us" mapping
units:write Propose unit details for the host to review. Never applies them
messages:read Receive the host's replies to your guests as message.sent events
messages:write Post your guests' messages into the host's Suitiee inbox
calendar:write Set prices, minimum stays and closed nights
operations:read See turnovers, cleaning and maintenance on the granted units, and the service catalogue
maintenance:write Report a problem with a unit. Costs nothing
cleaning:write Order a cleaning visit. This spends the host's balance

Conventions

Rate limits. 120 requests a minute per connection; 10 a minute per address on the token endpoint. A 429 carries Retry-After.

Webhooks

We POST to one URL you give us. Signed with HMAC-SHA256 over "{timestamp}.{rawBody}":

X-Suitiee-Event: availability.changed
X-Suitiee-Event-Id: 3f2504e0-…          ← deduplicate on this
X-Suitiee-Timestamp: 1789231200          ← refuse anything older than your own window
X-Suitiee-Signature-Version: 2
X-Suitiee-Callback-Signature: sha256=…

Verify it, then answer any 2xx. We do not read your body.

When you fail. We retry nine times over about a day and a half. After 24 hours of continuous failure we pause your stream, email your engineering contact, and hold everything — nothing is dropped. When it is resumed, the held events go out oldest first with their original ids.

During a secret rotation you also receive X-Suitiee-Callback-Signature-Previous, the same body signed with the previous secret, for 24 hours — so you can deploy the new secret on your own schedule rather than ours.

The events

Event When
connection.authorized A host completed consent, or re-consented with different units
connection.units_changed The host changed which units you may see
connection.revoked The host disconnected you. Your tokens are already dead
listing.updated Something a channel would show about a linked unit changed
listing.deleted The unit was deleted, or the host stopped granting it
availability.changed Nights opened or closed
rates.changed Prices or stay restrictions moved
booking.modified One of your bookings was moved in Suitiee
booking.cancelled One of your bookings was cancelled or expired in Suitiee
booking.checked_out The turnover for one of your bookings was confirmed
message.sent The host answered one of your guests. Deliver it
message.thread_closed Nobody at Suitiee is waiting on that thread any more
turnover.updated A cleaning visit changed state. completed on a turnover means the flat is ready
maintenance.updated A maintenance visit changed state — including one you raised
test.ping Only ever sent by POST /webhooks/test

GET /webhooks/samples returns a complete, real body for every one of them.

Never in an event: a guest's email or phone, another channel's bookings, another channel's prices, an access code, or the name of the person here who answered.

One event carries a value rather than a pointer, and only one. message.sent contains the words. That is not an exception made for convenience: the pointer rule exists to stop a stale value overwriting a fresh one, and a message has no later version to be stale against — it is append-only and identified by its own id, so applying it twice or late ends in the same place. Deduplicate on message.id and you are done.

Setting prices and closing nights

Reading the calendar is calendar:read; writing it is calendar:write, and a host grants them separately — recording the stays you sell is not the same permission as repricing somebody's flat.

There is no separate "channel rate". You are writing the host's own calendar, so a price you set here is the price every other channel they sell on is told about: Airbnb and Booking.com within the minute, every other connected platform on the next drain.

curl -sX PUT "https://suitiee.com/api/v1/channel/units/$UNIT/calendar" \
  -H "Authorization: Bearer $ACCESS" \
  -H 'Content-Type: application/json' \
  -d '{
        "from": "2026-07-01",
        "to": "2026-07-31",
        "weekdays": [4, 5],
        "rate": "600.00",
        "min_stay": 2
      }'

Send ranges, not nights. from/to are inclusive and weekdays (1 = Monday … 7 = Sunday) narrows them, so a whole season of weekend pricing is one call. Ninety calls for ninety nights is ninety writes here and ninety events at every other channel the host sells on — and it will trip your own rate limit long before it finishes.

An absent field is left alone. null clears it. Raising a weekend price does not require resending a minimum stay you never touched — and if you resend it by habit, you silently overwrite whatever the host set. The two instructions are spelled differently on purpose:

You send What happens
"min_stay": 3 Minimum stay becomes 3
"min_stay": null The minimum stay is removed; the unit's default applies
(field absent) Untouched

The writable fields are rate, min_stay, max_stay, closed_to_arrival, closed_to_departure and blocked. The response is the calendar read back for the range you wrote, so you can see what it now says — including the nights your write did not open, because a booking already holds them.

blocked is not how a booking is recorded. Use it for nights you are holding for a reason we cannot see. A block carries no guest, no dates to modify and no cancellation, so a partner who blocks instead of calling POST /bookings leaves a host who cannot see who is in their flat and a stay that can never be reconciled.

You cannot reopen a night somebody else's stay is holding. Availability is derived from real bookings; blocked: false withdraws a block of your own and nothing else.

Operations: is the flat ready, and getting things done

Selling the night is the easy half. These are the questions that arrive afterwards, and none of them is answerable from a booking system alone.

"Can we check in early?"

curl -s "https://suitiee.com/api/v1/channel/bookings/GTH-88213/turnover" \
  -H "Authorization: Bearer $ACCESS"

status: "completed" means the previous guest has gone and the flat has been cleaned and signed off. Anything else means it has not, and window.end is the honest answer to when it will be. 404 TURNOVER_NOT_FOUND means nothing is scheduled yet — a booking whose checkout is next week has no turnover, which is a fact rather than an error.

You can also be told rather than asking: subscribe to turnover.updated.

"The air conditioning is broken"

curl -sX POST "https://suitiee.com/api/v1/channel/units/$UNIT/maintenance" \
  -H "Authorization: Bearer $ACCESS" \
  -H 'Content-Type: application/json' \
  -d '{
        "trade": "ac",
        "priority": "urgent",
        "issue": "Guest reports the air conditioning in the second bedroom blows warm.",
        "booking_id": "GTH-88213"
      }'

It costs nothing and commits nobody: the visit is recorded, dispatched to a trade, and quoted before any money is involved — which is why this needs only maintenance:write and not the scope that spends the host's balance.

trade is one of ac, plumbing, electrical, appliance, general. priority is low, normal, high or urgent, and it decides the window the visit is scheduled into — so it is a real field, not a label. Send urgent for no water, no power, or a door that will not lock. A partner who sends everything as urgent gets a host who stops believing any of it.

Write the issue in the guest's own words where you can. "AC in the second bedroom blows warm" gets fixed on the first visit; "broken" does not.

"Can we get it cleaned mid-stay?"

First find what can be ordered and what it costs this host:

curl -s "https://suitiee.com/api/v1/channel/services?unit_id=$UNIT&trade=cleaning" \
  -H "Authorization: Bearer $ACCESS"

The price comes back resolved through the host's package and any discount their management company negotiated — not a list price. Show a guest the number the host will actually be charged, or the host finds the difference on a statement. Without unit_id the price is null rather than a guess: a studio and a four-bedroom villa are not the same clean.

curl -sX POST "https://suitiee.com/api/v1/channel/units/$UNIT/cleanings" \
  -H "Authorization: Bearer $ACCESS" \
  -H "Idempotency-Key: clean-40021" \
  -H 'Content-Type: application/json' \
  -d '{
        "service_id": "2c4e1a90-…",
        "window_start": "2026-10-07T11:00:00+03:00",
        "window_end":   "2026-10-07T15:00:00+03:00",
        "notes": "Guest reports the kitchen needs extra attention."
      }'

This spends the host's money. The visit is charged to their package or their wallet the moment it is created, which is why it needs the cleaning:write scope of its own and why a host can grant you everything else and refuse this one. Send an Idempotency-Key: the retry a timeout produces must not bill a second visit.

Send a window a crew can be dispatched into. "Between 11:00 and 15:00" is schedulable; an exact minute is a promise nobody can keep in a city with traffic.

422 INSUFFICIENT_BALANCE means exactly that — no package visit left and not enough in the wallet. Nothing was created and nothing was charged. Tell the host, not the guest.

POST /cleanings/{id}/cancel calls off a visit you ordered. One the host or their operator arranged is not yours to cancel and answers 404. The host is refunded and the crew stood down, but not inside the 24-hour notice period and not once somebody is already in the flat — those answer 409 naming the rule.

Seeing the work on a unit

GET /units/{unit}/cleanings and GET /units/{unit}/maintenance list every visit on the unit, whoever booked it. That is deliberate: a flat being cleaned is a flat that cannot be shown, and a unit with an open urgent maintenance visit is one to stop selling before a guest arrives at it. What you never see on them is who is doing the work, what the host is paying, or which stay another channel sold — booking_id is filled only when the stay is one of yours.

Guest messages

If your platform holds the conversation — the guest asks about the door code in your app, not on an OTA — these two calls put that conversation in front of the person who can answer it. The host sees your guest in the same inbox as their Booking.com and Airbnb threads, in the same list, counted in the same unread badge.

A guest writes to you.

curl -sX POST https://suitiee.com/api/v1/channel/messages \
  -H "Authorization: Bearer $ACCESS" \
  -H "Idempotency-Key: msg-40021" \
  -H 'Content-Type: application/json' \
  -d '{
        "thread_id": "THREAD-9912",
        "message_id": "MSG-40021",
        "booking_id": "GTH-88213",
        "guest_name": "Sara Ali",
        "body": "What time can we check in?",
        "sent_at": "2026-10-03T18:22:00+03:00"
      }'

201 the first time, 200 if we already held that message_id — so replaying your own outbox after a reconnect is safe and tells you what we had. Send booking_id when you have one and the thread attaches itself to the stay, the dates and the guest record; send unit_id when you do not; send neither and the message still appears, and links itself when a later one carries the booking.

There is no sender field. This endpoint records what the guest said. If your own thread also contains the host's replies, do not post them back to us — you already received them, and a second copy would appear under the operator's own words.

The host answers. You receive it as a message.sent event:

{
  "event": "message.sent",
  "event_id": "3f2504e0-…",
  "data": {
    "connection_id": "4f0d0a02-…",
    "thread_id": "THREAD-9912",
    "booking_id": "GTH-88213",
    "message": {
      "id": "c7a1e5b4-…",
      "body": "Check-in is from 15:00. The door code is in your arrival email.",
      "sent_at": "2026-10-03T18:40:00+03:00"
    }
  }
}

If your receiver was down, read the window you missed rather than waiting for the retry schedule:

curl -s "https://suitiee.com/api/v1/channel/messages?thread_id=THREAD-9912&since=2026-10-03T00:00:00%2B03:00" \
  -H "Authorization: Bearer $ACCESS"

Both directions come back, newest first: your messages with your ids on them, the host's with ours. A reply still on its way to you reads "status": "pending", so you can tell "we never sent it" from "you never received it".

The host has to have granted messages:read for you to receive replies at all. If they did not, the reply box on that thread tells them so, in words, rather than sending into a hole — but it is worth asking for both message scopes on the consent screen if you carry guest conversations.

The bits that are not obvious

Bookings from other channels are invisible to you. A stay the host took on Airbnb, or entered themselves, does not appear in GET /bookings and never will. It appears on the calendar as a closed night, which is everything you need to stop selling it.

A booking is yours by your own id. Send GTH-88213; ask for GTH-88213. What we store internally is our business.

Modifying stops when the turnover is confirmed. Once the guest has left and the cleaning has been priced, charged and given to a crew, the booking cannot be edited — cancel it instead. 409 BOOKING_NOT_MODIFIABLE says so.

POST /bookings/{id}/checkout is optional. Suitiee confirms the turnover by itself at the contracted checkout time. Send it only when you actually know the guest has gone: it gets a cleaner there hours earlier.

Holds occupy nights. status: "pending" closes the nights exactly as a confirmed stay does — anything else would let the flat be sold twice — and is released automatically if you have not confirmed it within your application's hold window (60 minutes by default).

Going live

We tick a certification checklist as you demonstrate each behaviour against the sandbox:

When they are all ticked, we flip your application live. Same client id, same secret, same URLs.

The checklist covers the selling path only. Guest messages, calendar writes and the operational endpoints are optional — a marketplace that only sells nights is a complete integration and certifies without touching them. Nothing in this list becomes required because a capability exists.

What changes when you go live is that things become real: cleaning visits are charged to the host and dispatched to an actual crew, maintenance reaches an actual tradesperson, and your webhook deliveries stop being marked as test traffic. In the sandbox none of those leave the building, which is what makes it safe to exercise them.

The calls, end to end

Everything below is a real request. $TOKEN is a connection access token; $CID/$SECRET are your application's credentials.

The units a host granted you

curl https://suitiee.com/api/v1/channel/units \
  -H "Authorization: Bearer $TOKEN"
{ "data": [ { "unit_id": "9f2c…", "name": {"en": "Olaya Suite", "ar": "جناح العليا"},
              "city": "riyadh", "currency": "SAR", "listing": null } ],
  "meta": { "current_page": 1, "last_page": 1, "per_page": 20, "total": 1 } }

Its listing document — names, description, capacity, beds, amenities as canonical codes, address, photo URLs, check-in times, licence, headline price, and a hash:

curl https://suitiee.com/api/v1/channel/units/9f2c…/listing -H "Authorization: Bearer $TOKEN"

Its calendar

curl "https://suitiee.com/api/v1/channel/units/9f2c…/calendar?from=2026-10-01&to=2026-10-07" \
  -H "Authorization: Bearer $TOKEN"
{ "data": { "unit_id": "9f2c…", "currency": "SAR", "rate_plan": {"id": "standard"},
            "calendar_version": 1842,
            "nights": [
              {"date":"2026-10-01","available":true,"rate":"450.00","min_stay":2,
               "max_stay":null,"closed_to_arrival":false,"closed_to_departure":false,"reason":null},
              {"date":"2026-10-02","available":false,"rate":"450.00","min_stay":2,
               "max_stay":null,"closed_to_arrival":false,"closed_to_departure":false,"reason":"booked"}
            ] } }

Record your listing's id, so we can tell you when that unit changes:

curl -X PUT https://suitiee.com/api/v1/channel/units/9f2c…/listing-link \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"external_listing_id":"L-88213","url":"https://example.com/units/88213"}'

Send a booking

curl -X POST https://suitiee.com/api/v1/channel/bookings \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: gth-88213-create' \
  -d '{
    "booking_id": "GTH-88213",
    "unit_id": "9f2c…",
    "status": "confirmed",
    "checkin":  "2026-10-04T15:00:00+03:00",
    "checkout": "2026-10-07T11:00:00+03:00",
    "guest": {"name":"Sara Ali","email":"sara@example.com","phone":"+966500000000","language":"ar"},
    "amount": {"value":"1350.00","currency":"SAR"}
  }'

201 with the booking, or 201 plus meta.warnings if the nights were already taken. Send external_listing_id instead of unit_id once you have recorded the link. Retry with the same Idempotency-Key and you get this same response back, not a second stay.

Change it, call it off, or confirm the guest left

curl -X PATCH  …/bookings/GTH-88213          -d '{"checkout":"2026-10-09T11:00:00+03:00"}'
curl -X POST   …/bookings/GTH-88213/cancel
curl -X POST   …/bookings/GTH-88213/checkout -d '{"checkout_at":"2026-10-07T09:30:00+03:00"}'

Check what we did with something you sent

curl https://suitiee.com/api/v1/channel/events/gth-88213-create -H "Authorization: Bearer $TOKEN"

Prove your receiver, before any host has connected you:

curl -X POST https://suitiee.com/api/v1/channel/webhooks/test -u "$CID:$SECRET" -d event=test.ping
curl https://suitiee.com/api/v1/channel/webhooks/samples -H "Authorization: Bearer $TOKEN"

Reconcile, nightly:

curl https://suitiee.com/api/v1/channel/connections -u "$CID:$SECRET"
curl "https://suitiee.com/api/v1/channel/bookings?updated_since=2026-10-01T00:00:00%2B03:00" \
  -H "Authorization: Bearer $TOKEN"

Sending us unit details

If you already hold a filled-in listing — a host built their page on your site first and is now connecting — you can offer it to Suitiee with POST /units/import, in the same shape GET /units/{unit}/listing returns.

Nothing is changed by that call. It becomes a draft the host reviews field by field, and applying it is their act. That is deliberate: a host's unit record drives what a cleaner is told at the door, what every other channel is shown and what a regulator is given as the licence number, so one marketplace writing to it directly would propagate a single data-entry mistake everywhere with nothing marking where it came from.

Two shapes:

Unknown keys are ignored rather than refused, so your own richer payload is fine as it is. Poll GET /events/{your-idempotency-key} to see what became of the draft.

When it goes wrong

401 UNAUTHENTICATED — the access token is missing, expired or revoked. Refresh. If the refresh also fails, the host has disconnected you and you must delete your copy of their credentials.

403 CONNECTION_REVOKED — the token is fine and the host has ended the relationship. Retrying will never help. Stop selling their units and remove them from your platform.

403 INSUFFICIENT_SCOPEdetails.required_scope names the one you are missing. The host granted fewer permissions than you asked for, or narrowed them since. Send them through the consent link again asking for what you need.

404 UNIT_NOT_GRANTED — the unit is not yours. Either the host unticked it, or the id is wrong. Re-read GET /units; anything not in that list is not visible to you and asking again will keep answering 404.

409 LISTING_NOT_LINKED — you sent external_listing_id for a listing nobody linked. Send PUT /units/{unit}/listing-link first, or send unit_id instead.

409 IDEMPOTENCY_MISMATCH — you reused a key with a different body. That is a bug on your side: either your key generator repeats, or a retry mutated the payload. Do not work around it by generating a new key for the retry — find out which of the two bodies you meant.

409 BOOKING_NOT_MODIFIABLE — the turnover has been confirmed, priced and dispatched. Cancel and rebook.

429 — you are over 120 requests a minute on one connection. Honour Retry-After. If you are hitting it during a nightly reconciliation, use GET /bookings?updated_since= instead of walking every booking.

Your events stopped arriving. Check whether your endpoint is answering 2xx. After 24 hours of continuous failure we pause your stream and email your engineering contact; nothing is dropped, and everything held goes out when it is resumed. Ask us to resume it.

You think you missed an event. You probably did not need it: pull GET /units/{unit}/calendar and you have the current truth regardless. If you want certainty, we re-announce every listing's whole horizon once a night, so any divergence corrects itself within a day.

A booking you sent is not on the host's calendar. Look it up with GET /events/{your-idempotency-key} — it says what we did with the request and why we refused it if we did.

422 BEYOND_HORIZON on a calendar write. You are writing further ahead than we maintain calendars for. details.max_date is the furthest night you can set; ask us to widen the horizon on your application if you genuinely price further out than that.

422 INSUFFICIENT_BALANCE when ordering a clean. The host has no package visit left and not enough in their wallet. Nothing was created and nothing was charged. This is a message for the host, not for the guest — a guest cannot fix it.

422 SERVICE_UNAVAILABLE when ordering a clean. The service_id is not an active catalogue row. Re-read GET /services; codes and ids do change when the catalogue does.

404 CLEANING_NOT_FOUND on a cancel. Either the id is wrong, or the visit was not ordered by you. A clean the host or their operator arranged is not yours to call off, and we answer 404 rather than 403 so a guessed id tells you nothing.

409 CANCEL_TOO_LATE / CANCEL_BLOCKED. The visit is inside its 24-hour notice period, or a crew is already in the flat. Neither is retryable; a person has to sort it out with the host.

The reply box on a thread says the host cannot answer you. They did not grant messages:read, or you have no webhook receiver subscribed to message.sent. Both are fixable by you: ask for the scope at consent, and subscribe to the event.

If you are not ready to build against this, or you want something working this week, the calendar-link route still exists and needs no agreement from anybody: see Calendar links. It carries availability only — no prices, no guest, no money, and minutes of delay — but it works today and a host can set it up alone.

Last updated 2026-09-09

Was this page helpful?