nestiz
Free trial

Documentation

Nestiz API documentation

Everything to connect your software to Airbnb, Booking.com and about a hundred platforms, from the first call to live mode.

Contents

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/… (or https://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-Key header on any POST (see "Retrying a call"). Every response carries Nestiz-Request-Id, the id of the call: quote it when you write to us.
  • No certification and no call with us: GET /checklist ticks 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 $body

Getting 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.

  1. Check your key: GET /ping returns your account name and "mode":"test".
  2. Create a listing, with your own id in external_id: POST /listings (a rental), or POST /properties then one POST /listings per room type (hotel, hostel, campground). Keep the returned id.
  3. Send the full calendar once: one PUT /listings/{id}/calendar with the coming year (prices, closures, stay rules), as your software has it. This is what happens when a customer goes live.
  4. 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").
  5. Connect a platform: POST /listings/{id}/channels, GET …/options, POST …/activate. With the test key the platform's side is simulated (any hotel_id, Airbnb's consent page answered at once).
  6. Receive bookings: create a webhook (POST /webhooks) and also read GET /bookings with 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.
  7. Direct bookings: when your software takes a booking itself (phone, website), POST /bookings, then PATCH and …/cancel: the nights close and reopen on every platform.
  8. Check GET /checklist. When every step is ticked, write to us: we open live mode, you get a nz_live_… key, and the same code runs against the real platforms.

Listings

GET /listingsList (paginated); filters external_id (find a listing by your own id), property_id (the room types of a property)
POST /listingsCreate
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}'
FieldDefault
namerequired, 120 characters at most
external_idyour own id, unique per accountnone
property_idproperty (hotel, campground…) this room type belongs to (see below)none
citynone
countryISO 3166-1 alpha-2 codeFR
currencyISO 4217 code, one of the 147 of GET /currencies (EUR, USD, MXN, BRL, ARS…); cannot be changed laterEUR
timezoneIANA time zoneEurope/Paris
unitsroom 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 property1
max_guests1 to 302
guests_included, extra_guest_fee, fewer_guest_discountper-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 /propertiesList (paginated); filter external_id
POST /propertiesCreate
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 fieldDefault
namerequired
typehotel, guest_house, hostel, resort; cannot be changed laterhotel
external_id, city, country, currency, timezoneas for a listing; currency cannot be changed
messagingguest messaging and reviews for the property, paid option (see Pricing); can be changedfalse
  • 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: units 12, booked 3, available 9.
  • A booking takes one bed per guest (adults and children; infants need no bed): rooms is 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).
  • kind cannot be changed later; units and beds_per_room can.

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 toone night, or a range of nights
ratenightly rate (bounds per currency, see GET /currencies; 3 to 2,000 in euros)
closedtrue: night closed for sale
min_stayminimum nights for an arrival that day (1 to 90); null removes it
max_staymaximum nights for an arrival that day (1 to 730); null removes it
min_stay_throughminimum nights of any stay that includes this night (1 to 90); null removes it
closed_to_arrival, closed_to_departureno 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 422 error 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_ignored in 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_plansThe 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) or amount (−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 …/calendar then also returns rate_plans: the price of each plan, night by night.
  • A booking states its rate_plan_id (standard or 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_mappingsMapped 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_planSame

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_includedguests included in the calendar price (1 to max_guests); null: same price for everyone
extra_guest_feeper additional guest, per night, in the listing currency
fewer_guest_discountper 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: confirmed or cancelled;
  • 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": "…"
}
  • status is confirmed or cancelled. Each change increases revision by 1.
  • A modification keeps the same id (and the same ota_reservation_code): update the stay you already have, never create a second one. Apply a version only if its revision is higher than the one you hold (the same event can arrive twice, or out of order).
  • platform: the platform channel (airbnb, booking, hostelworld…, see GET /platforms), direct, or other if 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) or null if 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…), null when 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 /bookingsCreate: 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}/cancelCancel: the nights are released
  • platform is direct. Your reference comes back in ota_reservation_code.
  • The guest can also be sent in a guest object, 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, with details.date).
  • Only direct bookings can be changed here (409 not_direct otherwise): 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}/paymentsStripe 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}/chargesCharges of the booking
  • idempotency_key required (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 /conversationsConversations, 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}/messagesReply: {"text":"…"}, or an attachment (below)
POST /conversations/{id}/closeClose the conversation (it reopens with the next guest message)
GET /bookings/{id}/messagesMessages of a booking's conversation (empty list if none yet)
POST /bookings/{id}/messagesWrite 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 same id as in GET /conversations/{id}.
  • Pages: GET /conversations returns has_more and next_cursor. Read every page while has_more is true, passing next_cursor as cursor; on the last page, next_cursor is null.
  • To miss nothing: the message.received webhook, and updated_since as a backstop. It compares the conversation's updated_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.
  • sender is guest or property (you, or the host from the platform).
  • A guest message triggers the message.received webhook (data = the message, with listing_id, property_id, booking_id, platform, guest_name).
  • Property with several room types: every conversation and every webhook carries listing_id and property_id (the property, null for a rental). Conversation of a booking: listing_id is the booked room type. A guest who writes before booking (booking_id null): the platform does not always say which room, so listing_id is then the property's first room type; rely on property_id. The conversation moves to the right room type as soon as the booking (or the Airbnb request) arrives, and its updated_at changes.
  • Never send twice: add Idempotency-Key when you send a message (see "Retrying a call").
  • Direct booking: no platform, so no message (409 not_supported).
  • Sandbox: POST /sandbox/messages with {"booking_id":"…","text":"…"} simulates a guest message.

Guest reviews

GET /reviewsReviews, most recent first; filters listing_id, property_id, updated_since; limit, cursor (pages as for conversations)
GET /reviews/{id}One review
POST /reviews/{id}/replyPublic reply: {"text":"…"} (once)
POST /reviews/{id}/guest_reviewAirbnb: 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_score on the platform's scale (10 for Booking.com, 5 for Airbnb).
  • An Airbnb review stays hidden until 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.received webhook.
  • Sandbox: POST /sandbox/reviews with {"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_settingsBooking lead time, booking window, preparation time, check-in and check-out days, maximum stay
GET/PATCH …/pricing_settingsFees, length-of-stay, early-bird and last-minute discounts, taxes collected by Airbnb
GET/PATCH …/booking_settingsInstant booking, check-in and check-out times, cancellation policy, house rules, what guests must know
GET/PATCH …/checkout_tasksTasks asked of departing guests
GET …/promotionsThe listing's promotions and their status
POST …/promotions/{type}/enable, …/disableNEW_LISTING_PROMOTION, HIGH_RATED_GUEST_DISCOUNT, MOBILE_ONLY_DISCOUNT
GET …/opportunitiesImprovements and discount programs Airbnb suggests for the listing
POST …/opportunities/{id}/apply{"input_fields":{…}} (the fields the opportunity lists)
POST …/publish, POST …/unpublishPublish or unpublish the Airbnb listing (it stays connected); unpublish takes {"reason":"…","details":"…"}
GET …/unpublish_reasonsAirbnb'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 …/contentThe listing content, read-only: title, description, photos, rooms and beds, amenities, address, capacity

Availability (availability_settings):

Field
booking_lead_timeminimum notice in hours: 0 to 24, 48, 72 or 168
max_days_noticebooking window in days: -1 (no limit), 0, 30, 60, … 365
allow_rtb_beyond_max_days_noticerequests to book allowed beyond the window
default_max_nightsmaximum stay, 1 to 1,125 (0 = Airbnb default)
allow_rtb_above_max_nightsrequests to book allowed above it (28 nights or more)
day_of_week_check_in, day_of_week_check_out7 booleans, Monday first
turnover_dayspreparation 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_type among PASS_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_type flat (whole amount in the listing currency) or percent (0 to 100); charge_type PER_GROUP (default), PER_PERSON, PER_PET (pet fee only); charge_period PER_BOOKING (default) or PER_NIGHT; offline = paid on site; fee_unit_type for 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_one a 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) and pass_through_taxes_collection_type.
  • GET also gives currency, guests_included and price_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:

kindThe guest…Answers
reservation_requestasks to book (no instant booking)accept, decline
inquiryasks about dates without bookingpreapprove, special_offer
alteration_requestasks to change a confirmed bookingaccept, decline, withdraw
GET /requestsRequests, 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}/acceptAccept
POST /requests/{id}/declineDecline; 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}/preapproveInquiry: invite the guest to book at the current price; {"block_instant_booking":true} holds the dates meanwhile
POST /requests/{id}/special_offerInquiry: {"total_price":450}, whole amount for the whole stay, fees included, without taxes or deposit
POST /requests/{id}/withdrawWithdraw 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, expired or closed (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) and request.updated (answered, or settled on Airbnb).
  • Sandbox: POST /sandbox/requests with {"kind":"reservation_request","listing_id":"…","arrival":"…","departure":"…","adults":2} (alteration_request: booking_id and 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 /webhooksList
POST /webhooksCreate: {"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}/testSends a ping event and says whether it arrived
GET /webhooks/{id}/deliveriesThe 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 https URL. 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=…, where v1 is the hex HMAC-SHA256 of <t>.<raw body> with your secret. Reject a t older than 5 minutes. The Nestiz-Event-Id and Nestiz-Event-Type headers repeat id and type.
  • Answer 2xx within 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 /bookings cursor, 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).

channelHow to connect
airbnbowner's consent on Airbnb's page
vrboVrbo account credentials, then a code received by SMS
googlelisting content and Stripe connected, then a single call (see below)
booking, expedia, agoda, trip, hostelworld and all the othersthe 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) or property_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).

  1. PUT /listings/{id}/content: the listing content (see below). missing_for_google says what is missing.
  2. POST /listings/{id}/payments/connect: your customer connects their Stripe account.
  3. POST /listings/{id}/channels with {"channel":"google"}: that is all (no options nor activate). 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
description5,000 characters at most
address, zip, city_nameaddress (placed on the map automatically in France)
latitude, longitudeoptional, together
phoneinternational number (+33612345678)
email, websitehttps website
photos30 at most, https URL or {"url","caption"}, first one as cover; 8 at least for Google
facilitiesnames from GET /facilities (an unknown name is ignored and returned in unknown_facilities)
bedrooms, beds, bathrooms
checkin_time, checkout_timeHH:MM (16:00 and 11:00 by default)
pets_allowedtrue / false
cleaning_feecleaning 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

  1. POST /listings/{id}/channels with {"channel":"airbnb","return_url":"https://your-software.com/back"}. The response contains authorization_url.
  2. Open this link for your customer. They log in to Airbnb and grant access.
  3. They come back to your return_url, with ?listing_id=…&channel=airbnb&status=authorized (or status=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's error (GET …/channels/airbnb).
  4. GET …/channels/airbnb/options gives their Airbnb listings, then activate. Several listings of one property (room types of a property_id): one authorization is enough, in test mode too. The other pending listings of the property become authorized too, and a listing connected later answers status: "authorized" right away: its authorization_url sends the host straight back to your return_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 (error field of the platform, status still connected): activate links it again.
  5. 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:

  1. Connect a first listing (steps 1 to 3 above).
  2. GET …/channels/airbnb/options lists every listing of the account; listing_id gives the listing of yours already connected to it (null = not yet).
  3. GET …/channels/airbnb/options/{option_id} gives the full content of each one, before connecting it (same fields as GET …/channels/airbnb/content: photos, title, description, rooms, amenities, address), to create the listing in your software and in the API (POST /listings).
  4. For each new listing: POST /listings/{id}/channels with {"channel":"airbnb","same_account_as":"<id of the first listing>"}. It is authorized at once, without Airbnb (return_url optional). Send its calendar, then activate with its option_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).

  1. POST /listings/{id}/channels with {"channel":"vrbo","username":"…","password":"…"}. Status verifying, and verification.step says what to ask for:
    • choose_phone: verification.phones lists the account's phones; answer {"phone_index":0};
    • add_phone: the account has no phone; answer {"country_code":"…","phone":"612345678"} (country_code from verification.country_codes);
    • enter_code: answer {"code":"123456"}.
  2. Each answer: POST /listings/{id}/channels/vrbo/verification. Once the code is accepted, status authorized.
  3. GET …/channels/vrbo/options gives their Vrbo listings, then activate.
  • 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 (with details.retry_after). Do not retry before that time, each attempt extends the block.
  • Sandbox: any credentials, then phone_index 0 and the code 123456.

Booking.com, Expedia, Agoda, Trip.com, Hostelworld and the others

  1. 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.
  2. POST /listings/{id}/channels with {"channel":"booking","hotel_id":"1234567"} (the others likewise, with the id given by the platform). If the platform asks for credentials (credentials in GET /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.
  3. GET …/channels/{channel}/options gives the property's rooms and rates. Booking.com with occupancy-based pricing (OBP): mapped like the others (see "Per-person pricing").
  4. POST …/channels/{channel}/activate with {"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}/channels lists the platforms and their status: authorizing, verifying (Vrbo), authorized, connected, failed or disconnected.
  • GET /listings/{id}/channels/{channel} reads a single platform.
  • Airbnb: account identifies the host's Airbnb account (null until it is authorized). Two of your listings with the same account are on the same Airbnb account, under one authorization; a host with several Airbnb accounts has one account per authorization. Vrbo: account is 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 /bookings as soon as it is connected (imported_bookings in the activate response), 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/bookingsA new booking from a platform
PATCH /sandbox/bookings/{id}A modification sent by the platform (dates, guests, amount, guest details)
POST /sandbox/bookings/{id}/cancelA cancellation by the platform
POST /sandbox/requestsAn Airbnb request: reservation request, inquiry or alteration (see "Airbnb requests")
POST /sandbox/messagesA guest message: {"booking_id":"…","text":"…"}
POST /sandbox/reviewsA 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}'
FieldDefault
listing_idrequired (creation only)
platformany channel of GET /platforms (creation only)airbnb
arrival, departurerequired at creation; sent together
guestname, email, phone, language, country, city, address, zip, arrival_time (or guest_name, guest_email, guest_phone flat)Test Guest
adults, children, infants2, 0, 0
roomsunits of the room type (property)1
amounttotalthe sum of the calendar prices
ota_reservation_codethe platform's numberone in the platform's format
notesthe 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 in details.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 in details.date.
  • A modification is checked the same way (its own nights count as free). It keeps the id, increases revision and sends booking.updated. New dates without amount: the price is computed again from the calendar.
  • The booking looks like a real one: price per night in price.nights, an indicative commission in payment.platform_commission, and for the platforms where the guest pays at the property, a masked test card (Visa 4242). 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, then phone_index 0 and the code 123456.
  • 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 rate alone does not touch the minimum stay or the closures.
  • Queue your calls and retry. On 429, wait Retry-After seconds; on 502 or 503, retry the same call a little later (it is safe: nothing is applied twice; for a POST, with the same Idempotency-Key). Never drop an update silently.
  • Webhooks first, cursor as a backstop. Treat each booking.* event at once, then every 15 minutes or so read GET /bookings from your last next_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 by id, apply only a higher revision.
  • 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 returns 409 with the existing id, 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.

#ScenarioWhat we expect
1Full sync: the listing goes live1 calendar call covering the year, with prices and rules that vary like a real calendar
2One price changed on one night1 call, 1 range, rate only
3Prices changed on several dates of several listings1 call per listing, every date inside
4A period priced at once (e.g. 1 to 20 November)1 range from/to, not 20
5Minimum stay changedmin_stay alone, the price is not resent
6Nights closed (stop sell) and reopenedclosed: true, then false
7Several restrictions at once (no arrival, no departure, maximum stay)1 call
8Half a year updated1 call, a few ranges
9A booking taken in your softwarePOST /bookings: the nights close everywhere
10It is moved, then cancelledPATCH /bookings/{id}, then POST …/cancel
11A platform booking: new, modified, cancelledPOST, PATCH, POST …/cancel on /sandbox/bookings: one stay in your software, moved, then released
12Rate limitsa queue, Retry-After respected, no 429
13Update logicchanges only; full sync at most once a day
14Messages 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" } } }
Statuscode
401unauthorizedmissing, unknown or revoked key
403live_not_enabled, account_suspended, limit_reached, test_mode_only
404not_foundunknown resource, or one belonging to another account; for an unknown endpoint, details.did_you_mean gives the right route when we can guess it
402payment_requiredunpaid subscription: add a payment method (GET /billing)
409conflict, 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_progresssee the message
422not_supported, credentials_refused, code_refusedoption or platform not supported; Vrbo credentials or code refused
413payload_too_largebody above 512 KB (8 MB for a message with an attachment)
422invalid_requestinvalid data: nothing was changed; an unknown field or query parameter returns the allowed ones in details.allowed
422idempotency_key_reusedIdempotency-Key already used for another call
429rate_limitedtoo many calls: wait Retry-After seconds
502channel_manager_errorthe platform did not answer: retry the same call
503unavailableservice 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 the Idempotency-Key header with a value unique to the action (a UUID), and the same one when you retry. Nestiz then returns the first answer (header Idempotent-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 2xx answer 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_key field (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) and RateLimit-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.

Rentalby 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 in rooms_billed_as_rentals of GET /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_url link, Apple Pay and Google Pay included; no SEPA debit), with nothing charged that day; until then, the live key returns 402 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; null when prices and charge are in the same currency), lines excluding VAT in the price currency (lines: listing and hotel, one line per bracket with from and to, messaging, room_types, cap, discount; hotel_small, hotel_medium, hotel_large on bills before 9 November 2026), and card_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_issue says so (missing or the bank's reason; null otherwise) 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 (except GET /billing). An updated payment method restarts the subscription; reconnect the platforms.

Support

By e-mail, or through the Nestiz assistant.