Docs/Channels/Airbnb

Manage the Airbnb Photo Tour

Own the photo tour end to end: upload photos, caption them, set the order, choose the cover, and file each one under a room. Every write goes to Airbnb and updates Repull's copy, so your next read returns what you just wrote.

PATCH/v1/channels/airbnb/listings/:id/photos

Parameters

idpathRequired

The Repull listing id (not the Airbnb listing id).

photo_idstringRequired

On PATCH /photos and PUT /photos/cover: the Airbnb-side photo id — the photoAirbnbId field of GET /photos.

captionstring

On PATCH /photos: the new caption, 500 characters or fewer. null clears it.

sort_orderinteger

On PATCH /photos: position in the tour, from 1 up. Relative, not an address — lower sorts earlier. To move several photos, use PUT /photos/order instead.

room_idstring

On PATCH /photos: the Airbnb room id from GET /rooms to file the photo under. null detaches it.

photo_idsarray

On PUT /photos/order: the Airbnb photo ids in display order, first photo first.

API sync is authorised per listing

Airbnb turns API sync on one listing at a time, not once for the account. Each listing carries its own sync category, and a listing whose category is none is closed to the API — Airbnb refuses every write to it even though the account is connected and every other listing on it writes fine. Reconnecting the Airbnb account does not change a listing's sync category. Only someone with access to the listing on Airbnb can turn sync on for it, on the listing itself.

  • sync_all — Repull manages content, rates and availability. Every write on this page works.
  • sync_rates_and_availability — Repull manages rates and availability; listing content is managed by the host on Airbnb.
  • none — the listing is not connected to Repull on Airbnb at all. Every write returns 403 listing_not_api_connected and nothing is sent to Airbnb, so nothing is partially applied.
  • To tell which is which, read GET /v1/channels/airbnb/listings: every entry of each listing's connections[] carries syncCategory (one of the three above, or null if it has never been reported) and writable — false exactly when syncCategory is none. Check writable before a write and you never send one that cannot land. Full detail: https://repull.dev/docs/errors/listing_not_api_connected

The workflow

Five endpoints, in the order you use them. All of them take the Repull listing id in the path and go to Airbnb upstream.

  • {id} is the Repull listing id — from GET /v1/properties or GET /v1/channels/airbnb/listings — not the Airbnb listing id. Repull translates it before calling Airbnb.
  • Upload — POST /v1/channels/airbnb/listings/{id}/photos, body {"photos": [{"image": "<base64>"}]}. Up to 20 per request.
  • Caption, position, room — PATCH /v1/channels/airbnb/listings/{id}/photos, body {"photo_id": "...", "caption": "...", "sort_order": 3, "room_id": "..."}. Send only the fields you want to change; at least one is required.
  • Order the whole tour — PUT /v1/channels/airbnb/listings/{id}/photos/order, body {"photo_ids": ["...", "..."]}. One call instead of one per photo.
  • Cover — PUT /v1/channels/airbnb/listings/{id}/photos/cover, body {"photo_id": "..."}. Also repoints the listing thumbnail.
  • Remove — DELETE /v1/channels/airbnb/listings/{id}/photos?photoId=....
  • Read back — GET /v1/channels/airbnb/listings/{id}/photos returns the tour in display order, each photo with photoAirbnbId, caption, sortOrder, roomAirbnbId, category and its CDN urls.

Uploading

image is base64 image data, not a url. A data:image/jpeg;base64, prefix is accepted and stripped; the decoded image must be under 25 MB. (This endpoint was previously documented as accepting public image urls that Airbnb would fetch. It never did — a url arrived at Airbnb as a ~60-byte image. If you have working upload code that sends url, it has not been uploading photos.)

  • Airbnb assigns the photo id and the CDN urls, so a freshly uploaded photo appears in GET /photos after the next sync, not instantly. Everything else on this page is visible immediately.
  • sort_order, caption, room_id, category and amenity can all be set on upload, so a fresh tour can be uploaded in order in one pass.
  • listing_id inside a photo object is accepted and ignored — the listing comes from the path. That is deliberate: a Repull id sent there would otherwise reach Airbnb and be refused.
# Upload two photos, already captioned and ordered
curl -X POST https://api.repull.dev/v1/channels/airbnb/listings/4118/photos \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "photos": [
      {"image": "'"$(base64 -w0 living-room.jpg)"'", "caption": "Living room", "sort_order": 1},
      {"image": "'"$(base64 -w0 deck.jpg)"'", "caption": "Deck at sunset", "sort_order": 2}
    ]
  }'

Ordering the tour

Airbnb has no bulk photo endpoint — order is one sort_order write per photo. PUT /photos/order does that loop for you, and because it runs server-side it can validate the whole order first and tell you exactly what landed if Airbnb refuses part-way.

  • Send the ids in the order you want them shown. Ids you leave out keep their current relative order behind the ones you list, so moving one photo to the front is {"photo_ids": ["<id>"]}.
  • Everything is validated before anything is written. A duplicate id, an id that is not on this listing, an inactive listing or a missing connection all fail with zero upstream writes.
  • Only photos whose position actually changes are written. Re-sending the order you already have writes nothing and returns unchanged equal to the tour size.
  • On the first upstream failure the run stops — nothing after it is attempted, so the tour cannot drift further while you read the error. The error body carries applied, failed_photo_id and not_attempted.
  • The operation is idempotent: once the cause is fixed, send the identical body again to finish the run.
  • Validation is against Repull's copy of the tour, so a listing whose photos have never synced returns 404. Use PATCH /photos — which needs no cached tour — until the first sync lands.
# Put the deck photo first, then the kitchen; everything else follows
curl -X PUT https://api.repull.dev/v1/channels/airbnb/listings/4118/photos/order \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"photo_ids": ["1583920174", "1583920188"]}'

# 422 — one id is not on this listing. Nothing was reordered.
# {
#   "error": {
#     "code": "invalid_params",
#     "message": "Photo 99 is not on listing 4118. Nothing was reordered.",
#     "field": "photo_ids",
#     "value_received": ["99"]
#   }
# }

# 422 — Airbnb refused the third write. The first two landed.
# {
#   "error": {
#     "code": "airbnb_rejected",
#     "message": "<Airbnb's own reason>",
#     "applied": [{"photo_id": "1583920174", "sort_order": 1},
#                 {"photo_id": "1583920188", "sort_order": 2}],
#     "failed_photo_id": "1583920190",
#     "not_attempted": ["1583920191"]
#   }
# }

Choosing the cover

Airbnb has no "cover" field — the cover is simply the first photo of the tour, so PUT /photos/cover is a position write with one extra effect: it repoints the listing thumbnail, which is what every list view in Repull renders. Without that, a cover change would be visible only inside the photo tour.

  • Usually a single upstream request: the chosen photo takes a position below the current first one and nothing else moves.
  • When the tour already starts at position 1 and there is no room below it, the tour is renumbered instead — one request per photo whose position actually changes, with the same partial-failure reporting as PUT /photos/order.
  • A photo that is already the cover writes nothing and returns applied: [].
  • The photo must already be in Repull's copy of the tour; one uploaded since the last sync returns 404. PATCH /photos can set its position in the meantime.
curl -X PUT https://api.repull.dev/v1/channels/airbnb/listings/4118/photos/cover \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"photo_id": "1583920174"}'

Filing photos under rooms

A photo can belong to a room, which is what drives Airbnb's per-room galleries. Pass the Airbnb room id from GET /v1/channels/airbnb/listings/{id}/rooms as room_id, or null to detach it. Repull records the assignment on both the Airbnb view of the photo and the platform-neutral one, so the room is visible wherever you read photos — see the Rooms page for creating the rooms themselves.

# File a photo under the second bedroom
curl -X PATCH https://api.repull.dev/v1/channels/airbnb/listings/4118/photos \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"photo_id": "1583920174", "room_id": "824113"}'

# Detach it again
curl -X PATCH https://api.repull.dev/v1/channels/airbnb/listings/4118/photos \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"photo_id": "1583920174", "room_id": null}'

What Airbnb will not let you do

  • A listing cannot lose its last photo. DELETE on the only remaining photo is refused. Upload the replacement first, then delete.
  • Ids and urls are Airbnb's. You cannot choose a photo id or a CDN url; both are assigned on upload and appear after the next sync.
  • `sort_order` is a sort key, not an address. Live tours run to eight-digit positions with gaps. Read the order back from GET /photos rather than assuming 1..n — except right after PUT /photos/order, which renumbers the tour densely from 1.
  • `reviewStatus` and `verificationStatus` are Airbnb's moderation fields, returned on each photo for completeness. Repull does not currently receive values for them on the sync, so today they are empty on every photo. Treat them as informational, never as a gate — do not block your own flow on them.
  • Photos are addressed by photo id upstream, with no listing attached, so Repull proves the photo belongs to the listing in your path before sending anything. A photo id from one of your other listings returns 404 — the same answer a photo that does not exist gets.

Errors

A successful write returns 200 (201 for an upload).

  • 422 invalid_params — the body failed validation before anything was sent to Airbnb. field names the field (for example operations.0.max_nights), value_received echoes it and fix says what to send.
  • 422 airbnb_rejected — Airbnb refused the change. message carries Airbnb's own reason. Correct the request; sending the same body again is refused again.
  • 403 listing_not_api_connected — Airbnb API sync is off for this listing (its sync category is none), so Airbnb accepts no writes to it and nothing was sent. Reconnecting the Airbnb account does not fix this — the host has to turn API sync on for that one listing inside Airbnb. See the section above, and https://repull.dev/docs/errors/listing_not_api_connected
  • 403 connection_reauth_required — the host's Airbnb authorization expired or was revoked, or Airbnb refused to refresh it. This takes the whole account down, not one listing. Retrying will not help. Reconnect Airbnb at https://repull.dev/dashboard/connections (or POST /v1/connect/airbnb), then retry.
  • 403 listing_inactive — the listing is inactive. Activate it with PATCH /v1/listings/{id} and {"active": true}, then retry.
  • 404 not_found — no Airbnb-connected listing with this id in your workspace. Check the id is the Repull listing id. 404 no_connection — the workspace has no Airbnb connection at all.
  • 429 airbnb_rate_limited — Airbnb is rate-limiting writes for this host. Wait retry_after seconds when present, otherwise back off exponentially, and put many dates in one operations array instead of one call per date.
  • 502 airbnb_error — Airbnb had an outage or timed out. Nothing about the request needs to change: retry with backoff.

Example

# Caption a photo and move it to third
curl -X PATCH https://api.repull.dev/v1/channels/airbnb/listings/4118/photos \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"photo_id": "1583920174", "caption": "Deck at sunset", "sort_order": 3}'

# Reorder the whole tour in one call
curl -X PUT https://api.repull.dev/v1/channels/airbnb/listings/4118/photos/order \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"photo_ids": ["1583920174", "1583920188", "1583920190"]}'

# Make one of them the cover
curl -X PUT https://api.repull.dev/v1/channels/airbnb/listings/4118/photos/cover \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"photo_id": "1583920188"}'

# Remove a photo
curl -X DELETE 'https://api.repull.dev/v1/channels/airbnb/listings/4118/photos?photoId=1583920190' \
  -H "Authorization: Bearer sk_live_YOUR_KEY"

Response

// 200 — PATCH /photos
{
  "data": {
    "id": "1583920174",
    "listingId": "22616426",
    "caption": "Deck at sunset",
    "sortOrder": 3,
    "category": "LISTING"
  },
  "stored": true
}

// 200 — PUT /photos/order
{
  "data": {
    "order": [
      { "photoId": "1583920174", "sortOrder": 1 },
      { "photoId": "1583920188", "sortOrder": 2 },
      { "photoId": "1583920190", "sortOrder": 3 }
    ],
    "applied": [
      { "photoId": "1583920174", "sortOrder": 1 },
      { "photoId": "1583920190", "sortOrder": 3 }
    ],
    "unchanged": 1
  },
  "stored": true
}
AI