Reviews
Every review on Airbnb and Booking.com in one API: read them in one feed, reply to what guests wrote, and write your review of a guest — without knowing which channel a review came from. Both directions (guest → host and host → guest) come back in the same shape.
What you get
- One feed across channels— Airbnb and Booking reviews land in the same shape. Filter by
platformwhen you only care about one. - Both directions —
reviewerRoledistinguishes guest reviews (about your listing) from host reviews (about the guest). - Sub-category ratings— cleanliness, communication, accuracy, etc. surfaced in
categories[]. Categories vary by channel. - Response visibility — the
responseblock shows whether a host reply has been posted and when. Filterstatus=unansweredto triage. - Reply and review from here — reply to a review on any channel, and review a guest (Airbnb), with the same review
id. - Review window —
expires_attells you when the channel-side response deadline closes (Airbnb is 14 days post-checkout).
How channels get into this feed
List reviews
Cursor-paginated. Pass the previous response's pagination.nextCursorback as cursor on the next call.
/v1/reviewscurl 'https://api.repull.dev/v1/reviews?platform=airbnb&listingId=4118&rating_min=1&rating_max=3&status=unanswered' \ -H 'Authorization: Bearer sk_live_...'
Query parameters
limitintegerDefault: 25Max reviews per page. 1-100; over 100 returns 422.
cursorstringOpaque cursor from the previous response. Omit on the first call.
platformstringFilter by source channel. One of airbnb or booking.
listingIdintegerOnly return reviews attached to this listing.
rating_minnumberInclusive lower bound on the overall rating (typically 1..5).
rating_maxnumberInclusive upper bound on the overall rating.
statusstringResponse status. One of responded, unanswered, all. Defaults to all.
reviewerRolestringDefault: guestDirection. One of guest (about your property), host (about the guest), all.
Response
{
"data": [
{
"id": 778201,
"external_id": "abnb_rev_xyz789",
"platform": "airbnb",
"listing_id": 4118,
"reservation_id": 216039,
"reservation_confirmation_code": "HMABCDE123",
"guest_id": 88341,
"guest_name": "Sarah Mitchell",
"guest_avatar": "https://images.repull.dev/g/88341.jpg",
"reviewer_role": "guest",
"rating": 2,
"categories": [
{ "category": "cleanliness", "rating": 1, "comment": "Found hair in the bathroom." },
{ "category": "communication", "rating": 5, "comment": null },
{ "category": "accuracy", "rating": 3, "comment": null },
{ "category": "location", "rating": 5, "comment": null },
{ "category": "check_in", "rating": 4, "comment": null },
{ "category": "value", "rating": 2, "comment": null }
],
"public_review": "Beautiful location but the cleaning was below standard.",
"private_feedback": "The bathroom needs a deeper clean — found hair in the shower.",
"is_reviewee_recommended": null,
"response": null,
"submitted_at": "2026-04-29T11:14:09.000Z",
"updated_at": "2026-04-29T11:14:09.000Z",
"expires_at": "2026-05-13T11:14:09.000Z",
"hidden": false
},
{
"id": 778202,
"external_id": "abnb_rev_xyz790",
"platform": "airbnb",
"listing_id": 4118,
"reservation_id": 216039,
"reservation_confirmation_code": "HMABCDE123",
"guest_id": 88341,
"guest_name": "Sarah Mitchell",
"guest_avatar": "https://images.repull.dev/g/88341.jpg",
"reviewer_role": "host",
"rating": null,
"categories": [],
"public_review": "Sarah was a respectful guest and left the place tidy.",
"private_feedback": null,
"is_reviewee_recommended": true,
"response": null,
"submitted_at": "2026-04-29T09:02:11.000Z",
"updated_at": "2026-04-29T09:02:11.000Z",
"expires_at": null,
"hidden": false
}
],
"pagination": {
"nextCursor": "eyJsYXN0SWQiOjc3ODIwMn0",
"hasMore": true
}
}idintegerStable internal review id. Pass to /v1/reviews/{id}.
external_idstringID in the source channel. Stable across syncs.
platformstringnullableairbnb or booking.
listing_idintegernullableInternal listing id this review is attached to.
reservation_idintegernullableInternal reservation id this review is about.
reservation_confirmation_codestringnullableChannel-side confirmation code for the reservation.
guest_idintegernullableInternal guest id.
guest_namestringnullableDisplay name of the guest.
reviewer_rolestringguest (review of host/property) or host (review of guest).
ratingnumbernullableOverall rating on the platform's scale (typically 1..5). Null for review types without a numeric overall.
categoriesarraySub-category ratings — cleanliness, communication, accuracy, etc. Categories vary by channel.
public_reviewstringnullablePublic-facing text shown on the listing page.
private_feedbackstringnullablePrivate feedback the reviewer sent only to the host.
is_reviewee_recommendedbooleannullableWhether the reviewer recommended the reviewee. Used on host-side reviews.
responseobjectHost response when present.
submitted_atstringnullableISO 8601 timestamp the review was submitted on the channel.
expires_atstringnullableWhen the channel response window closes (Airbnb: 14 days).
hiddenbooleanTrue when the review is hidden from public view on the channel.
Get one review
Returns a single Review wrapped in { data }. Use after a list call to refresh a single record (for example, when polling for a host response that just landed).
/v1/reviews/{id}curl 'https://api.repull.dev/v1/reviews/778201' \ -H 'Authorization: Bearer sk_live_...'
Response
{
"data": {
"id": 778201,
"external_id": "abnb_rev_xyz789",
"platform": "airbnb",
"listing_id": 4118,
"reservation_id": 216039,
"reservation_confirmation_code": "HMABCDE123",
"guest_id": 88341,
"guest_name": "Sarah Mitchell",
"guest_avatar": "https://images.repull.dev/g/88341.jpg",
"reviewer_role": "guest",
"rating": 2,
"categories": [
{ "category": "cleanliness", "rating": 1, "comment": "Found hair in the bathroom." }
],
"public_review": "Beautiful location but the cleaning was below standard.",
"private_feedback": "The bathroom needs a deeper clean — found hair in the shower.",
"is_reviewee_recommended": null,
"response": {
"body": "Sarah, thank you for the feedback — we've already retrained the cleaning team. Hope you'll give us another chance.",
"submitted_at": "2026-04-30T08:14:00.000Z"
},
"submitted_at": "2026-04-29T11:14:09.000Z",
"updated_at": "2026-04-30T08:14:00.000Z",
"expires_at": "2026-05-13T11:14:09.000Z",
"hidden": false
}
}Reply to a review
Posts your public reply under a review a guest wrote about you (reviewerRole: "guest"). Works on Airbnb and Booking.com — Repull reads the review's channel and sends the reply there. Each channel accepts one reply per review.
/v1/reviews/{id}/replyBody
messagestringRequiredYour reply, shown publicly under the guest's review. response is accepted as an alias.
curl -X POST 'https://api.repull.dev/v1/reviews/778201/reply' -H 'Authorization: Bearer sk_live_...' -H 'Content-Type: application/json' -d '{"message": "Thank you for staying with us — come back any time!"}'201— the reply was posted on the channel.422 unsupported_channel— the review is from a channel without a reply API; the error names the channels that work.403 listing_inactive— the review's listing is inactive; nothing was sent.422 pms_write_unsupported— the review came through a PMS whose API cannot reply (Hostaway); reply in the PMS.
Reviews from a connected PMS. A review read through Guesty or Hostaway carries pms ("guesty", "hostaway"); platform is still the channel the guest wrote it on. Replying to it goes through that PMS, and the response names it in pms. Guesty replies on Airbnb and Booking.com reviews; Hostaway's review API is read-only. See Writing through a PMS.
Review a guest
After checkout Airbnb opens a review of the guest for you. It is listed here with reviewerRole: "host" and an expiresAt deadline (14 days after checkout). Booking.com does not let hosts review guests.
Submitting publishes it, and it is final
409 review_already_submitted. Send it when it is ready./v1/reviews/{id}/guest-reviewBody
publicReviewstringRequiredYour review of the guest, shown on their Airbnb profile. Up to 1000 characters. comment is accepted as an alias.
isRevieweeRecommendedbooleanRequiredWhether you would host this guest again. Airbnb requires it, and Repull never fills it in for you.
ratingintegerA 1–5 score used for every category you do not rate in categoryRatings.
categoryRatingsarrayPer-category scores, each { category, rating, comment? }. Categories are cleanliness, communication and respect_house_rules; all three must end up rated, by here or by rating. comment is optional, up to 50 characters.
privateFeedbackstringA note to the guest that is not published. Up to 1000 characters.
curl -X POST 'https://api.repull.dev/v1/reviews/181567/guest-review' -H 'Authorization: Bearer sk_live_...' -H 'Content-Type: application/json' -d '{
"publicReview": "Joanne was a great guest — the place was left spotless.",
"rating": 5,
"categoryRatings": [
{ "category": "communication", "rating": 4, "comment": "A little slow to reply" }
],
"privateFeedback": "Thanks for taking such good care of the place!",
"isRevieweeRecommended": true
}'200— published on Airbnb.422 invalid_params— a required piece is missing;fieldnames it. Nothing was sent, so the review is not used up.422 unsupported_channel— the review is not from Airbnb.409 review_already_submitted,409 review_window_closed,409 not_host_review— already submitted, pastexpiresAt, or the id is the guest's review of you (reply to it instead).
What each rating means, and how both reviews stay hidden until both sides submit: Airbnb reviews.
What each channel supports
| Channel | Read | Reply | Review a guest |
|---|---|---|---|
| Airbnb | Yes | Yes | Yes |
| Booking.com | Yes | Yes | No — Booking.com has no host reviews |
| Guesty (PMS) | Yes | Yes — Airbnb and Booking.com reviews | No |
| Hostaway (PMS) | Yes | No — Hostaway's review API is read-only | No |
Common patterns
Unresponded reviews to follow up
Filter status=unanswered(guest-written reviews are the default — host-side reviews don't get host responses). Walk all pages, sort by expiresAtto triage the ones closest to the channel deadline.
import type { Review } from '@repull/sdk'
const queue: Review[] = []
let cursor: string | undefined
// reviewerRole defaults to guest — host-side reviews don't get host responses
do {
const page = await repull.reviews.list({
status: 'unanswered',
cursor,
limit: 100,
})
queue.push(...page.data)
cursor = page.pagination.nextCursor ?? undefined
} while (cursor)
queue.sort((a, b) => (a.expiresAt ?? '').localeCompare(b.expiresAt ?? ''))
for (const r of queue.slice(0, 20)) {
console.log(r.id, r.platform, '— expires', r.expiresAt, '—', r.publicReview?.slice(0, 60))
}Low-rating reviews this month
Filter on rating_max=3 (guest-written reviews are the default), then filter on submittedAt client-side to scope to the current month. Useful for an ops-quality dashboard.
import type { Review } from '@repull/sdk'
const startOfMonth = new Date()
startOfMonth.setUTCDate(1)
startOfMonth.setUTCHours(0, 0, 0, 0)
const cutoff = startOfMonth.toISOString()
const lowRatings: Review[] = []
let cursor: string | undefined
do {
const page = await repull.reviews.list({
rating_max: 3,
cursor,
limit: 100,
})
for (const r of page.data) {
if (r.submittedAt && r.submittedAt >= cutoff) lowRatings.push(r)
}
cursor = page.pagination.nextCursor ?? undefined
} while (cursor)
console.log(lowRatings.length, 'low-rating reviews this month')Average rating per listing
Pull guest-side reviews for one listing across all channels and average the ratings (skip nulls — some review types don't score numerically).
// The listing filter is query-string only (`listingId`), so this pattern
// calls the API directly.
const headers = { Authorization: `Bearer ${process.env.REPULL_API_KEY}` }
const reviews: Array<{ rating: number | null }> = []
let cursor: string | null = null
do {
const url = new URL('https://api.repull.dev/v1/reviews')
url.searchParams.set('listingId', '4118')
url.searchParams.set('limit', '100')
if (cursor) url.searchParams.set('cursor', cursor)
const page = await fetch(url, { headers }).then((r) => r.json())
reviews.push(...page.data)
cursor = page.pagination.nextCursor
} while (cursor)
const scored = reviews.filter((r) => r.rating != null)
const avg = scored.reduce((a, r) => a + (r.rating as number), 0) / scored.length
console.log('Listing 4118 average:', avg.toFixed(2), 'across', scored.length, 'reviews')Related
- Connect (multi-channel) — how Airbnb and Booking accounts get linked so reviews start syncing.
- Conversations — jump to the message thread for the reservation a review is about via
reservation_id. - Guests — pull the full profile of the guest who left the review via
guest_id.