Docs/Channels/Airbnb

Airbnb Guest Messaging

Send messages, edit, unsend, mark as read, and add reactions in Airbnb conversation threads. {threadId} is the Airbnb thread id (externalThreadId on a conversation). To message a guest on any channel with one call, use POST /v1/conversations/{id}/messages — see Send a Message (/docs/send-message).

POST/v1/channels/airbnb/messaging/:threadId/messages

Parameters

messagestring

The text to send. Required unless mediaUrl is set; with mediaUrl it follows the file as a separate message.

mediaUrlstring

Public https:// URL of ONE photo or video to send: JPEG, PNG, GIF or WebP (delivered as JPEG), MP4 or QuickTime, up to 10 MB. Repull downloads it and uploads it to Airbnb for you.

mediaTypestring

Optional MIME type hint, e.g. image/jpeg. The type is read from the file itself; this never overrides it.

Sending a photo or video

  • Airbnb carries one file per message and no text beside it. With mediaUrl, the file is sent first and message, if any, follows as a second message.
  • The thread must already be synced to Repull (GET /v1/conversations lists them); otherwise 404 not_found.
  • A send with mediaUrl answers 201 with the same response as POST /v1/conversations/{id}/messages: the stored file in attachments and one entry per Airbnb message in parts. A text-only send answers with Airbnb's own message object.
  • Failures are the attachment codes documented on Send a Message: attachment_type_not_supported, attachment_too_large, attachment_unreachable, attachment_url_not_allowed, message_not_sent (Airbnb does not allow files before a guest books), and message_partially_sent when the file arrived and the text did not.
  • For several files in one request, use POST /v1/conversations/{id}/messages with attachments.
  • Reading a thread (GET /v1/channels/airbnb/messaging/{threadId}/messages) returns each message with its attachments, inbound and outbound: 50 per page newest first, or ?all=true for up to 1000 oldest first.

Several Airbnb accounts

A workspace can connect more than one Airbnb account. By default this route returns every connected account's rows; pass ?account_id=<airbnb host id> to scope to one. Every row carries accountId + accountName either way (the older hostId / hostName remain as aliases), so you can group without a second call. Airbnb host ids exceed 2^53 — keep them as strings and never parse them as numbers: Number("1000000000000000002") is 1772489413932732200, a different account, and returns 404. A two-account walkthrough lives on the Account scope page (/docs/scoping).

# Every connected account — the default, unchanged
curl 'https://api.repull.dev/v1/channels/airbnb/messaging' \
  -H "Authorization: Bearer sk_live_YOUR_KEY"

# Just one account
curl 'https://api.repull.dev/v1/channels/airbnb/messaging?account_id=1000000000000000002' \
  -H "Authorization: Bearer sk_live_YOUR_KEY"

# An id this workspace has not connected — 404, with your own ids to copy
# {
#   "error": {
#     "code": "not_found",
#     "message": "No connected Airbnb account `1772489413932732200` in this workspace.",
#     "field": "account_id",
#     "value_received": "1772489413932732200",
#     "valid_values": ["1000000000000000002", "80000001"]
#   }
# }

Data freshness

Airbnb reads are served from Repull's local mirror and never call Airbnb upstream, so every response carries a dataFreshness envelope telling you whether to trust it. lastSyncedAt is the last import that actually landed data — a run that failed or was rate-limited never moves it. accounts[] gives the verdict per connected account; it is omitted when the workspace has no Airbnb account to attribute.

  • Top-level stale: true means every connected account is stale — nothing in the response is current. For the single-account workspace this is exactly the old behaviour.
  • Top-level stale: false with reason: "partial_account_staleness" means some accounts are fine and some are not. The response is usable; read accounts[] to see which rows to distrust. The reason is emitted deliberately even though stale is false, so a consumer reading only the aggregate is never told everything is fine while one account is down.
  • Top-level stale: false with no reason means every account is current.
  • With ?account_id=, accounts[] holds exactly that account and the top-level fields mirror it.
  • reason is one of host_disconnected_since_<iso>, host_disconnected, host_not_activated, sync_lag_>_24h, never_synced, or partial_account_staleness. fixUrl is the dashboard screen that resolves it.
{
  "dataFreshness": {
    "lastSyncedAt": "2026-09-18T04:12:09.000Z",
    "stale": false,
    "reason": "partial_account_staleness",
    "fixUrl": "https://repull.dev/dashboard/connections",
    "accounts": [
      {
        "accountId": "1000000000000000002",
        "accountName": "Seaside Stays",
        "lastSyncedAt": "2026-09-18T04:12:09.000Z",
        "stale": false
      },
      {
        "accountId": "80000001",
        "accountName": "Casey",
        "lastSyncedAt": null,
        "stale": true,
        "reason": "host_disconnected",
        "fixUrl": "https://repull.dev/dashboard/connections"
      }
    ]
  }
}

Example

# Send a message
curl -X POST https://api.repull.dev/v1/channels/airbnb/messaging/THREAD_ID/messages \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"message": "Welcome! Check-in instructions are..."}'

# Send a photo, then a caption
curl -X POST https://api.repull.dev/v1/channels/airbnb/messaging/THREAD_ID/messages \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Idempotency-Key: map-THREAD_ID" \
  -H "Content-Type: application/json" \
  -d '{"mediaUrl": "https://cdn.example.com/parking-map.jpg", "message": "Here is the parking map."}'
AI