Docs/Channels/Airbnb

The Airbnb publication contract

What a publish changes on a live Airbnb listing, what it cannot change, what you find out afterwards, and how to read back what actually took effect. Two-way sync is only trustworthy if the write half is honest about its limits — so this page is mostly about limits.

Two rules that explain almost everything

1. A publish is not one call to Airbnb.It is up to eight independent ones — details, description, amenities, rooms, policies, photos, pricing, checkout tasks — and each can fail on its own. A partial publish is normal, and it is not rolled back: the sections that landed stay applied. That is why the result is reported per section rather than as one boolean.

2. A 200 does not always mean the change was applied. Airbnb locks host-managed fields on established listings. A write to a locked field returns 200, names the field as locked in the response, and applies nothing. 1,180 of the 5,917 Airbnb listings synced through Repull carry at least one locked attribute, so this is the common case, not an edge case.

Never infer success from the status code alone

Every content write returns blockedFields, and every publish returns result.errors[] and result.lockedFields. An empty blockedFields and published: trueare what “it worked” looks like.

What a publish pushes to an existing listing

POST /v1/listings/{id}/publish/airbnb sends your canonical content. By default it sends only the sections that changed since the last successful publish; pass force: true to re-send everything.

SectionFields
detailsTitle, internal nickname, property type group and category, room type, capacity, bedrooms, beds, bathrooms, Wi-Fi network and password, house manual, quiet hours
descriptionName, summary, space, access, interaction, neighbourhood, transit, notes, house rules — primary locale only
amenitiesThe amenity set, mapped onto Airbnb's writable enum
roomsRooms and the beds in each
policiesCancellation policy, guest controls (children, infants, pets, smoking, events), check-in and check-out times
photosThe photo set, in order
pricingNightly and weekend price, extra-guest price, cleaning fee, security deposit, weekly and monthly discounts
checkout_tasksCheckout tasks and their instructions

What a publish does NOT carry — and where to send it instead

These are real Airbnb fields with their own endpoints. They are not part of a publish because each one is a deliberate act with its own blast radius — a compliance filing, a safety statement, a change to how a guest gets in.

WhatEndpoint
Descriptions in any other languagePUT /v1/channels/airbnb/listings/{id}/descriptions
Guest-safety disclosuresPUT /v1/channels/airbnb/listings/{id}/safety-disclosures
Check-in methodPUT /v1/channels/airbnb/listings/{id}/details
Permits, licences, registration numbersPUT /v1/channels/airbnb/listings/{id}/permits
Calendar: rates, availability, min/max stayPUT /v1/channels/airbnb/listings/{id}/availability

Creation-only fields

Two things are settled when the Airbnb listing is created and are not part of any later publish:

  • The postal address.Airbnb will not accept coordinates that are not backed by a full postal address, so a create is refused outright when street, city or country is missing — and the response names the missing fields rather than creating a listing at a default location. Moving a listing to a different address afterwards is not an API operation.
  • The host account. A listing belongs to the Airbnb account it was created under. Re-homing it means creating a new listing.

Property type, room type and the title are notcreation-only — they have an update path (PUT …/details, and the canonical details block on PUT /v1/listings/{id}/content). But Airbnb frequently locks them on an established listing, which is the next section.

Fields Airbnb refuses to change

On an established listing Airbnb treats some content as host-managed and will not accept a change to it through any API. It does not answer with an error: the request returns 200, the response carries the locked attributes, and nothing is applied for them. Commonly locked: the title, the summary and space text, property type category, the check-in option, the address fields, and individual amenities.

You never have to discover this by comparison. Read it up front:

curl -s https://api.repull.dev/v1/channels/airbnb/listings/23900/details \
  -H "Authorization: Bearer sk_live_YOUR_KEY" | jq '.data[0].lockedFields'

[ "name", "property_type_category", "amenities.wifi" ]

The same list rides along on GET /v1/channels/airbnb/listings/{id} as lockedFields on each connection. And every write reports what yourrequest hit — not the whole locked set, only the fields you actually sent:

{
  "listingId": "23900",
  "locale": "en",
  "written": ["notes"],
  "blockedFields": ["summary", "space"],
  "message": "Airbnb has locked these fields on this listing, so the change was NOT applied: summary, space. Airbnb returned success — it accepts the request and ignores the locked fields.",
  "fix": "Airbnb manages this content on an established listing and no API can change it — retrying will keep returning success while changing nothing. Edit the field directly in the Airbnb listing, or send the request again without it."
}

A lock is not a retryable error

There is no backoff, no alternative endpoint and no permission that gets around it. Either the content is edited on Airbnb by the host, or it stays as it is. Treat blockedFields as information for a human, not as a failure to retry.

Per-section results and errors

A publish answers 200 once it has attempted every section. Read result.published for the verdict and result.errors[] for what to do next.

{
  "listingId": "23900",
  "channel": "airbnb",
  "result": {
    "published": false,
    "sections": ["details", "pricing", "photos", "rooms", "policies"],
    "errors": [
      {
        "section": "description",
        "code": "locked",
        "message": "Airbnb has locked these description fields for this listing, so your changes were not applied: summary, space.",
        "lockedFields": ["summary", "space"]
      },
      {
        "section": "amenities",
        "code": "rejected",
        "message": "amenity key JACUZZI is not valid for this listing"
      }
    ],
    "lockedFields": ["summary", "space", "name"]
  }
}
codeWhat it meansWhat to do
lockedAirbnb will not change these fields on this listingNothing, programmatically. Surface it to the host.
no_contentThere was nothing canonical to send for that sectionWrite the content, then publish again.
rejectedAirbnb refused the section as sent; message is Airbnb's own reasonFix the content and publish again.

Re-publishing after a partial failure is safe: a section whose content has not changed is a no-op upstream. You do not need to unpick what already landed.

The single-field writes report the same idea in the shape that suits them: written and blockedFieldsinstead of sections. When Airbnb refuses the request outright, that is not a 200 at all — see error mapping.

Error mapping

StatusCodeMeaning
422invalid_paramsYour request never left us. The response names the field, what you sent, and what is accepted.
422airbnb_rejectedAirbnb refused the change as sent, and message is their reason. Resending the same body will be refused again.
403connection_reauth_requiredThe Airbnb authorization expired, was revoked, or never covered this listing — Airbnb authorizes writes per listing. Reconnect and include the listing.
403listing_inactiveThe listing is inactive in Repull. It keeps syncing but cannot be read or written through the API until you activate it.
429airbnb_rate_limitedAirbnb is throttling writes for this host. Honour retry_after when present.
502airbnb_errorAirbnb did not complete the request — an outage or timeout on their side. Nothing about the request needs to change; retry with backoff.

Idempotency

Every write on this page accepts an Idempotency-Keyheader. Send a unique string per distinct request — a UUID generated where you build the request.

  • First request runs normally, and the response is stored for 24 hours.
  • The same key again replays that stored response, tagged Idempotency-Status: cached. The operation does not run twice.
  • The same key while the first request is still in flight returns 409 idempotency_key_in_use.
  • The same key with a different body returns 422 idempotency_key_reused — almost always one key generated once and reused across a loop.
  • Responses of 500 or above are deliberately not stored, so a server error stays retryable.
curl -X POST https://api.repull.dev/v1/listings/23900/publish/airbnb \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Idempotency-Key: 9f1c2f7e-4a3b-4f2e-9c8d-1b6a0e5d7c31" \
  -H "Content-Type: application/json" \
  -d '{"airbnbConnectionId": 77}'

Idempotency covers the round trip, not the upstream. If a publish partially landed and you replay the key, you get the stored result of that publish — it does not re-attempt the sections that failed. To retry those, publish again with a new key.

Reading back what actually took effect

Repull serves channel reads from a synced mirror of the listing, so a read immediately after a write can still show the previous value — the sync worker has not caught up yet. Every channel read carries dataFreshness so you can tell a stale answer from a current one instead of guessing.

To read backCall
Descriptions, per localeGET /v1/channels/airbnb/listings/{id}/descriptions?locale=it
Which locales existGET /v1/channels/airbnb/listings/{id}/settings?type=locales
Property type, check-in method, locked fields, live/unlistedGET /v1/channels/airbnb/listings/{id}/details
Guest-safety disclosuresGET /v1/channels/airbnb/listings/{id}/safety-disclosures
Permits, and the questions behind themGET /v1/channels/airbnb/listings/{id}/permits?source=live
When each section last pushed, and what is still dirtyGET /v1/listings/{id}/publish-status

The permits read is the one place where a live upstream call is worth asking for: ?source=live returns the questions Airbnb asks for that listing, which you need before you can answer them.

Descriptions in more than one language

Airbnb keeps a separate description per locale, and so does Repull. A publish distributes the primary locale; other languages are written one at a time, which is why the locale is an explicit field and never a guess.

# Canonical copy — Repull's own, distributed by a later publish
curl -X PUT https://api.repull.dev/v1/listings/23900/content \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"locale":"it","summary":"Un appartamento luminoso nel centro"}'

# Straight to the live Airbnb listing, for that locale
curl -X PUT https://api.repull.dev/v1/channels/airbnb/listings/23900/descriptions \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"locale":"it","description":{"summary":"Un appartamento luminoso nel centro"}}'

Unlisting on Airbnb vs deactivating in Repull

These are different operations with different consequences

Deactivating changes what Repull bills and serves. Unlisting takes the property off the market.
Deactivate in RepullUnlist on Airbnb
CallDELETE /v1/listings/{id}, PATCH with {"active":false}, or the delete actionPOST /v1/channels/airbnb/listings/{id} with {"action":"unlist","airbnbConnectionId":77}
The guest-facing listingStays live and keeps taking bookingsGoes down and stops taking bookings
Billing and plan limitsNo longer billed, no longer counts toward the capUnchanged
API access to the listing403 listing_inactive until reactivatedUnchanged — you can still read and write it
Reverse it withPATCH /v1/listings/{id} {"active":true}{"action":"relist"}
Data keptYes, and it keeps syncingYes

unlist requires airbnbConnectionId. A Repull listing can be connected to more than one Airbnb listing — re-lists, or a property moved between host accounts — and taking down the wrong one is not something this API can undo for you. Find the id in GET /v1/channels/airbnb/listings/{id}: each row's id is the connection id.

The unlist is verified, not assumed: after Airbnb accepts the request the listing is read back, and if it is still live you get 422 airbnb_rejected saying so rather than a success. relist re-enables sync and puts the listing back on the market; it does not push content, so publish afterwards if the content changed while it was down.

A safe publish, end to end

# 1. What will Airbnb refuse to change on this listing?
curl -s https://api.repull.dev/v1/channels/airbnb/listings/23900/details \
  -H "Authorization: Bearer sk_live_YOUR_KEY" | jq '.data[0].lockedFields'

# 2. Write your canonical content (local — does not touch Airbnb)
curl -X PUT https://api.repull.dev/v1/listings/23900/content \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "summary": "Bright one-bedroom in the old town",
        "details": { "roomTypeCategory": "entire_home" },
        "policies": { "quietHoursStart": "22:00", "quietHoursEnd": "07:00" }
      }'

# 3. Publish, with a key that makes a retry safe
curl -X POST https://api.repull.dev/v1/listings/23900/publish/airbnb \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"airbnbConnectionId": 77}' | jq '.result'

# 4. Act on the per-section result: 'rejected' and 'no_content' are yours to
#    fix and publish again; 'locked' is for the host to change on Airbnb.

See also

AI