Docs/Channels/Airbnb

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 with GET /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

Every endpoint on this page takes Repull ids: the conversation id from 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. channel in 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, and GET /v1/inquiries marks these rows with relayedBy.

Find inquiries that need an answer

GET/v1/inquiries

Newest 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: open

open, pre_approved, special_offer_sent, booked, expired, declined, not_possible, or all.

listing_idinteger

Only inquiries about this Repull listing. An inactive listing returns 403 listing_inactive.

conversation_idinteger

The inquiry on one conversation. Combine with status=all to see it whatever its state.

limitintegerDefault: 50

1 to 100.

cursorstring

pagination.nextCursor from the previous page. Walk until pagination.hasMore is false. offset (0 to 10000) also works.

include_totalboolean

Adds 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:

statusWhen
bookedThe guest booked. reservationId is the reservation it became.
declinedYou declined the inquiry.
not_possibleAirbnb says the dates cannot be booked.
expiredAirbnb expired it, or the check-in date has passed.
pre_approvedIt was pre-approved and the stay is still ahead.
special_offer_sentA special offer is out and the stay is still ahead, whether it was sent through the API or in Airbnb’s own app.
openNobody 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 }
}
conversationIdstringnullable

Pass it to the pre-approval and special-offer endpoints below.

expectedPayoutobject

What Airbnb quoted the host for the stay the guest asked about: amount and currency.

relayedBystringnullable

The PMS this inquiry arrives through. When set, it cannot be pre-approved or offered from Repull.

respondBystringnullable

Airbnb's response deadline for the host. Like every timestamp here, an ISO-8601 string in UTC.

reservationIdstringnullable

The reservation the inquiry became, once status is booked.

Pre-approve an inquiry

POST/v1/conversations/{id}/pre-approval

Tells 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: false

true 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

POST/v1/conversations/{id}/special-offers

Offers 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

checkInstringRequired

First night, YYYY-MM-DD.

checkOutstringRequired

Departure day, YYYY-MM-DD. Must be after checkIn.

guestsobjectRequired

Who is staying. Airbnb counts adults plus children as guests; infants and pets are extra.

adultsintegerRequired

At least 1.

childrenintegerDefault: 0

0 or more.

infantsintegerDefault: 0

0 or more.

petsintegerDefault: 0

0 or more.

totalPricenumberRequired

What the guest pays for the whole stay, in the listing's Airbnb currency. Airbnb does not take a currency on an offer.

listingIdinteger

Repull 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"
}
idstringnullable

Airbnb's id for the offer. Use it to read or withdraw the offer.

statusstringnullable

Airbnb's status for the offer: active (the guest can book it), accepted, declined, expired or voided (withdrawn).

airbnbListingIdstringnullable

The Airbnb listing the offer is for. A string: Airbnb ids exceed what a JSON number can hold exactly.

expiresAtstringnullable

When 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

GET/v1/conversations/{id}/special-offers/{offerId}
DELETE/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

GET/v1/reservations?status=pending

A 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"
    }
  ]
}
respondBystring

When the request lapses, as ISO-8601 in UTC. Present only on a pending request that can still be answered.

statusDetailstring

request_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

POST/v1/reservations/{id}/accept

Takes 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

The response reports what Airbnb was asked to do and did not refuse. The reservation itself moves when Airbnb's own update reaches Repull, usually within seconds. Then reservation.request.updated fires with 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

POST/v1/reservations/{id}/decline

Both 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."}'
reasonstringRequired

One of Airbnb's decline reasons, listed below. Anything else returns 422 invalid_params listing the accepted values.

messagestringRequired

Sent 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 in message.

Webhooks

Subscribe to these instead of polling. They fire however the guest or host acted: through the API, or in Airbnb's own app.

EventWhen
inquiry.createdA guest asked about dates. The object is the inquiry as GET /v1/inquiries returns it.
inquiry.updatedAn inquiry moved: its status, dates, guest count, or the reservation it became. previousAttributes names what changed.
reservation.request.createdA guest asked to book. The object is the pending reservation, with respondBy.
reservation.request.updatedA 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

Airbnb usually does not announce that a request or inquiry ran out of time. When it says nothing, no webhook fires, but the GET endpoints still report it correctly: 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 as airbnb_error), 408, 425, 429 airbnb_rate_limited, 403 connection_reauth_required and 403 listing_inactive.
  • Final answers, such as 422 airbnb_rejected or a 409, 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.

CodeWhat to do
422invalid_paramsThe body is wrong; field names the field. Nothing was sent.
422channel_not_supportedNot a direct Airbnb conversation or booking. Act in the PMS named by pms, or there is nothing to answer.
422airbnb_link_missingRepull has no Airbnb thread, confirmation code or host for it. Reconnect Airbnb.
422listing_not_on_airbnbThe listingId in the offer is not on Airbnb. Omit it or pick another.
422airbnb_rejectedAirbnb refused; message is its reason. Change the request.
409conversation_already_bookedThe guest already booked. Nothing to pre-approve.
409inquiry_no_longer_openThe inquiry or offer already moved on. Re-read it.
409inquiry_expiredThe inquiry expired. Message the guest.
409reservation_not_pendingNot a pending request (see currentStatus). Nothing was sent.
409request_no_longer_pendingAlready answered, withdrawn or cancelled on Airbnb. Re-read it.
409request_expiredThe 24 hours ran out. The guest must request again.
403connection_reauth_requiredAirbnb refused the connection for this action. Reconnect with full access.
403listing_inactiveThe listing is inactive in Repull. Activate it first.
404not_foundNo such conversation, reservation or offer in this workspace.
429airbnb_rate_limitedBack off, then retry with the same Idempotency-Key.
502airbnb_error, offer_action_failedOutage or unclassified failure. Retry with backoff and the same Idempotency-Key.
500service_misconfiguredA 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.

  1. An inquiry.created webhook arrives (or poll GET /v1/inquiries for open inquiries).
  2. Skip rows with relayedBy set, and decide: pre-approve, or send a special offer.
  3. The inquiry's status moves to pre_approved or special_offer_sent, and inquiry.updated fires.
  4. When the guest books, inquiry.updated fires with status booked and reservationId set, 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/offers with {"type": "preapproval", "thread_id": "…"}, or "type": "offer" with listing_id (the Airbnb listing id, as a string), start_date, nights, total_price and guest_details. GET and DELETE with ?offerId= read and withdraw. See Special Offers.
  • POST /v1/channels/airbnb/reservations/{code} with {"action": "accept"}, decline (same reason and message as above) or cancel. See Airbnb Reservations.

They return the same error codes as the endpoints on this page.

AI