Guests
Aggregate guest profiles across all your channels — name, contacts, language, country, stay history, and lifetime value — in a single record. Use the list endpoint for fast inbox-style search; pull the detail endpoint when you need full contacts, flags, and reservation rollups.
What you get
- Unified profile— one record per guest, even when they've stayed via multiple channels under different display names.
- Pre-resolved primary contacts— the list-row already has the primary email and phone hoisted out, so your UI doesn't need a per-row lookup.
- Lifetime aggregates— reservation count, total revenue (decimal string for precision), first stay, last stay.
- Search + filter —
qmatches name, email, and phone;has_reservationhides browse-only profiles;listing_idnarrows to guests of one property. - Detail endpoint— full contacts list (up to 50), risk flags, notes, blacklist state, verification level, and a future/past/cancelled reservation rollup.
totalRevenue is a string
"14250.00"), not a number. This is intentional — it preserves precision when totals span multiple currencies. Parse with a Decimal lib (e.g. decimal.js, Python Decimal) instead of parseFloat.List guests
Cursor-paginated. Pass the previous response's pagination.next_cursorback as cursor on the next call. Loop until has_more is false.
/v1/guestscurl 'https://api.repull.dev/v1/guests?q=mitchell&has_reservation=true&limit=50' \ -H 'Authorization: Bearer sk_live_...'
Query parameters
limitintegerDefault: 50Max guests per page. 1-200.
cursorstringOpaque cursor from the previous response. Omit on the first call.
qstringFree-text search across guest name, email, and phone (partial match).
has_reservationbooleanWhen true, only return guests with at least one reservation. Hides browse-only / lead profiles.
listing_idintegerOnly return guests who have stayed at this listing.
Response
{
"data": [
{
"id": 88341,
"displayName": "Sarah",
"displayNameLong": "Sarah Mitchell",
"avatarUrl": "https://images.repull.dev/g/88341.jpg",
"language": "en",
"country": "US",
"phone": "+15551234567",
"email": "sarah.mitchell@example.com",
"totalReservations": 3,
"totalRevenue": "4250.00",
"lastStayedAt": "2026-03-18T11:00:00.000Z",
"firstStayedAt": "2024-08-04T15:00:00.000Z",
"created_at": "2024-08-01T09:14:22.000Z"
},
{
"id": 88412,
"displayName": "Marco",
"displayNameLong": "Marco Rossi",
"avatarUrl": null,
"language": "it",
"country": "IT",
"phone": "+393331234567",
"email": "marco.rossi@example.com",
"totalReservations": 1,
"totalRevenue": "1820.50",
"lastStayedAt": "2026-04-21T11:00:00.000Z",
"firstStayedAt": "2026-04-21T11:00:00.000Z",
"created_at": "2026-04-15T08:01:11.000Z"
}
],
"pagination": {
"next_cursor": "eyJsYXN0SWQiOjg4NDEyfQ",
"has_more": true
}
}idintegerStable internal guest id. Pass to /v1/guests/{id}.
displayNamestringShort display name (typically first name).
displayNameLongstringLong display name (first + last). Falls back to displayName when last name is missing.
avatarUrlstringnullableProfile photo URL.
languagestringnullablePreferred language (ISO 639-1).
countrystringnullableCountry (ISO 3166-1 alpha-2).
phonestringnullablePrimary phone (or first non-primary if no primary set).
emailstringnullablePrimary email.
totalReservationsintegerLifetime reservation count.
totalRevenuestringDecimal-as-string. Lifetime revenue, mixed-currency safe.
lastStayedAtstringnullableISO 8601 timestamp of the most recent checkout.
firstStayedAtstringnullableISO 8601 timestamp of the first checkout.
created_atstringISO 8601 timestamp the guest record was first created.
Get guest detail
Returns the full GuestProfile: every list-row field plus contacts (up to 50), risk flags, free-form notes, blacklist state, verification level, and a future/past/cancelled reservation summary.
/v1/guests/{id}curl 'https://api.repull.dev/v1/guests/88341' \ -H 'Authorization: Bearer sk_live_...'
Response
{
"id": 88341,
"displayName": "Sarah",
"displayNameLong": "Sarah Mitchell",
"avatarUrl": "https://images.repull.dev/g/88341.jpg",
"language": "en",
"country": "US",
"phone": "+15551234567",
"email": "sarah.mitchell@example.com",
"totalReservations": 3,
"totalRevenue": "4250.00",
"currency": "USD",
"isBlacklisted": false,
"blacklistedReason": null,
"riskLevel": "low",
"verificationLevel": "verified_id_phone",
"created_at": "2024-08-01T09:14:22.000Z",
"contacts": [
{ "type": "email", "value": "sarah.mitchell@example.com", "verified": true, "is_primary": true, "last_used": "2026-04-30T18:14:09.000Z" },
{ "type": "phone", "value": "+15551234567", "verified": true, "is_primary": true, "last_used": "2026-03-18T11:00:00.000Z" },
{ "type": "email", "value": "smitchell@work.example", "verified": false, "is_primary": false, "last_used": null }
],
"flags": [
{ "type": "info", "note": "Repeat guest — late checkout OK", "is_active": true, "created_at": "2025-09-12T10:11:00.000Z" }
],
"notes": [
{ "id": 12, "body": "Allergic to feather pillows", "category": "preference", "created_at": "2025-09-12T10:13:00.000Z", "created_by": "ops@coastalstays.example" }
],
"reservations_summary": {
"total": 3,
"future": 1,
"past": 2,
"cancelled": 0
}
}currencystringnullableCurrency code for totalRevenue (when single-currency); null on mixed-currency totals.
isBlacklistedbooleanWorkspace-level blacklist flag.
blacklistedReasonstringnullableFree-text reason when blacklisted.
riskLevelstringnullableRisk score: low, medium, high (or null when uncomputed).
verificationLevelstringnullableIdentity / contact verification level from the channel.
contactsarrayAll known contact channels (phones, emails). Up to 50.
flagsarrayRisk / operational flags (info, warning, block).
notesarrayFree-form notes left by your team.
reservations_summaryobjectAggregate counts of attached reservations.
Common patterns
Find a returning guest by email
q matches across name, email, and phone. Combine with has_reservation=true to skip browse-only leads.
const { data } = await repull.guests.list({
q: 'sarah.mitchell@example.com',
has_reservation: true,
limit: 5,
})
if (data.length === 0) {
// brand-new guest, create the reservation against a fresh profile
} else {
const returning = data[0]
console.log('Welcome back', returning.displayName,
'— last stay', returning.lastStayedAt)
}Top spenders this year
Walk all guests with has_reservation=true and sort by totalRevenueclient-side. Use a Decimal lib — the field is a string for a reason.
import Decimal from 'decimal.js'
const all: Array<{ id: number; name: string; rev: Decimal }> = []
let cursor: string | null | undefined
do {
const page = await repull.guests.list({
has_reservation: true,
cursor: cursor ?? undefined,
limit: 200,
})
for (const g of page.data) {
if (g.lastStayedAt && g.lastStayedAt.startsWith('2026-')) {
all.push({ id: g.id, name: g.displayNameLong, rev: new Decimal(g.totalRevenue) })
}
}
cursor = page.pagination.next_cursor
} while (cursor)
const top10 = all.sort((a, b) => b.rev.cmp(a.rev)).slice(0, 10)
for (const g of top10) console.log(g.name, '—', g.rev.toString())Guests with active reservations at one listing
Combine listing_id with has_reservation=true, then enrich the ones whose reservations_summary.future > 0 by hitting the detail endpoint.
const { data } = await repull.guests.list({
listing_id: 4118,
has_reservation: true,
limit: 200,
})
const active = await Promise.all(
data.map(g => repull.guests.get(g.id))
).then(profiles => profiles.filter(p => p.reservations_summary.future > 0))
for (const g of active) {
console.log(g.displayNameLong,
'— upcoming:', g.reservations_summary.future,
'— last stay:', g.lastStayedAt)
}Related
- Conversations — pull the message thread for any reservation linked to a guest.
- Get Reservations — reservations are joined back to a guest by
guest_id. - Connect (multi-channel) — how Airbnb, Booking, and direct accounts feed into one unified guest.