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 —
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. - 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, booking, vrbo.
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, 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=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 / 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.