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.next_cursor 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-200.

cursorstring

Opaque cursor from the previous response's pagination.next_cursor. 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": {
    "next_cursor": "eyJsYXN0SWQiOjQxMjk4OH0",
    "has_more": 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-200.

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": {
    "next_cursor": null,
    "has_more": 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.created_at
let cursor: string | null | undefined

do {
  const page = await repull.conversations.messages(thread.id, {
    cursor: cursor ?? undefined,
    order: 'asc',
    limit: 200,
  })
  for (const msg of page.data) {
    if (msg.sent_at > lastSeen) renderInUi(msg)
  }
  cursor = page.pagination.next_cursor
} 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.

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

do {
  const { data, pagination } = await repull.conversations.list({
    status: 'open',
    cursor: cursor ?? undefined,
    limit: 200,
  })
  for (const t of data) if (t.unread_count > 0) inbox.push(t)
  cursor = pagination.next_cursor
} 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.has_more, never on a count.

async function* allThreads() {
  let cursor: string | null | undefined
  do {
    const page = await repull.conversations.list({
      cursor: cursor ?? undefined,
      limit: 200,
    })
    for (const t of page.data) yield t
    cursor = page.pagination.next_cursor
  } 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