Docs/Documentation/Guest data

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 + filterq matches name, email, and phone; has_reservation hides 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

Returned as a decimal-as-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.

GET/v1/guests
curl 'https://api.repull.dev/v1/guests?q=mitchell&has_reservation=true&limit=50' \
  -H 'Authorization: Bearer sk_live_...'

Query parameters

limitintegerDefault: 50

Max guests per page. 1-200.

cursorstring

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

qstring

Free-text search across guest name, email, and phone (partial match).

has_reservationboolean

When true, only return guests with at least one reservation. Hides browse-only / lead profiles.

listing_idinteger

Only 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
  }
}
idinteger

Stable internal guest id. Pass to /v1/guests/{id}.

displayNamestring

Short display name (typically first name).

displayNameLongstring

Long display name (first + last). Falls back to displayName when last name is missing.

avatarUrlstringnullable

Profile photo URL.

languagestringnullable

Preferred language (ISO 639-1).

countrystringnullable

Country (ISO 3166-1 alpha-2).

phonestringnullable

Primary phone (or first non-primary if no primary set).

emailstringnullable

Primary email.

totalReservationsinteger

Lifetime reservation count.

totalRevenuestring

Decimal-as-string. Lifetime revenue, mixed-currency safe.

lastStayedAtstringnullable

ISO 8601 timestamp of the most recent checkout.

firstStayedAtstringnullable

ISO 8601 timestamp of the first checkout.

created_atstring

ISO 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.

GET/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
  }
}
currencystringnullable

Currency code for totalRevenue (when single-currency); null on mixed-currency totals.

isBlacklistedboolean

Workspace-level blacklist flag.

blacklistedReasonstringnullable

Free-text reason when blacklisted.

riskLevelstringnullable

Risk score: low, medium, high (or null when uncomputed).

verificationLevelstringnullable

Identity / contact verification level from the channel.

contactsarray

All known contact channels (phones, emails). Up to 50.

flagsarray

Risk / operational flags (info, warning, block).

notesarray

Free-form notes left by your team.

reservations_summaryobject

Aggregate 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)
}
AI