Inquiries & Booking Requests
Answer Airbnb guests before they book. Find the inquiries waiting on you, pre-approve them or send a special offer with your own dates and price, and accept or decline booking requests on listings that do not use Instant Book.
Inquiries and requests are different things
Airbnb has two ways for a guest to ask before they book, and they live in different places:
- An inquiryis a question about dates (“Is the flat free 1–5 October?”). There is no booking yet, only a conversation. You answer it with a pre-approval (the guest may book the dates and price they asked about) or a special offer (your own dates, guests and total). List them with
GET /v1/inquiries; act on them by conversation id. - A booking request is a guest asking to book a listing that does not use Instant Book. It is a reservation with status
pending, waiting for you. List them withGET /v1/reservations?status=pending; accept or decline them by reservation id. Airbnb expires a request the host has not answered within 24 hours.
Use Repull ids
GET /v1/conversations or GET /v1/inquiries (conversationId), and the reservation id from GET /v1/reservations. Not the Airbnb thread id or confirmation code. If you only hold Airbnb ids, see Working with Airbnb ids.Which channels support this
Only Airbnb listings connected to Repull directly. Everything else returns 422 channel_not_supported and nothing is sent:
- Booking.com, VRBO and direct bookings confirm instantly or have no request step, so there is nothing to approve.
channelin the error says which. - Airbnb through a property management system (Hostaway, Guesty and so on). That system owns the Airbnb connection, so answer the inquiry or request there. The error carries
pms, andGET /v1/inquiriesmarks these rows withrelayedBy.
Find inquiries that need an answer
/v1/inquiriesNewest first. By default only open inquiries are returned: nobody has answered them and the stay is still ahead. Inquiries on inactive listings are left out.
curl "https://api.repull.dev/v1/inquiries" \ -H "Authorization: Bearer sk_live_YOUR_KEY"
Query parameters
statusstringDefault: openopen, pre_approved, special_offer_sent, booked, expired, declined, not_possible, or all.
listing_idintegerOnly inquiries about this Repull listing. An inactive listing returns 403 listing_inactive.
conversation_idintegerThe inquiry on one conversation. Combine with status=all to see it whatever its state.
limitintegerDefault: 501 to 100.
cursorstringpagination.nextCursor from the previous page. Walk until pagination.hasMore is false. offset (0 to 10000) also works.
include_totalbooleanAdds pagination.total.
How the status is decided
Airbnb rarely announces that an inquiry has lapsed, so Repull works out each inquiry's status from what has happened to it, checking in this order and taking the first that applies:
| status | When |
|---|---|
booked | The guest booked. reservationId is the reservation it became. |
declined | You declined the inquiry. |
not_possible | Airbnb says the dates cannot be booked. |
expired | Airbnb expired it, or the check-in date has passed. |
pre_approved | It was pre-approved and the stay is still ahead. |
special_offer_sent | A special offer is out and the stay is still ahead, whether it was sent through the API or in Airbnb’s own app. |
open | Nobody has answered and the stay is still ahead. These need you. |
respondByis Airbnb's deadline for your answer, which counts toward your response rate. To hear about inquiries as they arrive and move, rather than polling, subscribe to the inquiry webhooks.
Response
{
"data": [
{
"id": "25173",
"conversationId": "164743",
"listingId": "23892",
"channel": "airbnb",
"status": "open",
"checkIn": "2026-10-01",
"checkOut": "2026-10-05",
"guests": { "total": 2, "adults": 2, "children": 0, "infants": 0, "pets": 0 },
"expectedPayout": { "amount": 812.4, "currency": "USD" },
"reservationId": null,
"relayedBy": null,
"respondBy": "2026-09-23T17:40:38.000Z",
"respondedAt": null,
"createdAt": "2026-09-22T17:40:38.000Z",
"updatedAt": "2026-09-22T17:40:38.000Z"
}
],
"pagination": { "nextCursor": null, "hasMore": false }
}conversationIdstringnullablePass it to the pre-approval and special-offer endpoints below.
expectedPayoutobjectWhat Airbnb quoted the host for the stay the guest asked about: amount and currency.
relayedBystringnullableThe PMS this inquiry arrives through. When set, it cannot be pre-approved or offered from Repull.
respondBystringnullableAirbnb's response deadline for the host. Like every timestamp here, an ISO-8601 string in UTC.
reservationIdstringnullableThe reservation the inquiry became, once status is booked.
Pre-approve an inquiry
/v1/conversations/{id}/pre-approvalTells the guest they may book the dates they asked about at the listed price, without waiting on you. The body is optional. To change the dates, the guest count or the price, send a special offer instead.
curl -X POST "https://api.repull.dev/v1/conversations/164743/pre-approval" \ -H "Authorization: Bearer sk_live_YOUR_KEY" \ -H "Idempotency-Key: preapprove-164743"
Body parameters
blockInstantBookingbooleanDefault: falsetrue stops the guest from using Instant Book, so they must book through this pre-approval. Leave it off unless you need that.
Response
201 Created. The inquiry now reads pre_approved in GET /v1/inquiries.
{
"conversationId": "164743",
"status": "pre_approved",
"blockInstantBooking": false,
"expiresAt": "2026-09-23T17:40:38Z"
}expiresAt is when the guest can no longer book on the pre-approval, when Airbnb reports it. A conversation that already has a booking is refused with 409 conversation_already_booked.
Send a special offer
/v1/conversations/{id}/special-offersOffers the guest your own terms: dates, guest count and a total price. The guest has 24 hours to book it. Use it to answer an inquiry with different terms, or to give a returning guest a custom price.
curl -X POST "https://api.repull.dev/v1/conversations/164743/special-offers" \
-H "Authorization: Bearer sk_live_YOUR_KEY" \
-H "Idempotency-Key: offer-164743-1" \
-H "Content-Type: application/json" \
-d '{
"checkIn": "2026-10-01",
"checkOut": "2026-10-05",
"guests": { "adults": 2, "children": 1 },
"totalPrice": 880
}'Body parameters
checkInstringRequiredFirst night, YYYY-MM-DD.
checkOutstringRequiredDeparture day, YYYY-MM-DD. Must be after checkIn.
guestsobjectRequiredWho is staying. Airbnb counts adults plus children as guests; infants and pets are extra.
adultsintegerRequiredAt least 1.
childrenintegerDefault: 00 or more.
infantsintegerDefault: 00 or more.
petsintegerDefault: 00 or more.
totalPricenumberRequiredWhat the guest pays for the whole stay, in the listing's Airbnb currency. Airbnb does not take a currency on an offer.
listingIdintegerRepull listing id to offer. Omit it to offer the listing the guest asked about, which is the usual case. A listing that is not on Airbnb returns 422 listing_not_on_airbnb.
Unknown fields are refused by name, so a misspelling cannot silently change the offer.
Response
201 Created. The inquiry now reads special_offer_sent in GET /v1/inquiries.
{
"id": "1459920384",
"conversationId": "164743",
"status": "active",
"listingId": "23892",
"airbnbListingId": "955656266214757921",
"checkIn": "2026-10-01",
"checkOut": "2026-10-05",
"nights": 4,
"guests": { "total": 3, "adults": 2, "children": 1, "infants": 0, "pets": 0 },
"totalPrice": 880,
"createdAt": "2026-09-22T18:00:00Z",
"expiresAt": "2026-09-23T18:00:00Z"
}idstringnullableAirbnb's id for the offer. Use it to read or withdraw the offer.
statusstringnullableAirbnb's status for the offer: active (the guest can book it), accepted, declined, expired or voided (withdrawn).
airbnbListingIdstringnullableThe Airbnb listing the offer is for. A string: Airbnb ids exceed what a JSON number can hold exactly.
expiresAtstringnullableWhen the guest can no longer book the offer.
An offer Airbnb refuses (dates taken, a price below Airbnb's minimum, too many guests) is never a 201: it is 422 airbnb_rejectedwith Airbnb's own reason in message.
Read or withdraw an offer
/v1/conversations/{id}/special-offers/{offerId}/v1/conversations/{id}/special-offers/{offerId}GET reads the offer back live from Airbnb, typically to check its status, in the same shape as above. DELETE withdraws an offer the guest has not booked, so it can no longer be booked, and returns { "id", "conversationId", "status": "withdrawn" }. The offer must belong to the conversation in the path; an offer id from another conversation returns 404. An offer the guest already booked cannot be withdrawn: 409 inquiry_no_longer_open.
# Check it curl "https://api.repull.dev/v1/conversations/164743/special-offers/1459920384" \ -H "Authorization: Bearer sk_live_YOUR_KEY" # Withdraw it curl -X DELETE "https://api.repull.dev/v1/conversations/164743/special-offers/1459920384" \ -H "Authorization: Bearer sk_live_YOUR_KEY"
Find booking requests
/v1/reservations?status=pendingA booking request is a reservation with status pending. Each one carries respondBy: answer before then, or Airbnb expires it (24 hours after the guest asked). A reservation.request.created webhook fires when a request arrives, so you do not need to poll.
curl "https://api.repull.dev/v1/reservations?status=pending&platform=airbnb" \ -H "Authorization: Bearer sk_live_YOUR_KEY"
status=pending lists only requests you can still answer. A request that has lapsed (its 24 hours ran out, or its check-in date passed) is left out, even when Airbnb never announced it, and is reported under cancelled with statusDetail: "request_expired" instead.
{
"data": [
{
"id": "236354",
"status": "pending",
"platform": "airbnb",
"confirmationCode": "HMT3X9KQZ2",
"checkIn": "2026-10-02",
"checkOut": "2026-10-06",
"respondBy": "2026-09-23T09:00:00.000Z"
}
]
}respondBystringWhen the request lapses, as ISO-8601 in UTC. Present only on a pending request that can still be answered.
statusDetailstringrequest_expired on a request nobody answered in time, which reads status cancelled. Absent otherwise.
Filtering on pending can also surface reservations that are not requests; accepting one of those is refused before Airbnb is contacted, with 409 reservation_not_pending and its currentStatus.
Accept a request
/v1/reservations/{id}/acceptTakes no body.
curl -X POST "https://api.repull.dev/v1/reservations/236354/accept" \ -H "Authorization: Bearer sk_live_YOUR_KEY" \ -H "Idempotency-Key: accept-236354"
{
"reservationId": "236354",
"confirmationCode": "HM9J2MFR3W",
"channel": "airbnb",
"action": "accept",
"status": "accepted",
"declineReason": null
}Airbnb confirms a few seconds later
requestStatus: "accepted", and reservation.created fires for the new booking. Read the new state from the webhook or from GET /v1/reservations/{id}.Decline a request
/v1/reservations/{id}/declineBoth fields are required. message goes to the guest with the decline, and Repull does not make one up for you.
curl -X POST "https://api.repull.dev/v1/reservations/236354/decline" \
-H "Authorization: Bearer sk_live_YOUR_KEY" \
-H "Idempotency-Key: decline-236354" \
-H "Content-Type: application/json" \
-d '{"reason": "dates_not_available", "message": "Sorry, those dates are no longer available."}'reasonstringRequiredOne of Airbnb's decline reasons, listed below. Anything else returns 422 invalid_params listing the accepted values.
messagestringRequiredSent to the guest by Airbnb with the decline. 1 to 500 characters; longer is refused. Follow up with a conversation message if you need to say more.
Decline reasons
dates_not_available— the dates are taken.not_comfortable— you are not comfortable with the booking.listing_not_ready— the listing cannot be booked right now.different_dates_needed— you want different dates.spam— the request is spam.other— anything else; explain inmessage.
Webhooks
Subscribe to these instead of polling. They fire however the guest or host acted: through the API, or in Airbnb's own app.
| Event | When |
|---|---|
| inquiry.created | A guest asked about dates. The object is the inquiry as GET /v1/inquiries returns it. |
| inquiry.updated | An inquiry moved: its status, dates, guest count, or the reservation it became. previousAttributes names what changed. |
| reservation.request.created | A guest asked to book. The object is the pending reservation, with respondBy. |
| reservation.request.updated | A request stopped waiting on you. requestStatus is accepted, declined, expired or voided (withdrawn by the guest or voided by Airbnb). |
A request or inquiry that lapses quietly fires no event
expired in GET /v1/inquiries, and cancelled with statusDetail: "request_expired" in GET /v1/reservations. If you track deadlines, schedule your own check at respondBy rather than waiting for an event.Full payloads for every event are in the event catalog.
Retrying safely
Send an Idempotency-Key header on every POST here. Without one, retrying after a network timeout can send the guest two special offers. With one:
- The same key again returns the first response, marked
Idempotency-Status: cached, and nothing is sent twice. - While the first request is still running, the same key returns
409 idempotency_key_in_use. Wait and retry. - Answers where Airbnb did nothing are not stored, so retry them with the same key and the retry reaches Airbnb again: server errors (
5xx, such asairbnb_error),408,425,429 airbnb_rate_limited,403 connection_reauth_requiredand403 listing_inactive. - Final answers, such as
422 airbnb_rejectedor a409, are stored and replayed. To send a corrected request after one, use a new key.
More on the header: Idempotency Keys.
Errors
An Airbnb refusal is never reported as a success. Each error says whether retrying can help; the 409s never can.
| Code | What to do |
|---|---|
| 422invalid_params | The body is wrong; field names the field. Nothing was sent. |
| 422channel_not_supported | Not a direct Airbnb conversation or booking. Act in the PMS named by pms, or there is nothing to answer. |
| 422airbnb_link_missing | Repull has no Airbnb thread, confirmation code or host for it. Reconnect Airbnb. |
| 422listing_not_on_airbnb | The listingId in the offer is not on Airbnb. Omit it or pick another. |
| 422airbnb_rejected | Airbnb refused; message is its reason. Change the request. |
| 409conversation_already_booked | The guest already booked. Nothing to pre-approve. |
| 409inquiry_no_longer_open | The inquiry or offer already moved on. Re-read it. |
| 409inquiry_expired | The inquiry expired. Message the guest. |
| 409reservation_not_pending | Not a pending request (see currentStatus). Nothing was sent. |
| 409request_no_longer_pending | Already answered, withdrawn or cancelled on Airbnb. Re-read it. |
| 409request_expired | The 24 hours ran out. The guest must request again. |
| 403connection_reauth_required | Airbnb refused the connection for this action. Reconnect with full access. |
| 403listing_inactive | The listing is inactive in Repull. Activate it first. |
| 404not_found | No such conversation, reservation or offer in this workspace. |
| 429airbnb_rate_limited | Back off, then retry with the same Idempotency-Key. |
| 502airbnb_error, offer_action_failed | Outage or unclassified failure. Retry with backoff and the same Idempotency-Key. |
| 500service_misconfigured | A fault on our side. Nothing was sent to Airbnb; do not resend. Report the request_id if it persists. |
End to end: from inquiry to booking
A guest asks about 1–5 October. You either let them book as asked, or offer a better deal for a longer stay. They book, and the booking reaches you like any other.
- An inquiry.created webhook arrives (or poll
GET /v1/inquiriesforopeninquiries). - Skip rows with
relayedByset, and decide: pre-approve, or send a special offer. - The inquiry's status moves to
pre_approvedorspecial_offer_sent, andinquiry.updatedfires. - When the guest books, inquiry.updated fires with status
bookedandreservationIdset, and the booking itself arrives as a new reservation through reservation.created.
const API = 'https://api.repull.dev'
const headers = {
Authorization: `Bearer ${process.env.REPULL_API_KEY}`,
'Content-Type': 'application/json',
}
// 1. An inquiry is waiting on you (from the inquiry.created webhook,
// or from polling GET /v1/inquiries?status=open)
async function answerInquiry(inq: any) {
if (inq.relayedBy) return // answer these in the PMS that relays them
const nights = (Date.parse(inq.checkOut) - Date.parse(inq.checkIn)) / 86_400_000
// 2a. Short stay: let them book as asked
if (nights < 7) {
const res = await fetch(`${API}/v1/conversations/${inq.conversationId}/pre-approval`, {
method: 'POST',
headers: { ...headers, 'Idempotency-Key': `preapprove-${inq.id}` },
})
if (!res.ok) console.warn(inq.id, (await res.json()).error.code)
return
}
// 2b. Long stay: offer 10% off what Airbnb quoted
const total = Math.round((inq.expectedPayout.amount ?? 0) * 0.9)
if (!total) return
const res = await fetch(`${API}/v1/conversations/${inq.conversationId}/special-offers`, {
method: 'POST',
headers: { ...headers, 'Idempotency-Key': `offer-${inq.id}` },
body: JSON.stringify({
checkIn: inq.checkIn,
checkOut: inq.checkOut,
guests: { adults: inq.guests.adults ?? 1, children: inq.guests.children ?? 0 },
totalPrice: total,
}),
})
const body = await res.json()
if (!res.ok) console.warn(inq.id, body.error.code, body.error.message)
}
// 3. Your webhook handler
// (verify the X-Repull-Signature header first: /docs/verify-webhook-signatures)
export async function onWebhook(delivery: { event: string; eventId: string; data: any }) {
const { object } = delivery.data
switch (delivery.event) {
case 'inquiry.created':
await answerInquiry(object)
break
case 'inquiry.updated':
if (object.status === 'booked') {
// The guest booked. object.reservationId is the new reservation,
// which also arrives through reservation.created.
}
break
}
}expectedPayout is what Airbnb quoted the host; totalPrice on an offer is what the guest pays. Price the offer on your own basis if those differ for your listings.
Working with Airbnb ids
If you only hold Airbnb's own ids, the Airbnb channel endpoints do the same things, talking to Airbnb as the account that owns the thread or booking. The endpoints above also record the action on the inquiry, so its status in GET /v1/inquiries follows; these do not. Prefer the endpoints above when you can.
POST /v1/channels/airbnb/offerswith{"type": "preapproval", "thread_id": "…"}, or"type": "offer"withlisting_id(the Airbnb listing id, as a string),start_date,nights,total_priceandguest_details.GETandDELETEwith?offerId=read and withdraw. See Special Offers.POST /v1/channels/airbnb/reservations/{code}with{"action": "accept"},decline(samereasonandmessageas above) orcancel. See Airbnb Reservations.
They return the same error codes as the endpoints on this page.