Docs/Documentation/Guest data

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 platform when you only care about one.
  • Both directionsreviewerRole distinguishes 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 response block shows whether a host reply has been posted and when. Filter status=unansweredto triage.
  • Review windowexpires_at tells you when the channel-side response deadline closes (Airbnb is 14 days post-checkout).

How channels get into this feed

A review only shows up here once the channel itself is connected. See Connect (multi-channel) for how Airbnb / Booking / VRBO accounts get linked. After connection, the per-channel backfill runs on a schedule — expect first reviews within minutes.

List reviews

Cursor-paginated. Pass the previous response's pagination.nextCursorback as cursor on the next call.

GET/v1/reviews
curl '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: 25

Max reviews per page. 1-100; over 100 returns 422.

cursorstring

Opaque cursor from the previous response. Omit on the first call.

platformstring

Filter by source channel. One of airbnb, booking, vrbo.

listingIdinteger

Only return reviews attached to this listing.

rating_minnumber

Inclusive lower bound on the overall rating (typically 1..5).

rating_maxnumber

Inclusive upper bound on the overall rating.

statusstring

Response status. One of responded, unanswered, all. Defaults to all.

reviewerRolestringDefault: guest

Direction. 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
  }
}
idinteger

Stable internal review id. Pass to /v1/reviews/{id}.

external_idstring

ID in the source channel. Stable across syncs.

platformstringnullable

airbnb, booking, vrbo.

listing_idintegernullable

Internal listing id this review is attached to.

reservation_idintegernullable

Internal reservation id this review is about.

reservation_confirmation_codestringnullable

Channel-side confirmation code for the reservation.

guest_idintegernullable

Internal guest id.

guest_namestringnullable

Display name of the guest.

reviewer_rolestring

guest (review of host/property) or host (review of guest).

ratingnumbernullable

Overall rating on the platform's scale (typically 1..5). Null for review types without a numeric overall.

categoriesarray

Sub-category ratings — cleanliness, communication, accuracy, etc. Categories vary by channel.

public_reviewstringnullable

Public-facing text shown on the listing page.

private_feedbackstringnullable

Private feedback the reviewer sent only to the host.

is_reviewee_recommendedbooleannullable

Whether the reviewer recommended the reviewee. Used on host-side reviews.

responseobject

Host response when present.

submitted_atstringnullable

ISO 8601 timestamp the review was submitted on the channel.

expires_atstringnullable

When the channel response window closes (Airbnb: 14 days).

hiddenboolean

True 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).

GET/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')
  • 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.
AI