Reviews
Cross-channel guest and host reviews — Airbnb, Booking.com, VRBO — in one unified feed. Filter by platform, listing, rating range, response status, or reviewer role. Both directions (guest → host and host → guest) are returned through the same shape.
What you get
- One feed across channels— Airbnb, Booking, VRBO reviews land in the same shape. Filter by
platformwhen you only care about one. - Both directions —
reviewer_roledistinguishes 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=needs_responseto triage. - 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.next_cursorback as cursor on the next call.
/v1/reviewscurl 'https://api.repull.dev/v1/reviews?platform=airbnb&listing_id=4118&rating_min=1&rating_max=3&status=needs_response' \ -H 'Authorization: Bearer sk_live_...'
Query parameters
limitintegerDefault: 50Max reviews per page. 1-200.
cursorstringOpaque cursor from the previous response. Omit on the first call.
platformstringFilter by source channel. One of airbnb, booking, vrbo.
listing_idintegerOnly 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 needs_response, responded, all. Defaults to all.
reviewer_rolestringDirection. One of guest (about your property), host (about the guest).
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": {
"next_cursor": "eyJsYXN0SWQiOjc3ODIwMn0",
"has_more": true
}
}idintegerStable internal review id. Pass to /v1/reviews/{id}.
external_idstringID in the source channel. Stable across syncs.
platformstringnullableairbnb, booking, vrbo.
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
}
}Common patterns
Unresponded reviews to follow up
Filter status=needs_response and reviewer_role=guest— host-side reviews don't get host responses. Walk all pages, sort by expires_atto triage the ones closest to the channel deadline.
const queue: Review[] = []
let cursor: string | null | undefined
do {
const page = await repull.reviews.list({
status: 'needs_response',
reviewer_role: 'guest',
cursor: cursor ?? undefined,
limit: 200,
})
queue.push(...page.data)
cursor = page.pagination.next_cursor
} while (cursor)
queue.sort((a, b) => (a.expires_at ?? '').localeCompare(b.expires_at ?? ''))
for (const r of queue.slice(0, 20)) {
console.log(r.id, r.platform, '— expires', r.expires_at, '—', r.public_review?.slice(0, 60))
}Low-rating reviews this month
Combine rating_max=3 with reviewer_role=guest, then filter on submitted_at client-side to scope to the current month. Useful for an ops-quality dashboard.
const startOfMonth = new Date()
startOfMonth.setUTCDate(1)
startOfMonth.setUTCHours(0, 0, 0, 0)
const cutoff = startOfMonth.toISOString()
const lowRatings: Review[] = []
let cursor: string | null | undefined
do {
const page = await repull.reviews.list({
rating_max: 3,
reviewer_role: 'guest',
cursor: cursor ?? undefined,
limit: 200,
})
for (const r of page.data) {
if (r.submitted_at && r.submitted_at >= cutoff) lowRatings.push(r)
}
cursor = page.pagination.next_cursor
} 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).
const reviews: Review[] = []
let cursor: string | null | undefined
do {
const page = await repull.reviews.list({
listing_id: 4118,
reviewer_role: 'guest',
cursor: cursor ?? undefined,
limit: 200,
})
reviews.push(...page.data)
cursor = page.pagination.next_cursor
} 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 / Booking / VRBO 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.