Connect your software (PMS, property management or campground tool) to Airbnb, Booking.com and the other platforms through Nestiz. You send your listings, prices and closures; you receive the bookings. Nestiz handles the platforms.
At a glance
- Base URL:
https://nestiz.fr/api/v1/…(orhttps://app.nestizapp.fr/api/v1/…) - Format: JSON, UTF-8. Dates
YYYY-MM-DD, timestamps ISO 8601 in UTC. - Key in every call:
Authorization: Bearer nz_test_… - Vendor space: nestiz.fr/editeurs/compte (sign-in link sent by e-mail; create your free test account yourself at nestiz.fr/editeurs/compte?inscription=1): your keys (create, revoke), your subscription, your recent calls. A key is shown only once, when created, and is never sent by e-mail.
- Two keys, as with Stripe:
nz_test_…, the sandbox: nothing is sent to the platforms. Bookings and connections are simulated. Made for development.nz_live_…, live mode: opened by Nestiz when your integration is ready.
- Test and live data are kept apart: a test key never sees a live listing, and the other way round.
- Vacation rentals (apartments, houses) and properties (hotels, guest houses, hostels, resorts, campgrounds): a property groups its room types.
- Platforms: Airbnb, Vrbo, Booking.com, Google, Expedia, Agoda, Trip.com, Hostelworld and about a hundred more (booking sites, campgrounds, travel agencies, booking engines): the full list is given by
GET /platforms. - Online guide: nestiz.fr/editeurs/docs. Machine-readable description (OpenAPI 3.1, no key needed): nestiz.fr/api/v1/openapi.json, to import into Postman or to give to your code assistant.
- Safe retries:
Idempotency-Keyheader on anyPOST(see "Retrying a call"). Every response carriesNestiz-Request-Id, the id of the call: quote it when you write to us. - No certification and no call with us:
GET /checklistticks itself as you test (see "Integration checklist").
curl https://nestiz.fr/api/v1/ping -H "Authorization: Bearer nz_test_…"
# {"object":"ping","mode":"test","account":"My PMS"}The examples are written for a Linux or macOS terminal. On Windows, the command prompt and PowerShell strip the quotes of a JSON passed between single quotes (-d '{"name":…}'), and the API answers 422 "The request body is not valid JSON". Put the JSON in a file, or use PowerShell:
# curl: the JSON in a file listing.json
curl.exe -X POST https://nestiz.fr/api/v1/listings -H "Authorization: Bearer nz_test_…" -H "Content-Type: application/json" --data "@listing.json"
# PowerShell
$body = @{ name = "Vieux-Port studio"; city = "Marseille"; external_id = "pms-1234"; max_guests = 2 } | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri https://nestiz.fr/api/v1/listings -Headers @{ Authorization = "Bearer nz_test_…" } -ContentType "application/json" -Body $bodyGetting started
The whole integration is done with the test key, against the sandbox: nothing reaches a real platform, and everything a platform can do (book, modify, cancel, write, review) can be triggered by an API call.
- Check your key:
GET /pingreturns your account name and"mode":"test". - Create a listing, with your own id in
external_id:POST /listings(a rental), orPOST /propertiesthen onePOST /listingsper room type (hotel, hostel, campground). Keep the returnedid. - Send the full calendar once: one
PUT /listings/{id}/calendarwith the coming year (prices, closures, stay rules), as your software has it. This is what happens when a customer goes live. - Then send only what changes: each time a user saves a price, a closure or a minimum stay in your software, one call with only those nights (see "Best practices").
- Connect a platform:
POST /listings/{id}/channels,GET …/options,POST …/activate. With the test key the platform's side is simulated (anyhotel_id, Airbnb's consent page answered at once). - Receive bookings: create a webhook (
POST /webhooks) and also readGET /bookingswith the cursor. Then play the platform with the sandbox: a new booking (POST /sandbox/bookings), a modification (PATCH /sandbox/bookings/{id}), a cancellation (POST /sandbox/bookings/{id}/cancel). Your software must create the stay, then move it, then release it. - Direct bookings: when your software takes a booking itself (phone, website),
POST /bookings, thenPATCHand…/cancel: the nights close and reopen on every platform. - Check
GET /checklist. When every step is ticked, write to us: we open live mode, you get anz_live_…key, and the same code runs against the real platforms.
Listings
GET /listings | List (paginated); filters external_id (find a listing by your own id), property_id (the room types of a property) |
POST /listings | Create |
GET /listings/{id} | Read |
PATCH /listings/{id} | Update |
DELETE /listings/{id} | Remove: nothing is sold any more |
curl -X POST https://nestiz.fr/api/v1/listings \
-H "Authorization: Bearer nz_test_…" -H "Content-Type: application/json" \
-d '{"name":"Seaside studio","city":"Valparaíso","country":"CL","currency":"CLP","timezone":"America/Santiago","external_id":"pms-1234","units":1,"max_guests":2}'| Field | Default | |
|---|---|---|
name | required, 120 characters at most | |
external_id | your own id, unique per account | none |
property_id | property (hotel, campground…) this room type belongs to (see below) | none |
city | none | |
country | ISO 3166-1 alpha-2 code | FR |
currency | ISO 4217 code, one of the 147 of GET /currencies (EUR, USD, MXN, BRL, ARS…); cannot be changed later | EUR |
timezone | IANA time zone | Europe/Paris |
units | room type of a property only: identical units sold together (1 to 50). A rental is always 1 (otherwise 422): for several identical units, create a property | 1 |
max_guests | 1 to 30 | 2 |
guests_included, extra_guest_fee, fewer_guest_discount | per-person pricing (see "Per-person pricing") | null, 0, 0 |
An external_id already in use returns 409 with the id of the existing listing: you can safely retry a creation without creating a duplicate. A deleted listing frees its external_id (and its name): you can create it again as is.
The response also carries sync_error: in live mode, the last update (listing details, calendar) the channel manager did not take, null once everything went through.
Your account has a listing cap (500, against a runaway creation loop). Beyond it: 403 limit_reached. Ask us to raise it.
GET /currencies lists the accepted currencies, with their decimals and the bounds of a nightly rate (rate_bounds, in currency units):
{ "object": "currency", "code": "CLP", "decimals": 0, "rate_bounds": { "min": 3000, "max": 2200000 } }Properties and room types
A rental is a single listing. A property (hotel, hostel, campground…) is a property (/properties) that groups its room types, each of them a listing (/listings) with property_id. Prices, calendar, bookings and platforms are managed per room type, exactly like a rental.
For a campground: one property, and one room type per kind of accommodation (tent pitch, RV pitch, cabin…), with units = the number of identical pitches or cabins.
GET /properties | List (paginated); filter external_id |
POST /properties | Create |
GET /properties/{id} | Read (with room_types and rooms) |
PATCH /properties/{id} | Update (name, city, country, time zone, external_id) |
DELETE /properties/{id} | Remove, once its room types are removed |
curl -X POST https://nestiz.fr/api/v1/properties \
-H "Authorization: Bearer nz_test_…" -H "Content-Type: application/json" \
-d '{"name":"Camping del Lago","type":"resort","city":"Bariloche","country":"AR","currency":"ARS","timezone":"America/Argentina/Buenos_Aires","external_id":"pms-c1"}'
curl -X POST https://nestiz.fr/api/v1/listings \
-H "Authorization: Bearer nz_test_…" -H "Content-Type: application/json" \
-d '{"name":"Tent pitch","property_id":"…","units":40,"max_guests":6,"guests_included":1,"extra_guest_fee":4500}'| Property field | Default | |
|---|---|---|
name | required | |
type | hotel, guest_house, hostel, resort; cannot be changed later | hotel |
external_id, city, country, currency, timezone | as for a listing; currency cannot be changed | |
messaging | guest messaging and reviews for the property, paid option (see Pricing); can be changed | false |
- A room type inherits the city, country, currency and time zone of its property: do not send them (
422).units= number of identical rooms, pitches or cabins. - 20 room types at most per property.
- A booking can cover several units of the same room type: field
rooms.
Hostel dorms: selling beds
A room type with "kind": "dorm" sells the beds of a dormitory instead of whole rooms. units is the number of beds for sale (up to 500), and beds_per_room the beds in one physical room (default: all beds in one room). One guest per bed: max_guests is 1 and the calendar rate is the price of one bed per night. Private rooms of the same hostel stay ordinary room types.
# Two 6-bed dorm rooms sold as one room type: 12 beds, €25 a bed
curl -X POST https://nestiz.fr/api/v1/listings \
-H "Authorization: Bearer nz_test_…" -H "Content-Type: application/json" \
-d '{"name":"Bed in 6-bed mixed dorm","property_id":"…","kind":"dorm","units":12,"beds_per_room":6}'- Calendar and availability count beds:
units12,booked3,available9. - A booking takes one bed per guest (adults and children; infants need no bed):
roomsis the number of beds. A platform may send one room per bed or a single room for the whole group: both are counted the same way. - Billing counts physical rooms, not beds: 12 beds of 6 per room = 2 rooms.
- Booking.com, Hostelworld, Expedia, Agoda and the other hotel platforms sell dorm beds (they receive the price for 1 person). Airbnb, Vrbo and Google do not: connecting a dorm to them is refused (
422 not_supported). kindcannot be changed later;unitsandbeds_per_roomcan.
Calendar: prices, closures, minimum stay
PUT /listings/{id}/calendar updates nights by ranges. to is included, and rate is in the listing currency (95 = 95.00).
{
"ranges": [
{ "from": "2026-11-01", "to": "2026-11-30", "rate": 95, "min_stay": 2 },
{ "date": "2026-11-11", "closed": true },
{ "from": "2026-12-20", "to": "2026-12-31", "rate": 140, "closed_to_arrival": false }
]
}| Range field | |
|---|---|
date, or from and to | one night, or a range of nights |
rate | nightly rate (bounds per currency, see GET /currencies; 3 to 2,000 in euros) |
closed | true: night closed for sale |
min_stay | minimum nights for an arrival that day (1 to 90); null removes it |
max_stay | maximum nights for an arrival that day (1 to 730); null removes it |
min_stay_through | minimum nights of any stay that includes this night (1 to 90); null removes it |
closed_to_arrival, closed_to_departure | no arrival or no departure that day |
- A missing field stays unchanged.
- If two ranges overlap, the last one wins.
- All or nothing: one invalid value and nothing is saved. The
422error points to the faulty range (details.index). - Nights go from today (in the listing time zone) to 730 days ahead. Nights of a range that are already past are skipped (
past_nights_ignoredin the response); a range that ended before yesterday is refused (422). A range that runs past the window ("two years from today") is taken up to the last accepted day and the rest is skipped (nights_beyond_window_ignored); a range entirely beyond it is refused. - One call covers 730 nights and 500 ranges at most.
- A night without a price is never for sale.
GET /listings/{id}/calendar?from=2026-11-01&to=2026-11-30 (30 days by default, 730 at most):
{ "date": "2026-11-11", "rate": 95, "closed": true, "min_stay": 2,
"max_stay": null, "min_stay_through": null, "closed_to_arrival": false, "closed_to_departure": false,
"units": 1, "booked": 0, "available": 0 }available is what is actually for sale: the units, minus the stays, and 0 if the night is closed or has no price. In live mode, a night is for sale only once the platforms have received its price: syncing is instant, with a delay of 0 to 30 seconds depending on the platform.
Platform rejections. 200 means Nestiz saved the calendar. A platform can still reject the update a second later (Airbnb: "You can only accept long-term rentals (10 or more)" when a night allows shorter stays than the account permits). A few seconds later, the connection carries the platform's own message in error, its reason in error_code and what the rejected update held in rejected_update (GET /listings/{id}/channels), and the channel.updated webhook is sent. A rejected update is rejected whole, prices included: fix the values, then call POST …/channels/{channel}/resync (once a minute). The full calendar goes out again and the answer waits for the platform (a few seconds): status accepted, or rejected with error, error_code and rejected_update; started if the platform is slower (check GET …/channels/{channel}).
Rate plans
The standard plan follows the calendar. Add derived plans: a percentage or an amount applied to the standard price of each night, with the same closures and stay rules (non-refundable, breakfast included…).
GET /listings/{id}/rate_plans | The listing's plans (standard first) |
POST /listings/{id}/rate_plans | {"name":"Non-refundable","adjustment":{"type":"percent","value":-10}} |
PATCH /listings/{id}/rate_plans/{plan} | name, adjustment, meal_type |
DELETE /listings/{id}/rate_plans/{plan} | Remove (once its mapping is removed) |
adjustment.type:percent(−90 to +200) oramount(−1,000 to +1,000, in the listing currency). Negative = discount,0= the standard price.meal_type:none,room_only,breakfast,bed_and_breakfast,half_board,full_board,all_inclusive,lunch,dinner,self_catering.- 10 plans at most per listing, standard included.
GET …/calendarthen also returnsrate_plans: the price of each plan, night by night.- A booking states its
rate_plan_id(standardor the plan id).
Map a plan to a platform rate (Booking.com and the platforms with a property id; Airbnb and Vrbo have a single rate per listing): the standard plan is mapped by activate; each derived plan afterwards with
GET …/channels/{channel}/rate_mappings | Mapped derived plans |
POST …/channels/{channel}/rate_mappings | {"option_id":"…","rate_plan_id":"…"} (option_id from GET …/options) |
DELETE …/channels/{channel}/rate_mappings/{plan} | Remove |
A different price per platform (to cover a platform's commission, for instance): create a derived plan ({"name":"Airbnb","adjustment":{"type":"percent","value":15}}), then have it feed the platform. Airbnb and Vrbo take a single rate per listing: choose the plan that feeds it, before or after activate:
PUT …/channels/airbnb/rate_plan | {"rate_plan_id":"…"} (a derived plan), or "standard" |
PUT …/channels/vrbo/rate_plan | Same |
The platform then receives, night by night, the calendar price with the plan's adjustment, and the same closures and stay rules. Switching plans (or changing the adjustment) after activate does not cut the listing off: only prices change. An Airbnb listing connected before 2026-11-07 on the standard plan may answer 409 reconnect_required: disconnect it, pick the plan, then connect it again (once). GET …/channels/{channel} shows rate_plan_id. A plan that feeds a platform cannot be deleted. Booking.com and the others: map the derived plan with rate_mappings above.
Per-person pricing
By default, a night has the same price whatever the number of guests. For a price that depends on the number of guests (hostel bed, campground pitch, cabin), set on the listing (POST or PATCH /listings/{id}):
| Field | |
|---|---|
guests_included | guests included in the calendar price (1 to max_guests); null: same price for everyone |
extra_guest_fee | per additional guest, per night, in the listing currency |
fewer_guest_discount | per guest below guests_included, per night (double room sold to one person) |
curl -X PATCH https://nestiz.fr/api/v1/listings/{id} \
-H "Authorization: Bearer nz_test_…" -H "Content-Type: application/json" \
-d '{"guests_included":1,"extra_guest_fee":12}'
# Pitch at 20 a night for 1 person: 32 for 2, 44 for 3…The calendar keeps a single price per night, the one for guests_included guests. Each platform receives the rest its own way:
- Booking.com with occupancy-based pricing (OBP), Agoda, Trip.com, Hostelworld and the other platforms priced per number of guests: one price for each number of guests, computed by Nestiz, derived plans included;
- Airbnb: guests included and extra guest fee of the listing (whole amounts; Airbnb has no discount for fewer guests);
- Booking.com with a single price (RLO) and Vrbo: the price sent is the one for the guests included; the differences are set in the platform extranet.
Amounts: 2 decimals at most, at most a quarter of the currency's maximum nightly rate (500 in euros). An amount that can never apply is not kept (an extra guest fee when guests_included = max_guests, for instance).
Bookings
GET /bookings: bookings in the order of their last change. A cancellation or a change of dates therefore brings the booking back into the list.
Parameters:
listing_id: one listing only;property_id: every room type of a property;status:confirmedorcancelled;updated_since: only bookings changed since this date (ISO 8601);limit: 1 to 100 (50 by default);cursor: to read the next page.
To miss nothing: keep the next_cursor of each response and pass it to the next call. Read every page while has_more is true, then come back every minute with the last cursor.
{
"object": "list",
"data": [{
"id": "…", "object": "booking", "mode": "live", "listing_id": "…", "property_id": null,
"platform": "airbnb", "status": "confirmed",
"arrival": "2026-11-05", "departure": "2026-11-08", "nights": 3,
"guest": { "name": "Jane Martin", "email": "…", "phone": "…", "language": "en",
"country": "US", "city": "Boston", "address": "…", "zip": "02108", "arrival_time": "18:00" },
"adults": 2, "children": 0, "infants": 0, "rooms": 1, "rate_plan_id": "standard",
"amount": 285.0, "currency": "EUR",
"ota_reservation_code": "HMXXXX", "notes": null,
"payment": { "collected_by": "property", "type": "credit_card", "platform_commission": 42.75,
"platform_payment_fee": null,
"card": { "brand": "visa", "last4": "1111", "expiration": "12/2028",
"holder": "Jane Martin", "virtual": false } },
"price": { "nights": [{ "date": "2026-11-05", "amount": 95.0 }], "discounts": null,
"cleaning": 30.0, "extra_fees": null, "city_tax": 6.6,
"city_tax_collected_on_site": false, "tax_collected_by_platform": null,
"security_deposit": null },
"revision": 1,
"created_at": "…", "updated_at": "…"
}],
"has_more": false,
"next_cursor": "…"
}statusisconfirmedorcancelled. Each change increasesrevisionby 1.- A modification keeps the same
id(and the sameota_reservation_code): update the stay you already have, never create a second one. Apply a version only if itsrevisionis higher than the one you hold (the same event can arrive twice, or out of order). platform: the platformchannel(airbnb,booking,hostelworld…, seeGET /platforms),direct, orotherif it is not recognised.rooms: units of this room type booked (property), 1 for a rental.payment.collected_by:platform(the guest has already paid the platform),property(to be collected by the host) ornullif the platform does not say.payment.card: masked summary of the guarantee card (brand, last 4 digits, expiry, holder; virtual card:virtual_balance,virtual_currency,virtual_activation,virtual_expiration). The full number and the CVV never go through the API.price: what the platform details (price per night, discounts, cleaning, city tax…),nullwhen it does not send it.GET /bookings/{id}reads a single booking.- Nestiz updates availability as soon as a booking arrives: you have nothing to send back.
Direct bookings
A booking taken by you or your customer (phone, website, on site) is recorded in Nestiz: its nights are closed at once on every platform.
POST /bookings | Create: listing_id, arrival, departure, guest_name required; guest_email, guest_phone, adults, children, infants, rooms, amount, reference (your number), notes |
PATCH /bookings/{id} | Update (dates, guest, amount…): only the fields sent change (departure alone, for example) |
POST /bookings/{id}/cancel | Cancel: the nights are released |
platformisdirect. Yourreferencecomes back inota_reservation_code.- The guest can also be sent in a
guestobject, with the same fields as in the response:{"guest":{"name":"Jane Martin","email":"…","phone":"…","language":"en","country":"GB","city":"…","address":"…","zip":"…","arrival_time":"18:00"}}. - Closures and minimum stays do not apply (the host is booking); only overbooking is refused (
409 not_available, withdetails.date). - Only direct bookings can be changed here (
409 not_directotherwise): a platform booking is changed on the platform. - They trigger the same webhooks as the others.
Charging the guarantee card
For a "pay at the property" booking (Booking.com, Expedia…), charge the card left by the guest without ever seeing its number: the money goes straight to your customer's Stripe account.
GET /listings/{id}/payments | Stripe account connected? (connected, stripe_account) |
POST /listings/{id}/payments/connect | {"return_url":"https://…"}: Stripe link where your customer connects their account |
POST /bookings/{id}/charges | {"amount":150,"idempotency_key":"…","description":"…"} |
GET /bookings/{id}/charges | Charges of the booking |
idempotency_keyrequired (8 to 100 characters): the same key never charges twice and returns the same result. Always retry with the same key.- Charge declined (card declined, bank authentication required):
status: "failed". - Refused outright: booking without a card (
409 no_card), already paid on the platform (409 already_paid), or above the booking amount (422). - Sandbox: everything is simulated, the charge succeeds.
Guest messaging
Messages from Airbnb, Booking.com, Vrbo, Expedia and the other platforms that allow it, Airbnb inquiries included (a conversation with no booking yet). Agoda too: Agoda does not open its messaging to channel managers, so Nestiz writes to the guest's relay address (the message reaches their Agoda inbox) and the guest's reply comes back into the same conversation, with the message.received webhook. Nothing changes for you: same calls.
GET /conversations | Conversations, most recent first, with their last message; filters listing_id, property_id, booking_id, updated_since; limit, cursor |
GET /conversations/{id} | One conversation and its messages, in order (the last 500) |
POST /conversations/{id}/messages | Reply: {"text":"…"}, or an attachment (below) |
POST /conversations/{id}/close | Close the conversation (it reopens with the next guest message) |
GET /bookings/{id}/messages | Messages of a booking's conversation (empty list if none yet) |
POST /bookings/{id}/messages | Write to the guest of a booking (the conversation opens if needed) |
{ "id": "…", "object": "message", "conversation_id": "…", "sender": "guest",
"text": "Hello, what time can we arrive?", "attachments": [], "sent_at": "2026-11-01T09:12:03Z" }Attachments. attachments lists the links of the files in a message. To send one (image or PDF, 5 MB at most), in a message of its own, with no text (the platforms take one or the other):
{ "attachment": { "file_name": "access-plan.pdf", "file_type": "application/pdf", "data": "<the file in base64>" } }file_type: image/jpeg, image/png, image/gif, image/webp or application/pdf. Body of the call: 8 MB at most (a 5 MB file is about 6.7 MB in base64).
- A reply (
POST) returns the message, with the sameidas inGET /conversations/{id}. - Pages:
GET /conversationsreturnshas_moreandnext_cursor. Read every page whilehas_moreistrue, passingnext_cursorascursor; on the last page,next_cursorisnull. - To miss nothing: the
message.receivedwebhook, andupdated_sinceas a backstop. It compares the conversation'supdated_at, the time it changed at Nestiz (new message, closed, guest name), not the time the message was written: a message that reaches us late is never skipped. senderisguestorproperty(you, or the host from the platform).- A guest message triggers the
message.receivedwebhook (data= the message, withlisting_id,property_id,booking_id,platform,guest_name). - Property with several room types: every conversation and every webhook carries
listing_idandproperty_id(the property,nullfor a rental). Conversation of a booking:listing_idis the booked room type. A guest who writes before booking (booking_idnull): the platform does not always say which room, solisting_idis then the property's first room type; rely onproperty_id. The conversation moves to the right room type as soon as the booking (or the Airbnb request) arrives, and itsupdated_atchanges. - Never send twice: add
Idempotency-Keywhen you send a message (see "Retrying a call"). - Direct booking: no platform, so no message (
409 not_supported). - Sandbox:
POST /sandbox/messageswith{"booking_id":"…","text":"…"}simulates a guest message.
Guest reviews
GET /reviews | Reviews, most recent first; filters listing_id, property_id, updated_since; limit, cursor (pages as for conversations) |
GET /reviews/{id} | One review |
POST /reviews/{id}/reply | Public reply: {"text":"…"} (once) |
POST /reviews/{id}/guest_review | Airbnb: your review of the guest |
{ "id": "…", "object": "review", "listing_id": "…", "booking_id": "…", "platform": "booking",
"guest_name": "Jane", "overall_score": 9.2, "scores": { "clean": 10, "location": 9 },
"headline": "…", "public_text": "…", "positive_text": "…", "negative_text": "…",
"private_text": null, "tags": [], "hidden": false, "received_at": "…",
"reply": null, "replied_at": null, "guest_review_sent": false }overall_scoreon the platform's scale (10 for Booking.com, 5 for Airbnb).- An Airbnb review stays
hiddenuntil both sides have written (14 days at most). - Review of the guest:
{"scores":[{"category":"cleanliness","rating":5},{"category":"communication","rating":5},{"category":"respect_house_rules","rating":5}],"public_review":"…","private_review":"…","is_reviewee_recommended":true}. - A new review triggers the
review.receivedwebhook. - Sandbox:
POST /sandbox/reviewswith{"booking_id":"…","score":9,"text":"…"}.
Airbnb listing settings
Everything Airbnb lets a channel manager set on a connected listing, under /listings/{id}/channels/airbnb/… (the listing must be connected to Airbnb, otherwise 409 not_connected). GET reads, PATCH changes only the fields sent; a list sent (fees, discounts, taxes…) replaces the whole list. Airbnb's refusals come back as 422 platform_refused, with Airbnb's message.
Prices and the minimum stay stay night by night in the calendar (PUT /listings/{id}/calendar); guests included and the extra guest fee follow the listing (guests_included, extra_guest_fee). Weekend, seasonal and promotional prices are yours to compute: send the resulting price of each night.
GET/PATCH …/availability_settings | Booking lead time, booking window, preparation time, check-in and check-out days, maximum stay |
GET/PATCH …/pricing_settings | Fees, length-of-stay, early-bird and last-minute discounts, taxes collected by Airbnb |
GET/PATCH …/booking_settings | Instant booking, check-in and check-out times, cancellation policy, house rules, what guests must know |
GET/PATCH …/checkout_tasks | Tasks asked of departing guests |
GET …/promotions | The listing's promotions and their status |
POST …/promotions/{type}/enable, …/disable | NEW_LISTING_PROMOTION, HIGH_RATED_GUEST_DISCOUNT, MOBILE_ONLY_DISCOUNT |
GET …/opportunities | Improvements and discount programs Airbnb suggests for the listing |
POST …/opportunities/{id}/apply | {"input_fields":{…}} (the fields the opportunity lists) |
POST …/publish, POST …/unpublish | Publish or unpublish the Airbnb listing (it stays connected); unpublish takes {"reason":"…","details":"…"} |
GET …/unpublish_reasons | Airbnb's questionnaire of reasons (?locale=fr) |
GET …/calendar?from=…&to=… | The calendar as Airbnb has it: price, availability, stay rules, notes (366 days at most) |
GET …/content | The listing content, read-only: title, description, photos, rooms and beds, amenities, address, capacity |
Availability (availability_settings):
| Field | |
|---|---|
booking_lead_time | minimum notice in hours: 0 to 24, 48, 72 or 168 |
max_days_notice | booking window in days: -1 (no limit), 0, 30, 60, … 365 |
allow_rtb_beyond_max_days_notice | requests to book allowed beyond the window |
default_max_nights | maximum stay, 1 to 1,125 (0 = Airbnb default) |
allow_rtb_above_max_nights | requests to book allowed above it (28 nights or more) |
day_of_week_check_in, day_of_week_check_out | 7 booleans, Monday first |
turnover_days | preparation time between stays: 0, 1 or 2 days |
Fees, discounts, taxes (pricing_settings):
{
"standard_fees": [
{ "fee_type": "PASS_THROUGH_CLEANING_FEE", "amount_type": "flat", "amount": 40 },
{ "fee_type": "PASS_THROUGH_PET_FEE", "amount_type": "flat", "amount": 15, "charge_type": "PER_PET", "charge_period": "PER_BOOKING" }
],
"default_pricing_rules": [
{ "rule_type": "STAYED_AT_LEAST_X_DAYS", "threshold_one": 7, "price_change": 10 },
{ "rule_type": "BOOKED_BEYOND_AT_LEAST_X_DAYS", "threshold_one": 60, "price_change": 5 },
{ "rule_type": "BOOKED_WITHIN_AT_MOST_X_DAYS", "threshold_one": 3, "price_change": 15 }
]
}standard_fees:fee_typeamongPASS_THROUGH_CLEANING_FEE,PASS_THROUGH_SHORT_TERM_CLEANING_FEE,PASS_THROUGH_PET_FEE,PASS_THROUGH_LINEN_FEE,PASS_THROUGH_RESORT_FEE,PASS_THROUGH_MANAGEMENT_FEE,PASS_THROUGH_COMMUNITY_FEE,PASS_THROUGH_ELECTRICITY_FEE,PASS_THROUGH_WATER_FEE,PASS_THROUGH_HEATING_FEE,PASS_THROUGH_AIR_CONDITIONING_FEE,PASS_THROUGH_UTILITY_FEE,PASS_THROUGH_SECURITY_DEPOSIT;amount_typeflat(whole amount in the listing currency) orpercent(0 to 100);charge_typePER_GROUP(default),PER_PERSON,PER_PET(pet fee only);charge_periodPER_BOOKING(default) orPER_NIGHT;offline= paid on site;fee_unit_typefor metered fees.default_pricing_rules:price_change= discount in percent (1 to 99).STAYED_AT_LEAST_X_DAYS= length of stay (weekly: 7, monthly: 28);BOOKED_BEYOND_AT_LEAST_X_DAYS= early bird (threshold_onea multiple of 28 or 30);BOOKED_WITHIN_AT_MOST_X_DAYS= last minute (28 days at most). Airbnb applies the best discount that matches, on top of the nightly price.pass_through_taxes: occupancy taxes collected by Airbnb (tax_type,amount_type,amount,taxable_base…, as in Airbnb's API) andpass_through_taxes_collection_type.GETalso givescurrency,guests_includedandprice_per_extra_person(read only).
Booking (booking_settings): instant_booking_allowed_category (everyone or well_reviewed_guests), instant_book_welcome_message (200 characters), check_in_time_start ("8" to "26", "FLEXIBLE"), check_in_time_end ("8" to "27", "FLEXIBLE"), check_out_time (0 to 23), cancellation_policy_settings (cancellation_policy_category: flexible, moderate, firm_14, better_strict_with_grace_period, strict_14_with_grace_period, super_strict_30, super_strict_60 and their _new versions; lts_cancellation_policy_id for 28 nights or more; non_refundable_price_factor, 0 to 1, for a non-refundable rate), guest_controls (allows_children_as_host, allows_infants_as_host, allows_pets_as_host, pet_capacity, allows_smoking_as_host, allows_events_as_host) and listing_expectations_for_guests ([{"type":"requires_stairs","added_details":"…"}]). Airbnb does not give these settings back: GET returns what you sent through the API.
Checkout tasks: return_keys, turn_things_off, throw_trash, lock_up, gather_towels, additional_requests, each {"is_present":true,"task_detail":"…"}.
Listing content (GET …/content, read-only): read from Airbnb on each call. Photos in Airbnb's order, large size; description = summary, the space, access and the rest, each text once; rooms: room_number 0 = shared spaces; a field Airbnb does not give is null. Title, photos and description can only be changed on Airbnb. To reuse them in the listing details, send them in PUT /listings/{id}/content.
{
"object": "airbnb_listing_content",
"listing_id": "lst_…",
"airbnb_listing_id": "1234567890",
"title": "Bright flat by the Canal Saint-Martin",
"description": "Bright one-bedroom flat…\n\nQueen bed in the bedroom…",
"photos": [{ "url": "https://…", "caption": "Living room" }],
"property_type": "apartment",
"capacity": 4, "bedrooms": 1, "beds": 2, "bathrooms": 1,
"rooms": [{ "room_number": 1, "beds": [{ "type": "queen_bed", "quantity": 1 }] }],
"amenities": ["Wifi", "Kitchen", "Washer"],
"address": { "street": "12 quai de Valmy", "zip": "75010", "city": "Paris", "country": "FR",
"formatted": "12 quai de Valmy, 75010 Paris", "latitude": 48.8722, "longitude": 2.3651 },
"checkin_time": "16:00", "checkout_time": "11:00"
}Sandbox: everything is kept, nothing goes to Airbnb; one opportunity is offered (sandbox_weekly_discount), …/calendar returns your own calendar and …/content a simulated listing.
Airbnb requests
What Airbnb asks the host to decide, with 24 hours to answer:
kind | The guest… | Answers |
|---|---|---|
reservation_request | asks to book (no instant booking) | accept, decline |
inquiry | asks about dates without booking | preapprove, special_offer |
alteration_request | asks to change a confirmed booking | accept, decline, withdraw |
GET /requests | Requests, most recent change first; filters listing_id, property_id, status (pending…), updated_since; limit, cursor (pages as for conversations) |
GET /requests/{id} | One request |
POST /requests/{id}/accept | Accept |
POST /requests/{id}/decline | Decline; reservation request: {"reason":"dates_not_available","message_to_guest":"…","message_to_airbnb":"…"} (reason: dates_not_available, not_a_good_fit, waiting_for_better_reservation, not_comfortable) |
POST /requests/{id}/preapprove | Inquiry: invite the guest to book at the current price; {"block_instant_booking":true} holds the dates meanwhile |
POST /requests/{id}/special_offer | Inquiry: {"total_price":450}, whole amount for the whole stay, fees included, without taxes or deposit |
POST /requests/{id}/withdraw | Withdraw an alteration request |
{ "id": "…", "object": "request", "kind": "reservation_request", "status": "pending",
"platform": "airbnb", "listing_id": "…", "booking_id": null, "conversation_id": "…",
"arrival": "2026-12-04", "departure": "2026-12-07", "adults": 2, "children": 0, "infants": 0,
"amount": 420.0, "currency": "EUR", "guest_name": "David",
"actions": ["accept", "decline"], "expires_at": "2026-11-02T09:12:03Z", … }status:pending,accepted,declined,preapproved,special_offer_sent,withdrawn,expiredorclosed(settled on Airbnb). A decision is final.- An accepted reservation request becomes a booking once Airbnb has charged the guest: it arrives as any other (
booking.created). An accepted alteration updates the booking (booking.updated). Declining a request for unavailable dates keeps them blocked on Airbnb until you reopen them. - An inquiry cannot be declined: leave it unanswered and it lapses.
conversation_id: the guest's conversation, to write to them.- Webhooks
request.received(new request) andrequest.updated(answered, or settled on Airbnb). - Sandbox:
POST /sandbox/requestswith{"kind":"reservation_request","listing_id":"…","arrival":"…","departure":"…","adults":2}(alteration_request:booking_idand the new values). Accepting creates or changes the simulated booking, with its webhooks.
Webhooks
Rather than polling GET /bookings every minute, get a call as soon as a booking arrives, changes or is cancelled.
GET /webhooks | List |
POST /webhooks | Create: {"url":"https://…","events":[…]} (every event if events is missing) |
GET /webhooks/{id} | Read |
PATCH /webhooks/{id} | url, events, enabled |
DELETE /webhooks/{id} | Delete |
POST /webhooks/{id}/test | Sends a ping event and says whether it arrived |
GET /webhooks/{id}/deliveries | The last 50 deliveries (status, HTTP code, error) |
Events: booking.created, booking.updated, booking.cancelled (data = the booking, as GET /bookings/{id}), channel.updated (data = the platform connection, with listing_id), message.received (data = the guest message), review.received (data = the review), request.received and request.updated (data = the Airbnb request, see "Airbnb requests").
{ "id": "evt_…", "object": "event", "type": "booking.created", "mode": "live",
"created_at": "2026-11-01T09:12:03Z", "data": { "id": "…", "object": "booking", … } }- Public
httpsURL. 5 webhooks at most per mode (test and live are separate). - The
whsec_…secret is returned once, at creation. - Check the signature: header
Nestiz-Signature: t=1730452323,v1=…, wherev1is the hex HMAC-SHA256 of<t>.<raw body>with your secret. Reject atolder than 5 minutes. TheNestiz-Event-IdandNestiz-Event-Typeheaders repeatidandtype. - Answer
2xxwithin 10 s. Otherwise, retries after 1 min, 5 min, 30 min, 2 h, 6 h and 12 h. After 100 failures in a row, the webhook is disabled (PATCH {"enabled":true}to restart it: pending events are then sent). - The same event can arrive twice: deduplicate by
id. - Webhooks complement the
GET /bookingscursor, they do not replace it: read the list from time to time to miss nothing.
Platforms
No Nestiz screen: your customer stays in your software (for Airbnb, they only go through Airbnb's official page to give their consent).
channel | How to connect |
|---|---|
airbnb | owner's consent on Airbnb's page |
vrbo | Vrbo account credentials, then a code received by SMS |
google | listing content and Stripe connected, then a single call (see below) |
booking, expedia, agoda, trip, hostelworld and all the others | the property's id on the platform (and, for some, a key or a password) |
GET /platforms lists every platform you can connect, the main ones first:
{ "object": "platform", "channel": "hotelspecials", "name": "HotelSpecials",
"category": "booking_site", "main": false, "connection": "property_id",
"credentials": [ { "key": "username", "secret": false }, { "key": "password", "secret": true } ] }connection:oauth(Airbnb),account(Vrbo),listing_content(Google) orproperty_id(the others).category:booking_site,outdoor(campgrounds),travel_agencies,booking_engine.credentials: fields to ask your customer for, on top of the property id (empty for most).secret: true= mask it when typed.
Three steps, for all of them: POST /listings/{id}/channels (start), GET …/channels/{channel}/options (listings or rooms offered), then POST …/channels/{channel}/activate with {"option_id":"…"}. Final status: connected.
Set the listing's prices before connecting. The platform receives your calendar as soon as it is connected. Without prices, the call returns 409 no_rates. A calendar sent just before goes out first: activate waits up to 20 s for it to arrive, and returns 409 calendar_syncing (retry a minute later) only if it is still being sent after that.
Google Vacation Rentals
Google has no extranet: what guests see comes from the listing content you send. The click leads to a secure booking page that takes the guest's card; the booking is charged at once on your customer's Stripe, and cancelled if the card is declined (nights reopened, booking.cancelled webhook, declined charge visible in GET /bookings/{id}/charges).
PUT /listings/{id}/content: the listing content (see below).missing_for_googlesays what is missing.POST /listings/{id}/payments/connect: your customer connects their Stripe account.POST /listings/{id}/channelswith{"channel":"google"}: that is all (no options noractivate). Google picks up new listings every week: allow about 4 weeks before the first appearance.
Rentals only for now (not the room types of a property).
Listing content (GET/PUT /listings/{id}/content, missing fields unchanged, null clears):
| Field | |
|---|---|
description | 5,000 characters at most |
address, zip, city_name | address (placed on the map automatically in France) |
latitude, longitude | optional, together |
phone | international number (+33612345678) |
email, website | https website |
photos | 30 at most, https URL or {"url","caption"}, first one as cover; 8 at least for Google |
facilities | names from GET /facilities (an unknown name is ignored and returned in unknown_facilities) |
bedrooms, beds, bathrooms | |
checkin_time, checkout_time | HH:MM (16:00 and 11:00 by default) |
pets_allowed | true / false |
cleaning_fee | cleaning charged separately (Google: fee per booking) |
cancellation | {"free_until_days":7,"penalty_nights":1} (default) |
Google needs at least address, phone, website, description and 8 photos.
Airbnb
POST /listings/{id}/channelswith{"channel":"airbnb","return_url":"https://your-software.com/back"}. The response containsauthorization_url.- Open this link for your customer. They log in to Airbnb and grant access.
- They come back to your
return_url, with?listing_id=…&channel=airbnb&status=authorized(orstatus=failed&error=…).error:authorization_refused(access not granted on Airbnb),account_in_use(this Airbnb account is already connected, by another software or by an account in the Nestiz app: we are notified and contact you to release it),channel_manager_error(retry). The detail is in the channel'serror(GET …/channels/airbnb). GET …/channels/airbnb/optionsgives their Airbnb listings, thenactivate. Several listings of one property (room types of aproperty_id): one authorization is enough, in test mode too. The other pending listings of the property becomeauthorizedtoo, and a listing connected later answersstatus: "authorized"right away: itsauthorization_urlsends the host straight back to yourreturn_url, without Airbnb. If the property's connection was cut on Airbnb's side, the host goes through Airbnb again. Another Airbnb account for this room type: add"new_account": true. Listing cut off on Airbnb's side (errorfield of the platform, status stillconnected):activatelinks it again.- Then set the listing on Airbnb through the API: see "Airbnb listing settings".
Importing all of a host's Airbnb listings. One authorization covers the whole Airbnb account:
- Connect a first listing (steps 1 to 3 above).
GET …/channels/airbnb/optionslists every listing of the account;listing_idgives the listing of yours already connected to it (null= not yet).GET …/channels/airbnb/options/{option_id}gives the full content of each one, before connecting it (same fields asGET …/channels/airbnb/content: photos, title, description, rooms, amenities, address), to create the listing in your software and in the API (POST /listings).- For each new listing:
POST /listings/{id}/channelswith{"channel":"airbnb","same_account_as":"<id of the first listing>"}. It isauthorizedat once, without Airbnb (return_urloptional). Send its calendar, thenactivatewith itsoption_id: its bookings come in at once.
Vrbo
Vrbo has no consent page: your customer gives you the e-mail and password of their Vrbo account, then the code they receive by SMS. Nestiz keeps neither the password nor the code: they are passed to the connector, then erased (30 minutes later at most, if the verification is abandoned).
POST /listings/{id}/channelswith{"channel":"vrbo","username":"…","password":"…"}. Statusverifying, andverification.stepsays what to ask for:choose_phone:verification.phoneslists the account's phones; answer{"phone_index":0};add_phone: the account has no phone; answer{"country_code":"…","phone":"612345678"}(country_codefromverification.country_codes);enter_code: answer{"code":"123456"}.
- Each answer:
POST /listings/{id}/channels/vrbo/verification. Once the code is accepted, statusauthorized. GET …/channels/vrbo/optionsgives their Vrbo listings, thenactivate.
- Several listings on the same Vrbo account: repeat step 1 for each with the same credentials; the account already connected is reused, with no new code.
- Vrbo does not send the bookings made before the connection: close those nights in the calendar (
closed) before activating. - Vrbo blocks repeated attempts:
409 platform_rate_limited(withdetails.retry_after). Do not retry before that time, each attempt extends the block. - Sandbox: any credentials, then
phone_index0 and the code123456.
Booking.com, Expedia, Agoda, Trip.com, Hostelworld and the others
- In the platform extranet, your customer accepts Channex.io as their connectivity provider (Trip.com: the Trip.com team enables it on request). The approval can take a few minutes.
POST /listings/{id}/channelswith{"channel":"booking","hotel_id":"1234567"}(the others likewise, with the id given by the platform). If the platform asks for credentials (credentialsinGET /platforms), add them:{"channel":"hotelspecials","hotel_id":"HS-9","credentials":{"username":"…","password":"…"}}. They are passed to the connector and never returned; Nestiz keeps them encrypted for 30 minutes at most, the time to activate (after that, repeat step 2). If the platform has not granted us access yet, or refuses the credentials:409 not_authorized_yet.GET …/channels/{channel}/optionsgives the property's rooms and rates. Booking.com with occupancy-based pricing (OBP): mapped like the others (see "Per-person pricing").POST …/channels/{channel}/activatewith{"option_id":"…"}.
Property: each room type is connected with the same hotel_id and picks its room on the platform. Nestiz keeps a single connection per property and per platform. Two room types cannot be mapped to the same room (409), and a property id already connected elsewhere is refused (409).
Afterwards
GET /listings/{id}/channelslists the platforms and their status:authorizing,verifying(Vrbo),authorized,connected,failedordisconnected.GET /listings/{id}/channels/{channel}reads a single platform.- Airbnb:
accountidentifies the host's Airbnb account (nulluntil it is authorized). Two of your listings with the sameaccountare on the same Airbnb account, under one authorization; a host with several Airbnb accounts has oneaccountper authorization. Vrbo:accountis the account's e-mail. DELETE /listings/{id}/channels/{channel}disconnects the platform for this listing (the property's other room types stay connected). Removing a listing also disconnects its platforms.- Bookings already made on the platform arrive in
GET /bookingsas soon as it is connected (imported_bookingsin theactivateresponse), except Vrbo. - In test mode the whole flow is simulated: no real listing is touched.
Sandbox (test key only)
The sandbox plays the platforms' side, like the "Booking CRS" of a channel manager: you create, modify and cancel platform bookings by API, and they arrive in your software exactly as real ones would (same JSON, same webhooks, same GET /bookings cursor).
POST /sandbox/bookings | A new booking from a platform |
PATCH /sandbox/bookings/{id} | A modification sent by the platform (dates, guests, amount, guest details) |
POST /sandbox/bookings/{id}/cancel | A cancellation by the platform |
POST /sandbox/requests | An Airbnb request: reservation request, inquiry or alteration (see "Airbnb requests") |
POST /sandbox/messages | A guest message: {"booking_id":"…","text":"…"} |
POST /sandbox/reviews | A guest review: {"booking_id":"…","score":9,"text":"…"} |
curl -X POST https://nestiz.fr/api/v1/sandbox/bookings \
-H "Authorization: Bearer nz_test_…" -H "Content-Type: application/json" \
-d '{"listing_id":"…","platform":"booking","arrival":"2026-11-21","departure":"2026-11-24",
"guest":{"name":"Jane Martin","email":"[email protected]","phone":"+447700900000","language":"en","country":"GB","arrival_time":"18:00"},
"adults":2,"children":1,"infants":0,"notes":"Late arrival"}'
curl -X PATCH https://nestiz.fr/api/v1/sandbox/bookings/{id} \
-H "Authorization: Bearer nz_test_…" -H "Content-Type: application/json" \
-d '{"arrival":"2026-11-22","departure":"2026-11-26","adults":3}'| Field | Default | |
|---|---|---|
listing_id | required (creation only) | |
platform | any channel of GET /platforms (creation only) | airbnb |
arrival, departure | required at creation; sent together | |
guest | name, email, phone, language, country, city, address, zip, arrival_time (or guest_name, guest_email, guest_phone flat) | Test Guest |
adults, children, infants | 2, 0, 0 | |
rooms | units of the room type (property) | 1 |
amount | total | the sum of the calendar prices |
ota_reservation_code | the platform's number | one in the platform's format |
notes | the guest's request |
- Refused (
409 not_available), as on a platform, if a night is not for sale (no price, closed, already booked), if the minimum or maximum stay is not met, or if the arrival or departure falls on a closed day. You can therefore test your prices, closures and stay rules. The reason is indetails.reason:no_rate(no price for that night),closed,booked,rooms,min_stay,max_stay,min_stay_through,closed_to_arrival,closed_to_departure; the night indetails.date. - A modification is checked the same way (its own nights count as free). It keeps the
id, increasesrevisionand sendsbooking.updated. New dates withoutamount: the price is computed again from the calendar. - The booking looks like a real one: price per night in
price.nights, an indicative commission inpayment.platform_commission, and for the platforms where the guest pays at the property, a masked test card (Visa4242). Airbnb and Vrbo:payment.collected_by=platform, no card. - Connections are simulated too: any
hotel_id, Airbnb's consent answered at once, Vrbo with any credentials, thenphone_index0 and the code123456. - Charging a card (
POST /bookings/{id}/charges) always succeeds.
Best practices
Taken from what channel managers ask of the software they certify; Nestiz does not make it a condition, but it is what keeps a calendar right.
- Send changes, not the whole calendar. One call when the user saves, with only the nights that changed (one night, or a few dates: one call with several ranges, not one call per night). The full calendar once when a listing is connected, then at most once a day, at night, to correct any drift; never a full send on a timer every few minutes.
- Group a burst of edits. A save that touches many nights is one call. If your software emits one event per night, wait a few seconds and send them together.
- Send only the fields that changed. A range with
ratealone does not touch the minimum stay or the closures. - Queue your calls and retry. On
429, waitRetry-Afterseconds; on502or503, retry the same call a little later (it is safe: nothing is applied twice; for aPOST, with the sameIdempotency-Key). Never drop an update silently. - Webhooks first, cursor as a backstop. Treat each
booking.*event at once, then every 15 minutes or so readGET /bookingsfrom your lastnext_cursor: a webhook missed while your server was down is caught there. No full re-read of every booking "just in case". - Never refuse a platform booking. It is already confirmed for the guest. If your software sees no room left, store it anyway and flag the overbooking to your user.
- Same booking, same
id. A modification updates the stay you have (dates, guests, price); a cancellation releases its nights. Deduplicate byid, apply only a higherrevision. - Keep
ota_reservation_code: it is what the host gives the platform's support on the phone. - Your ids in
external_id: a creation retried after a timeout returns409with the existingid, never a duplicate. - Set the prices before connecting: a night without a price is never for sale.
Integration checklist
GET /checklist says where your integration stands, step by step. Each step ticks itself from what you really did with the key (sandbox first); nothing to fill in, no screenshare.
{ "object": "checklist", "mode": "test", "ready_for_live": false, "done": 9, "total": 14,
"steps": [ { "id": "full_sync", "title": "Send a full calendar (at least 300 days ahead) in one call",
"done": true, "how": "PUT /listings/{id}/calendar with ranges covering the year…" }, … ] }The scenarios, each triggered from your software's screens (a price changed by a user, a booking that arrives), not from a test script: that is the only way to know your software really does it.
| # | Scenario | What we expect |
|---|---|---|
| 1 | Full sync: the listing goes live | 1 calendar call covering the year, with prices and rules that vary like a real calendar |
| 2 | One price changed on one night | 1 call, 1 range, rate only |
| 3 | Prices changed on several dates of several listings | 1 call per listing, every date inside |
| 4 | A period priced at once (e.g. 1 to 20 November) | 1 range from/to, not 20 |
| 5 | Minimum stay changed | min_stay alone, the price is not resent |
| 6 | Nights closed (stop sell) and reopened | closed: true, then false |
| 7 | Several restrictions at once (no arrival, no departure, maximum stay) | 1 call |
| 8 | Half a year updated | 1 call, a few ranges |
| 9 | A booking taken in your software | POST /bookings: the nights close everywhere |
| 10 | It is moved, then cancelled | PATCH /bookings/{id}, then POST …/cancel |
| 11 | A platform booking: new, modified, cancelled | POST, PATCH, POST …/cancel on /sandbox/bookings: one stay in your software, moved, then released |
| 12 | Rate limits | a queue, Retry-After respected, no 429 |
| 13 | Update logic | changes only; full sync at most once a day |
| 14 | Messages and reviews (optional) | a guest message received and answered |
Things your software may not do (several rate plans, closed to departure, per-person prices…): tell us when you ask for live mode, it is not a blocker.
Errors
Always the same format:
{ "error": { "code": "invalid_request", "message": "ranges[1].rate must be between 3 and 2000 EUR. Nothing was changed.", "details": { "index": 1, "field": "rate" } } }| Status | code | |
|---|---|---|
| 401 | unauthorized | missing, unknown or revoked key |
| 403 | live_not_enabled, account_suspended, limit_reached, test_mode_only | |
| 404 | not_found | unknown resource, or one belonging to another account; for an unknown endpoint, details.did_you_mean gives the right route when we can guess it |
| 402 | payment_required | unpaid subscription: add a payment method (GET /billing) |
| 409 | conflict, not_available, no_rates, calendar_syncing, not_authorized_yet, already_connected, listing_not_ready, property_not_ready, platform_refused, platform_rate_limited, reconnect_required, request_in_progress | see the message |
| 422 | not_supported, credentials_refused, code_refused | option or platform not supported; Vrbo credentials or code refused |
| 413 | payload_too_large | body above 512 KB (8 MB for a message with an attachment) |
| 422 | invalid_request | invalid data: nothing was changed; an unknown field or query parameter returns the allowed ones in details.allowed |
| 422 | idempotency_key_reused | Idempotency-Key already used for another call |
| 429 | rate_limited | too many calls: wait Retry-After seconds |
| 502 | channel_manager_error | the platform did not answer: retry the same call |
| 503 | unavailable | service temporarily unavailable |
Every error also carries request_id, the id of the call in our log (the same as the Nestiz-Request-Id header): quote it when you write to us and we find the exact call.
Query parameters. An unknown parameter is refused (422, with the list of allowed ones): a mistyped filter (update_since) never returns the whole list instead.
Retrying a call
A call whose answer was lost (network cut, timeout) can be retried safely:
GET,PUT,PATCH,DELETE: send the same call again, the result is the same.POST(direct booking, message to a guest, answer to a request…): send theIdempotency-Keyheader with a value unique to the action (a UUID), and the same one when you retry. Nestiz then returns the first answer (headerIdempotent-Replayed: true) instead of acting a second time.
curl -X POST https://nestiz.fr/api/v1/bookings \
-H "Authorization: Bearer nz_test_…" -H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2a4e-9b1d-4c58-8a3e-0d5b7c1e2f90" \
-d '{"listing_id":"…","arrival":"2026-12-04","departure":"2026-12-07","guest_name":"Jane Martin"}'- Only a
2xxanswer is kept, for 24 hours. A refused call (409,422,502…) did nothing: the same key runs it again. - Same key with another body or another endpoint:
422 idempotency_key_reused. - First call still running:
409 request_in_progress, retry a few seconds later with the same key. - Charging a card keeps its own
idempotency_keyfield (required).
Limits
- 120 calls per minute per key. A single calendar call is enough for a whole season. Every response tells you where you stand:
RateLimit-Limit,RateLimit-Remaining(calls left in the minute) andRateLimit-Reset(seconds until a call frees up). - Nestiz batches and spreads what is sent to the platforms. Syncing is instant, with a delay of 0 to 30 seconds depending on the platform.
Pricing
Per month, excluding VAT, no subscription and no minimum: you pay as your customers connect. French VAT is added only for companies based in the EU. Prices in US dollars; the charge is made in euros, at the European Central Bank rate of the day before (the 27th), shown on the invoice.
| Rental | by brackets, messaging included: $4 up to 10 listings, $3.50 from 11 to 30, $2 from 31 to 50, $1.50 from 51 to 80, $1 from 81 to 1,999, $0.95 from 2,000 to 3,999, $0.90 from 4,000 to 6,999, $0.80 above |
| Property (hotel, guest house, hostel, campground…) | by brackets, whatever its number of rooms: $17 up to 5 properties, $14 from 6 to 10, $10 from 11 to 20, $7 from 21 to 499, $6 from 500 to 999, $5 from 1,000 to 1,999, $4 above |
| Property messaging and reviews | + $7 per property that takes it (property messaging) |
| Property with more than 20 room types | + $1 per room type over 20, $7 at most per property |
Rentals and properties are priced by brackets: only the units above a threshold move to the next price, so the bill never drops when a listing is added.
Cap: the bill never exceeds $115 + $1 per rental + $7 per property (property messaging and room types over 20 on top; above 1,999 rentals or 499 properties, the per-unit prices of the brackets above). When it applies, the bill carries a "Cap applied" line (cap in lines, negative amount).
Examples: 1 listing = $4; 10 listings = $40; 100 listings = $215 (10 × $4 + 20 × $3.50 + 20 × $2 + 30 × $1.50 + 20 × $1); 500 listings = $615; 1 property with 8 units = $17 ($24 with messaging); 10 properties = $155; 20 listings and 10 properties = $205 (cap; $230 without it).
Vendor subscribed before 10 November 2026: its euro price list and its currency do not change; the list above applies instead as soon as it saves at least 5% (discount line).
- Test mode free, with no time limit.
- 10 rate plans at most per listing. A duplicate connection to the same platform is billed €1 per month.
- Only what is connected to a platform is billed: a listing created but not connected yet costs nothing.
- A property is one address. Its room types must be at the same place: same property on Booking.com, Agoda, Expedia…, or same Airbnb listing address (same street and street number; only the apartment number may differ). A room type elsewhere, or connected only to Vrbo or Google (no address to compare), is billed as a rental:
billed_as: "rental"on the listing (billed_as_rental_reason), and listed inrooms_billed_as_rentalsofGET /billing. Nothing is refused: the connection works. One property per address. A real annex (same place, other address): write to us and we approve it. - No trial in live mode: test mode is the trial. Live mode opens as soon as a card is on file (
card_urllink, Apple Pay and Google Pay included; no SEPA debit), with nothing charged that day; until then, the live key returns402 payment_required. - Charged on the 28th of each month, for the next month, in advance: the listings and properties connected that day, full price, no proration. A listing connected after the 28th counts from the next 28th.
- Terms of use: nestiz.fr/editeurs/conditions
GET /billing: subscription status (payment_method_required,active,past_due,canceled,suspended…), next charge date (next_charge_at), currency charged (currency, the euro), amount charged (monthly_amount), subtotal excluding VAT (monthly_amount_excl_vat), VAT rate (vat_rate), price currency (price_currency, the US dollar), subtotal excluding VAT in that currency (price_amount_excl_vat), rate applied (exchange_rate: dollars per euro,exchange_rate_date;nullwhen prices and charge are in the same currency), lines excluding VAT in the price currency (lines:listingandhotel, one line per bracket withfromandto,messaging,room_types,cap,discount;hotel_small,hotel_medium,hotel_largeon bills before 9 November 2026), andcard_url, the link to add or change the card.- Card issued in India: the bank must authorize the monthly payment once (RBI rules), up to the monthly amount at that time. This is done on the payment page of
card_url. Not authorized yet, declined or cancelled:payment_method_issuesays so (missingor the bank's reason;nullotherwise) and an email gives the link to authorize it, before the next charge. The prorated amount charged to register it is credited back on the next invoice. Monthly amount now above the authorized one: the bank sends a notice to approve that payment (we send an email reminder), nothing to redo on our side. - Failed payment: nothing changes until the last day of the month, while Stripe retries. Still unpaid that day: the platforms are disconnected and the live key returns
402 payment_required(exceptGET /billing). An updated payment method restarts the subscription; reconnect the platforms.
Support
By e-mail, or through the Nestiz assistant.