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).
/v1/channels/airbnb/messaging/:threadId/messagesParameters
messagestringThe text to send. Required unless mediaUrl is set; with mediaUrl it follows the file as a separate message.
mediaUrlstringPublic 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.
mediaTypestringOptional 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 andmessage, if any, follows as a second message. - The thread must already be synced to Repull (
GET /v1/conversationslists them); otherwise404 not_found. - A send with
mediaUrlanswers201with the same response asPOST /v1/conversations/{id}/messages: the stored file inattachmentsand one entry per Airbnb message inparts. 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), andmessage_partially_sentwhen the file arrived and the text did not. - For several files in one request, use
POST /v1/conversations/{id}/messageswithattachments. - Reading a thread (
GET /v1/channels/airbnb/messaging/{threadId}/messages) returns each message with itsattachments, inbound and outbound: 50 per page newest first, or?all=truefor 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: truemeans 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: falsewithreason: "partial_account_staleness"means some accounts are fine and some are not. The response is usable; readaccounts[]to see which rows to distrust. Thereasonis emitted deliberately even thoughstaleisfalse, so a consumer reading only the aggregate is never told everything is fine while one account is down. - Top-level
stale: falsewith noreasonmeans every account is current. - With
?account_id=,accounts[]holds exactly that account and the top-level fields mirror it. reasonis one ofhost_disconnected_since_<iso>,host_disconnected,host_not_activated,sync_lag_>_24h,never_synced, orpartial_account_staleness.fixUrlis 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."}'