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.
- Base URL —
https://suitiee.com/api/v1/channel - Full reference — the OpenAPI document, with every field, every error and every event body.
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
- A host on your platform presses Connect Suitiee. You send them to
/channel/authorize. - They sign in, tick which of their units you may see, and approve. Their browser comes back to you with a code.
- You exchange the code for an access token and a refresh token.
GET /units→ for each unitGET /units/{unit}/listing→ build your listing → tell us its id withPUT /units/{unit}/listing-link.GET /units/{unit}/calendar?from=&to=→ seed your availability, prices and minimum stays.- From then on we POST you a signed event whenever something changes, and you re-read the endpoint the event points at.
- 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
- Success —
{ "data": …, "message": "…" }· Errors —{ "error": { "code", "message", "details" } } - Lists —
?page=,?per_page=(max 100), withmetacarrying the page counts - Money — a decimal string plus an ISO currency, never a JSON number. A rate that has been through a float cannot be reconciled against a payout
- Dates — nights are
Y-m-d; datetimes are ISO 8601 with an offset. Stored times areAsia/Riyadh - Ids — ours are UUIDs (
unit_id,connection_id); yours are opaque strings up to 128 characters, and we always give yours back to you - Idempotency — send
Idempotency-Keyon every write. The same key with the same body returns the first result; the same key with a different body is a409 - Every response carries
X-Suitiee-Api-Version: 1andX-Request-Id, including 401s, 422s and 429s
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:
- the consent round-trip, and a code exchanged
- an access token refreshed
- a listing link recorded
- a calendar pulled and applied
- a booking created, modified and cancelled
- an overlap warning handled
- a webhook signature verified
- a 24-hour pause survived and the held events replayed
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:
target: {unit_id: "…"}proposes changes to a unit you already have.target: {new: true, name: "…"}proposes a unit the host does not have in Suitiee yet. If they accept it, the unit is created for them, inactive, for them to finish filling in.
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_SCOPE — details.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.
Calendar links are still there
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.