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
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.
| Section | Fields |
|---|---|
details | Title, internal nickname, property type group and category, room type, capacity, bedrooms, beds, bathrooms, Wi-Fi network and password, house manual, quiet hours |
description | Name, summary, space, access, interaction, neighbourhood, transit, notes, house rules — primary locale only |
amenities | The amenity set, mapped onto Airbnb's writable enum |
rooms | Rooms and the beds in each |
policies | Cancellation policy, guest controls (children, infants, pets, smoking, events), check-in and check-out times |
photos | The photo set, in order |
pricing | Nightly and weekend price, extra-guest price, cleaning fee, security deposit, weekly and monthly discounts |
checkout_tasks | Checkout 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.
| What | Endpoint |
|---|---|
| Descriptions in any other language | PUT /v1/channels/airbnb/listings/{id}/descriptions |
| Guest-safety disclosures | PUT /v1/channels/airbnb/listings/{id}/safety-disclosures |
| Check-in method | PUT /v1/channels/airbnb/listings/{id}/details |
| Permits, licences, registration numbers | PUT /v1/channels/airbnb/listings/{id}/permits |
| Calendar: rates, availability, min/max stay | PUT /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
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"]
}
}code | What it means | What to do |
|---|---|---|
locked | Airbnb will not change these fields on this listing | Nothing, programmatically. Surface it to the host. |
no_content | There was nothing canonical to send for that section | Write the content, then publish again. |
rejected | Airbnb refused the section as sent; message is Airbnb's own reason | Fix 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
| Status | Code | Meaning |
|---|---|---|
| 422 | invalid_params | Your request never left us. The response names the field, what you sent, and what is accepted. |
| 422 | airbnb_rejected | Airbnb refused the change as sent, and message is their reason. Resending the same body will be refused again. |
| 403 | connection_reauth_required | The Airbnb authorization expired, was revoked, or never covered this listing — Airbnb authorizes writes per listing. Reconnect and include the listing. |
| 403 | listing_inactive | The listing is inactive in Repull. It keeps syncing but cannot be read or written through the API until you activate it. |
| 429 | airbnb_rate_limited | Airbnb is throttling writes for this host. Honour retry_after when present. |
| 502 | airbnb_error | Airbnb 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 back | Call |
|---|---|
| Descriptions, per locale | GET /v1/channels/airbnb/listings/{id}/descriptions?locale=it |
| Which locales exist | GET /v1/channels/airbnb/listings/{id}/settings?type=locales |
| Property type, check-in method, locked fields, live/unlisted | GET /v1/channels/airbnb/listings/{id}/details |
| Guest-safety disclosures | GET /v1/channels/airbnb/listings/{id}/safety-disclosures |
| Permits, and the questions behind them | GET /v1/channels/airbnb/listings/{id}/permits?source=live |
| When each section last pushed, and what is still dirty | GET /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
| Deactivate in Repull | Unlist on Airbnb | |
|---|---|---|
| Call | DELETE /v1/listings/{id}, PATCH with {"active":false}, or the delete action | POST /v1/channels/airbnb/listings/{id} with {"action":"unlist","airbnbConnectionId":77} |
| The guest-facing listing | Stays live and keeps taking bookings | Goes down and stops taking bookings |
| Billing and plan limits | No longer billed, no longer counts toward the cap | Unchanged |
| API access to the listing | 403 listing_inactive until reactivated | Unchanged — you can still read and write it |
| Reverse it with | PATCH /v1/listings/{id} {"active":true} | {"action":"relist"} |
| Data kept | Yes, and it keeps syncing | Yes |
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
- Active & inactive listings — what deactivating does, in full
- Idempotency — the header, everywhere it applies
- Listing content & details — reading the content you publish
- Errors — the full error catalogue