Creating reservations

One endpoint books a stay wherever the listing's bookings live. A listing managed in a connected PMS is booked in that PMS; any other listing gets a direct booking. Quote it first, check what the listing supports, and change or cancel it through the same API.

Where the booking is made

The listing is…What happens
Managed in a connected PMSThe booking is created in the PMS first, then recorded in Repull from the PMS's own record, so the next sync lands on the same confirmation code and nothing is duplicated. The PMS checks availability and prices the stay. It is never created only in Repull — the PMS would keep selling the dates.
Not managed in a PMSA direct booking in Repull, with the guest, the conversation, the calendar block and the reservation.created webhook. Priced from the listing's own rates. Availability is not checked — call GET /v1/availability/{propertyId} first if that matters.

The PMSs that take bookings this way are Mews, Cloudbeds, Hostaway, Guesty, Beds24, BookingSync, Lodgify, Smoobu, Hospitable, iGMS, OwnerRez and Track. What each one can do differs, and anything a PMS cannot do is refused with a clear error rather than faked — see PMS reservation support.

Channel bookings are not created here

platform takes direct (the default), website or owner. Airbnb, Booking.com and Vrbo bookings belong to the channel: they arrive through sync, and a booking that came from a channel — including one relayed through a PMS — is changed or cancelled on the channel (409 reservation_owned_by_channel). On a PMS listing, owner is refused too: block owner stays in the PMS.

Check what a listing supports

GET /v1/listings/{id} returns capabilities.reservations: which writes the API will perform for that listing. A flag is true only when the PMS supports it, the workspace is still connected to the PMS, and the connection's write policy allows bookings through the API. Read it before you build a booking form, and use notes to explain to your user why something is off. GET /v1/connect/{provider} returns the same object for a whole PMS connection.

GET/v1/listings/{id}
curl "https://api.repull.dev/v1/listings/4118" \
  -H "Authorization: Bearer sk_live_YOUR_KEY"

# "capabilities": {
#   "reservations": {
#     "managedBy": "pms",
#     "provider": "hostaway",
#     "create": true,
#     "modify": true,
#     "cancel": true,
#     "quote": true,
#     "customPrice": true,
#     "notes": "Direct-channel bookings: create (at a set total, or priced by Hostaway's calculator), quote, …",
#     "verifiedAgainst": "vendor_docs"
#   }
# }
managedBystring

`pms` — booked in the connected PMS. `repull` — a direct booking.

providerstringnullable

The PMS, e.g. `hostaway`. Null for direct bookings.

createboolean

`POST /v1/reservations` will book this listing.

modifyboolean

`PATCH /v1/reservations/{id}` will change its bookings.

cancelboolean

`POST /v1/reservations/{id}/cancel` will cancel its bookings.

quoteboolean

`POST /v1/reservations/quote` will price it.

customPriceboolean

`totalPrice` on create is honoured. Otherwise the PMS (or the listing's rates) prices the stay.

notesstring

What the flags do not say: limits, the access the PMS needs, and why something is off.

verifiedAgainststringnullable

`sandbox` (run end to end on the vendor's sandbox) or `vendor_docs` (verified against the vendor's API documentation). Null for direct bookings.

Quote first

POST /v1/reservations/quote asks the PMS for the price and availability of a stay without booking anything. It is the same check a create makes when you send no totalPrice, so available: true with a total is what that create would charge — though the dates can still be taken in between.

POST/v1/reservations/quote
curl -X POST "https://api.repull.dev/v1/reservations/quote" \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "listingId": 4118, "checkIn": "2026-10-01", "checkOut": "2026-10-05", "adults": 2, "children": 1 }'
listingIdintegerRequired

The listing to quote.

checkIndateRequired

Arrival, YYYY-MM-DD.

checkOutdateRequired

Departure, YYYY-MM-DD, after checkIn.

adultsinteger

Adults, 1 or more.

childreninteger

Children, 0 or more.

guestCountinteger

Total guests, when you do not split adults and children.

unitIdstring

Quote one unit (`GET /v1/listings/{id}` → `units[].id`).

Bookable

{
  "listingId": "4118",
  "provider": "hostaway",
  "checkIn": "2026-10-01",
  "checkOut": "2026-10-05",
  "available": true,
  "total": 880,
  "currency": "USD",
  "breakdown": { "accommodation": 720, "cleaningFee": 100, "taxes": 60 },
  "restrictions": []
}

Not bookable as asked

available: false is an answer, not an error. The PMS's reasons are in restrictions, in its own words.

{
  "listingId": "4118",
  "provider": "guesty",
  "checkIn": "2026-10-01",
  "checkOut": "2026-10-02",
  "available": false,
  "total": null,
  "currency": null,
  "breakdown": null,
  "restrictions": ["Guesty: minimum stay is 3 nights"]
}
  • A listing not managed in a PMS answers 422 pms_not_linked. Price it with GET /v1/quotes instead.
  • Mews, Cloudbeds and iGMS have no quote API and answer 422 pms_write_unsupported. You can still book them; on iGMS a totalPrice is required.
  • Hospitable quotes need an active Hospitable Direct plan.

Create the reservation

POST/v1/reservations
curl -X POST "https://api.repull.dev/v1/reservations" \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9f1c2f7e-4a3b-4f2e-9c8d-1b6a0e5d7c31" \
  -d '{
    "listingId": 4118,
    "checkIn": "2026-10-01",
    "checkOut": "2026-10-05",
    "guest": {
      "firstName": "Ada",
      "lastName": "Lovelace",
      "email": "ada@example.com",
      "phone": "+14035551234"
    },
    "adults": 2,
    "children": 1,
    "totalPrice": 880,
    "notes": "Late arrival, around 22:00.",
    "status": "confirmed",
    "sendConfirmationEmail": false
  }'

Request fields

listingIdintegerRequired

The listing to book — see `GET /v1/properties`.

checkIndateRequired

Arrival, YYYY-MM-DD.

checkOutdateRequired

Departure, YYYY-MM-DD, after checkIn.

guestobjectRequired

`firstName` (required), `lastName`, `email`, `phone` (E.164 preferred). Matched against existing guests on email, then phone, plus name, so repeat guests are not duplicated.

adultsinteger

Adults, 1 or more.

childreninteger

Children, 0 or more.

guestCountinteger

Total guests. On a PMS listing without `adults`, used as the adult count.

statusstringDefault: confirmed

`confirmed` or `tentative` — an optional hold, where the PMS has one (iGMS refuses it).

platformstringDefault: direct

`direct`, `website` or `owner`. `owner` is refused on a PMS listing.

totalPricenumber

PMS listings only. The total for the whole stay in the listing's currency. Honoured where `capabilities.reservations.customPrice` is true; omit it and the PMS prices the stay. Required on iGMS.

notesstring

PMS listings only. Booking notes stored in the PMS, up to 5000 characters.

unitIdstring

PMS listings only. Book this unit (`GET /v1/listings/{id}` → `units[].id`). Needed on a multi-room Beds24 property; refused by Hostaway and iGMS.

sendConfirmationEmailboolean

PMS listings only. Ask the PMS to email the guest its own confirmation, where it supports that.

checkInTimestring

Direct-booking listings only. HH:MM, e.g. 16:00.

checkOutTimestring

Direct-booking listings only. HH:MM, e.g. 10:00.

currencystring

Direct-booking listings only. ISO 4217 code; a PMS books in the property's currency.

guestIdinteger

Direct-booking listings only. Attach an existing guest in this workspace instead of matching one.

Refused by name, never dropped

A field the listing cannot take answers 422 unsupported_field with the field in fields. A PMS listing uses the PMS's own arrival times, currency and guest records; a direct-booking listing is priced from its own rates, so it refuses totalPrice.

Response — 201

{
  "id": "215708",
  "confirmationCode": "HA-4471923",
  "listingId": "4118",
  "platform": "direct",
  "status": "confirmed",
  "checkIn": "2026-10-01",
  "checkOut": "2026-10-05",
  "guestId": "91234",
  "totalPrice": 880,
  "currency": "USD",
  "unit": null,
  "pms": {
    "provider": "hostaway",
    "reservationId": "4471923",
    "applied": ["reservation"],
    "errors": [],
    "partial": false,
    "failedSections": []
  }
}
idstring

The Repull reservation id. Pass it to `GET`, `PATCH` and `/cancel`.

confirmationCodestring

On a PMS listing, the PMS's own code — the one the next sync carries.

totalPricenumbernullable

What the booking was recorded at: your `totalPrice` where the PMS honours it, else the PMS's price (or, for a direct booking, the listing's rates).

unitobjectnullable

PMS listings: the unit the PMS assigned, or null.

pmsobject

Present when it was booked in a PMS.

A direct booking

On a listing no PMS manages, send the direct-booking fields instead. There is no pms block in the response.

curl -X POST "https://api.repull.dev/v1/reservations" \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 3b8e0c52-1d7a-4b1e-a2f9-6c0d8e7f1a24" \
  -d '{
    "listingId": 4119,
    "checkIn": "2026-10-01",
    "checkOut": "2026-10-05",
    "guest": { "firstName": "Ada", "email": "ada@example.com" },
    "guestCount": 2,
    "checkInTime": "16:00",
    "currency": "USD"
  }'

Idempotency: send a key, and keep it

Always send an Idempotency-Key — a UUID generated where you build the booking. A network timeout on a booking is exactly the case it exists for. The key is also sent to the PMSas the booking's reference, so a retry that reaches the PMS a second time finds the booking instead of making another one.

AnswerRetry?
201, or any final refusalStored for 24 hours. The same key replays it with Idempotency-Status: cached; the PMS is not called again.
502 pms_errorNot stored. Retry with the SAME key — the PMS finds the booking if the first attempt went through.
409 pms_duplicateThe PMS already holds a booking with this key. existing names it: look it up instead of creating it again.
502 reservation_created_in_pms_onlyStored, unlike every other 5xx. Do NOT retry, and never with a new key.
422 pms_rejectedCorrect the request, then send it with a NEW key.
403 connection_reauth_requiredNot stored. Reconnect, then retry with the same key.

When the booking exists only in the PMS

Rarely, the PMS takes the booking but its record cannot be read back in time. The answer is a 502 that is final: the booking exists in the PMS (existing names it), it arrives in Repull with the next sync, and reservation.created fires then.

{
  "error": {
    "code": "reservation_created_in_pms_only",
    "message": "Created in Lodgify (LG-88231) but its record could not be read here. It will arrive with the next sync.",
    "fix": "Do NOT retry. The booking exists in Lodgify (see `existing`) but could not be recorded in Repull yet; it arrives with the next sync and `reservation.created` fires then. Retrying with the same `Idempotency-Key` replays this answer.",
    "docs_url": "https://repull.dev/docs/errors/reservation_created_in_pms_only",
    "request_id": "req_01J5X7Y8Z9ABCDEF12345678",
    "provider": "lodgify",
    "existing": { "externalId": "88231", "confirmationCode": "LG-88231" },
    "pms": { "applied": ["reservation"], "errors": [] },
    "retryable": false
  }
}

Do not retry with a new key

A retry with the same key replays this answer. A retry with a new key is a new booking request, and on a PMS without its own duplicate check it books the stay twice. Treat retryable: false as final and wait for the webhook. See reservation_created_in_pms_only.

Partial success

A booking is made in steps: the reservation itself, then follow-ups such as notes or a tentative state. When the reservation was created but a follow-up did not apply, the answer is still 201, with pms.partial: true and the steps in pms.failedSections. The booking exists — do not create it again. Fix the missing part in the PMS, or tell your user what did not apply.

{
  "id": "215709",
  "confirmationCode": "SM-1290031",
  "listingId": "4120",
  "status": "confirmed",
  "totalPrice": 640,
  "currency": "EUR",
  "pms": {
    "provider": "smoobu",
    "reservationId": "1290031",
    "applied": ["reservation"],
    "errors": [{ "section": "notes", "code": "rejected", "message": "Smoobu: notice too long" }],
    "partial": true,
    "failedSections": [{ "section": "notes", "code": "rejected", "message": "Smoobu: notice too long" }]
  }
}

Changing and cancelling

Changes and cancellations follow the booking to where it lives. A stay managed in a PMS is changed in the PMS first, then Repull's copy is refreshed from the PMS's record — changing only Repull's copy would be undone by the next sync. Both endpoints return the PMS's outcome as pms.

PATCH/v1/reservations/{id}
curl -X PATCH "https://api.repull.dev/v1/reservations/215708" \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5d2c9a10-8e4f-4c3b-9a71-2f6e0b8d4c55" \
  -d '{ "checkIn": "2026-10-02", "checkOut": "2026-10-06", "guestCount": 3 }'
  • On a PMS stay, checkIn, checkOut and guestCount are changed. Moving it to another listing and changing arrival or departure times are done in the PMS (422 pms_write_unsupported), as is anything the PMS's API cannot change — dates on Smoobu, for example.
  • The PMS checks availability: taken dates answer 409 pms_unavailable.
  • Several PMSs keep the booked total when the dates change (Hostaway, Beds24, BookingSync, iGMS, Hospitable); OwnerRez does not recalculate charges either. Adjust the price in the PMS if it should change.
  • Guest details, pricing, status, platform and notes are refused by name with 422 unsupported_field. See Update a reservation.
POST/v1/reservations/{id}/cancel
curl -X POST "https://api.repull.dev/v1/reservations/215708/cancel" \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7a3f1e92-0c5b-4d8e-b6a4-9e2d1c0f8b37" \
  -d '{ "reason": "Guest changed plans" }'
  • A PMS booking is cancelled in the PMS, then read back. No cancellation fee is charged.
  • Lodgify declines the booking rather than deleting it.
  • OwnerRez's API cannot cancel: 422 pms_write_unsupported — cancel it in OwnerRez.
  • Direct, website and owner bookings are cancelled in Repull, releasing the nights.
  • Cancelling a booking that is already cancelled returns alreadyCancelled: true, not an error.
  • A channel booking — including one relayed through a PMS — answers 409 reservation_owned_by_channel. So does a change; group bookings in the PMS answer 409 pms_group_booking.

See Cancel a reservation for channel bookings. However a booking is changed — here, in the PMS, or on a channel — reservation.updated and reservation.cancelled fire when the change reaches Repull.

Each PMS at a glance

PMSCreateChangeCancelQuotetotalPrice
Mews✓✓✓–✓
Cloudbeds✓✓✓–– (rate plan price)
Hostaway✓✓✓✓✓
Guesty✓✓✓ (direct + Vrbo)✓✓
Beds24✓✓✓✓✓
BookingSync✓✓✓✓✓
Lodgify✓✓✓ (declines)✓✓
Smoobu✓✓ (no dates)✓✓✓
Hospitable✓✓✓✓ (Direct plan)✓
iGMS✓✓✓–✓ (required)
OwnerRez✓✓–✓– (property rates)
Track✓✓✓✓✓ (Channel Key: "Allow Custom Pricing")

Constraints and the access each PMS needs are on PMS reservation support. Mews and Cloudbeds were run end to end on their vendors' sandboxes; every other PMS is verified against the vendor's API documentation.

Errors

Every error carries fix and docs_url, plus provider when a PMS is involved. Switch on code.

CodeHTTPMeaning
pms_write_unsupported422The PMS’s API cannot do this. Do it in the PMS.
unsupported_field422A field this listing cannot take, named in `fields`.
pms_rejected422The PMS refused the content. Fix it; retry with a NEW key.
pms_unavailable409Dates or unit taken in the PMS. Nothing booked.
pms_duplicate409Already booked with this key. `existing` names it.
pms_group_booking409Part of a group booking. Change it in the PMS.
reservation_owned_by_channel409A channel booking. Change it on the channel.
no_connection409The listing’s PMS is no longer connected. Reconnect.
pms_writes_off409The connection’s write policy turns API bookings off.
connection_reauth_required403The grant lacks booking write access. Reconnect.
pms_not_linked422Quote only: the listing is not managed in a PMS.
pms_error502The PMS could not be reached. Retry with the SAME key.
reservation_created_in_pms_only502Booked in the PMS only. Do NOT retry.
AI