Docs/Documentation/Guest data

Conversations

Read your guest message threads and the messages inside them across every connected channel — Airbnb, Booking.com, VRBO, website, email — through one API. One thread per reservation; messages stream back with sender, channel, direction, and delivery state.

What you get

  • Unified inbox— one paginated feed of threads across every channel you've connected, ordered by most recent activity.
  • Single-thread expand— pull the host + guest blocks (with contacts) for any thread by ID, no extra round-trip needed.
  • Message history— cursor-paginated messages within a thread, with direction (inbound / outbound), source channel, attachments, and the original platform timestamp.
  • Filterable— narrow by platform or status at the list level so you don't pull the world client-side.

Push, don't poll

The conversations endpoints back a UI well, but for live inboxes subscribe to conversation.updated via webhooks and only re-fetch the threads that changed.

List threads

Returns your conversation threads ordered by most recent message. Cursor-paginated — pass the previous response's pagination.nextCursor back as cursor on the next call.

GET/v1/conversations
curl 'https://api.repull.dev/v1/conversations?limit=50&platform=airbnb&status=open' \
  -H 'Authorization: Bearer sk_live_...'

Query parameters

limitintegerDefault: 50

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

cursorstring

Opaque cursor from the previous response's pagination.nextCursor. Omit on the first call.

platformstring

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

statusstring

Filter by thread status. One of open, archived. Defaults to open.

Response

{
  "data": [
    {
      "id": 412987,
      "platform": "airbnb",
      "guest_id": 88341,
      "listing_id": 4118,
      "reservation_id": 216039,
      "subject": null,
      "last_message_at": "2026-04-30T18:14:09.000Z",
      "last_message_preview": "What time can I check in?",
      "unread_count": 2,
      "status": "open",
      "created_at": "2026-04-22T09:01:14.000Z",
      "updated_at": "2026-04-30T18:14:09.000Z"
    },
    {
      "id": 412988,
      "platform": "booking",
      "guest_id": 88342,
      "listing_id": 4119,
      "reservation_id": 216064,
      "subject": null,
      "last_message_at": "2026-04-30T17:02:11.000Z",
      "last_message_preview": "Grazie!",
      "unread_count": 0,
      "status": "open",
      "created_at": "2026-04-21T11:24:02.000Z",
      "updated_at": "2026-04-30T17:02:11.000Z"
    }
  ],
  "pagination": {
    "nextCursor": "eyJsYXN0SWQiOjQxMjk4OH0",
    "hasMore": true
  }
}
idinteger

Stable internal thread id. Pass to /v1/conversations/{id}.

platformstring

Source channel: airbnb, booking, vrbo, website, email.

guest_idintegernullable

ID of the linked guest. Hydrate via /v1/guests/{id}.

listing_idintegernullable

ID of the listing the thread belongs to.

reservation_idintegernullable

ID of the linked reservation.

subjectstringnullable

Thread subject (email/website channels) or null.

last_message_atstringnullable

ISO 8601 timestamp of the most recent message.

last_message_previewstringnullable

Short preview of the most recent message body.

unread_countinteger

Inbound messages the host hasn't read yet.

statusstring

open or archived.

created_atstring

ISO 8601 timestamp the thread was first created in Repull.

updated_atstring

ISO 8601 timestamp of the last server-side update.

Get one thread

Returns the full ConversationDetail: every field on the list-row plus inline host and guestblocks (with up to 50 contacts), so a thread-detail header doesn't need a second round-trip.

GET/v1/conversations/{id}
curl 'https://api.repull.dev/v1/conversations/412987' \
  -H 'Authorization: Bearer sk_live_...'

Response

{
  "id": 412987,
  "platform": "airbnb",
  "guest_id": 88341,
  "listing_id": 4118,
  "reservation_id": 216039,
  "subject": null,
  "last_message_at": "2026-04-30T18:14:09.000Z",
  "last_message_preview": "What time can I check in?",
  "unread_count": 2,
  "status": "open",
  "created_at": "2026-04-22T09:01:14.000Z",
  "updated_at": "2026-04-30T18:14:09.000Z",
  "host": {
    "id": 4,
    "airbnbId": "1234567",
    "firstName": "Coastal",
    "displayName": "Coastal Stays Lisbon",
    "avatarUrl": "https://images.repull.dev/h/4.jpg"
  },
  "guest": {
    "id": 88341,
    "displayName": "Sarah Mitchell",
    "avatarUrl": "https://images.repull.dev/g/88341.jpg",
    "contacts": [
      { "type": "email", "value": "sarah.mitchell@example.com", "isPrimary": true,  "isVerified": true  },
      { "type": "phone", "value": "+15551234567",               "isPrimary": false, "isVerified": false }
    ]
  }
}

Field-name conventions

Top-level conversation fields use snake_case (last_message_at, unread_count) to match the underlying threads model. Nested host/guest blocks use camelCase (displayName, isPrimary) because they're shared with the guests endpoints. Both are stable.

Messages within a thread

Cursor-paginated. Defaults to oldest-first so you can append straight into a chat view; pass order=desc to walk back from the latest message.

GET/v1/conversations/{id}/messages
curl 'https://api.repull.dev/v1/conversations/412987/messages?limit=100&order=asc' \
  -H 'Authorization: Bearer sk_live_...'

Query parameters

limitintegerDefault: 100

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

cursorstring

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

orderstringDefault: asc

Sort order. asc returns oldest first, desc returns newest first.

Response

{
  "data": [
    {
      "id": 9912034,
      "external_message_id": "abnb_msg_abcd1234",
      "direction": "inbound",
      "sender_type": "guest",
      "sender_name": "Sarah Mitchell",
      "sender_avatar": "https://images.repull.dev/g/88341.jpg",
      "channel": "airbnb",
      "body": "Hi! What time can I check in tomorrow?",
      "translated_body": null,
      "attachments": [],
      "is_automated": false,
      "ai_generated": false,
      "sent_at": "2026-04-30T17:58:42.000Z",
      "delivered_at": "2026-04-30T17:58:43.000Z",
      "read_at": null
    },
    {
      "id": 9912035,
      "external_message_id": "abnb_msg_abcd1235",
      "direction": "outbound",
      "sender_type": "host",
      "sender_name": "Coastal Stays Lisbon",
      "sender_avatar": "https://images.repull.dev/h/4.jpg",
      "channel": "airbnb",
      "body": "Anytime after 3pm — door code is in your check-in guide.",
      "translated_body": null,
      "attachments": [],
      "is_automated": true,
      "ai_generated": true,
      "sent_at": "2026-04-30T18:01:11.000Z",
      "delivered_at": "2026-04-30T18:01:12.000Z",
      "read_at": "2026-04-30T18:14:01.000Z"
    }
  ],
  "pagination": {
    "nextCursor": null,
    "hasMore": false
  }
}
idinteger

Stable internal message id.

external_message_idstringnullable

ID assigned by the source channel. Stable across syncs.

directionstring

inbound (from guest) or outbound (from host or automation).

sender_typestringnullable

Free-form role from the channel: guest, host, system, airbnb, etc. Use direction for binary logic.

sender_namestring

Display name of the sender.

channelstringnullable

Delivery channel: airbnb, booking, sms, email, etc.

bodystring

Message body in the original language.

translated_bodystringnullable

English translation when the original is non-English.

attachmentsarray

Inline attachments (images, etc.).

is_automatedboolean

True when sent by a Vanio automation (template, schedule).

ai_generatedboolean

True when the body was authored by Vanio AI.

sent_atstring

ISO 8601 timestamp of the original send (from the source platform).

delivered_atstring

ISO 8601 timestamp of channel-side delivery.

read_atstringnullable

ISO 8601 timestamp the recipient read the message.

Common patterns

Stream new messages

Subscribe to conversation.updated via webhooks and re-fetch only threads with a fresh last_message_at. Polling the list endpoint at less than one minute is wasteful — the webhook fires within seconds.

// In your webhook handler:
//   POST /your-handler { type: "conversation.updated", data: { id } }
const thread = await repull.conversations.get(event.data.id)

// Walk only the messages you haven't seen yet
const lastSeen = lastSeenAt[thread.id!] ?? thread.createdAt!
let cursor: string | undefined

do {
  const page = await repull.conversations.messages(thread.id!, {
    cursor,
    order: 'asc',
    limit: 100,
  })
  for (const msg of page.data) {
    if (msg.sentAt! > lastSeen) renderInUi(msg)
  }
  cursor = page.pagination.nextCursor ?? undefined
} while (cursor)

lastSeenAt[thread.id!] = new Date().toISOString()

Filter unread only

The cheapest unread query: list with status=open and filter on unread_count > 0 client-side. Cheaper than per-thread message scans.

import type { Conversation } from '@repull/sdk'

const inbox: Conversation[] = []
let cursor: string | undefined

do {
  const { data, pagination } = await repull.conversations.list({ cursor, limit: 100 })
  for (const t of data) if (t.status === 'open' && (t.unreadCount ?? 0) > 0) inbox.push(t)
  cursor = pagination.nextCursor ?? undefined
} while (cursor)

console.log(inbox.length, 'threads need a reply')

Pagination across thousands of threads

Cursors are stable across writes — new threads pushed in mid-walk won't shift your position. Always loop on pagination.hasMore, never on a count.

async function* allThreads() {
  let cursor: string | undefined
  do {
    const page = await repull.conversations.list({ cursor, limit: 100 })
    for (const t of page.data) yield t
    cursor = page.pagination.nextCursor ?? undefined
  } while (cursor)
}

for await (const thread of allThreads()) {
  // process — works the same for 50 or 50,000
}
  • Send a Message — post a reply back to the originating channel.
  • Connect (multi-channel) — how Airbnb, Booking, VRBO, and direct accounts get linked into a single inbox.
  • Guests — hydrate the guest behind a thread with full contacts, flags, and stay history.
  • Webhooks — subscribe to conversation.updated for push instead of poll.
AI