Creating a listing

How to get a property into Repull with a full set of content — address, occupancy, policies, amenities, photos — and how to tell, before you publish anything, whether a channel will accept it. Nothing on this page touches Airbnb or Booking.com.

The shortest create that works

name is the only required field. Everything else is optional and can be filled in later.

curl -X POST https://api.repull.dev/v1/listings \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Sunset Loft #2"}'

{ "id": "48211" }

The response is 201 with the new listing id. It is a string— every id in this API is a string, including the ones that look like integers. Keep it as one; parsing it into a number is the one thing that will go wrong quietly.

A create can be refused with 402 listings_limit_exceeded when the workspace is already at its plan's listing count. Deactivating a listing you no longer need frees a slot immediately — see active & inactive listings.

Everything you can set at create

Sending all of it in one call is worth doing when you have it — a listing created with a complete address and occupancy is publishable straight away, and one created with only a name is not.

GroupFields
Identityname (required), propertyType
Addressstreet, city, state, postalCode (alias zipcode), countryCode, lat, lng
Occupancybedrooms, bathrooms, beds, personCapacity
Copysummary, description
MoneydefaultDailyPrice, cleaningFee
PoliciescancellationPolicy, checkInTimeStart, checkOutTime, allowsPets, allowsSmoking, allowsChildren, allowsEvents
curl -X POST https://api.repull.dev/v1/listings \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "Sunset Loft #2",
        "propertyType": "apartment",
        "street": "1200 Ocean Drive",
        "city": "Miami Beach",
        "state": "FL",
        "postalCode": "33139",
        "countryCode": "US",
        "lat": 25.7805,
        "lng": -80.1300,
        "bedrooms": 2,
        "beds": 3,
        "bathrooms": 1.5,
        "personCapacity": 4,
        "defaultDailyPrice": 240,
        "cleaningFee": 85,
        "cancellationPolicy": "moderate",
        "checkInTimeStart": "15:00",
        "checkOutTime": "11:00",
        "allowsPets": false
      }'

The address rule

This is the part of a listing that decides whether it can ever go live, and it is the part people leave for later. Coordinates are not enough on their own: Airbnb refuses to create a listing without a real postal address behind them, and the refusal happens at publish time, long after you thought the listing was finished.

  • Always: street and city.
  • For a US property, additionally: state and the postal code.
  • Always send countryCode for a property outside the US.Omitting it does not leave the country blank — the listing behaves as a US one, and then fails the US rules above for a property that is nowhere near the US.
  • lat and lng are required for an Airbnb publish as well, alongside the postal fields rather than instead of them.

state and postalCode are settable on both endpoints

They are accepted on POST /v1/listings and in the address block of PUT /v1/listings/{id}/content. If you built against an earlier version of this API and worked around their absence — or concluded a US listing could not be created through the API at all — that workaround can come out.

Correcting the address on a Repull listing is always allowed and is what a later create reads. It does not move a listing that is already live on a channel: on Airbnb the address is settled when the listing is created there and is not an API operation afterwards — see creation-only fields.

Writing the rest of the content

PUT /v1/listings/{id}/contentis the full content write. It takes named sections — title (alias name), description, summary, address, occupancy, details, policies, amenities, photos — plus locale and photosMode. Every one of them is optional.

curl -X PUT https://api.repull.dev/v1/listings/48211/content \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "title": "Sunset Loft #2 — ocean block, quiet street",
        "summary": "Bright two-bedroom a block from the sand.",
        "address": { "state": "FL", "postalCode": "33139" },
        "occupancy": { "maxGuests": 4, "bedrooms": 2, "beds": 3, "bathrooms": 1.5 },
        "details": { "propertyTypeCategory": "apartment", "roomTypeCategory": "entire_home" },
        "policies": { "checkInTimeStart": "15", "checkOutTime": "11", "quietHoursStart": "22:00" }
      }'

{
  "id": "48211",
  "changed": ["title", "summary", "address", "occupancy", "details", "policies"],
  "deferred": []
}

changednames the sections that were actually written — read it rather than assuming the whole body applied. deferred names anything you sent that was accepted but not stored, which in practice means a photos array that carried no usable image URL. A section in deferred left the previous value untouched.

Merge or replace — the distinction that costs data

Most sections merge at field level: send {"occupancy": {"beds": 3}} and the bedroom count you set earlier survives. Three things do not work that way.

SectionBehaviour
amenitiesFull replacement. Send the complete set every time. Omit it and the existing amenities are untouched; send [] and they are cleared.
photosFull replacement, unless you pass "photosMode": "append". Omit to leave the set alone; [] clears it.
policies.guestSafetyDisclosuresFull replacement of the disclosure set, on the same terms.

A partial amenity write is an amenity wipe

Sending {"amenities": ["wifi"]} to add Wi-Fi to a listing that already has twenty amenities leaves it with one. Read the current set from GET /v1/listings/{id}?include=amenities, add to it, and send the whole thing back.

What locale actually covers

locale says which language the copy in this request is written in, and content is stored per locale so each language keeps its own row. It is narrower than it looks, and this is the part that surprises people.

Only four things are stored per locale: title (and its alias name), description, summary, and policies.houseRules. Everything else — address, occupancy, details, amenities, photos, the rest of policies— is language-agnostic and is written regardless of the locale you send.

An Italian title also overwrites the listing name

titleis stored twice: once in the per-locale row, and once as the listing's own language-free name. So {"locale":"it","title":"Loft sul mare"} writes the Italian row andrenames the listing. If you are backfilling translations, send every language's title first and your primary language last, or leave title out of the translation writes entirely and send only description, summary and policies.houseRules.
# Italian copy, without touching the listing's language-free name
curl -X PUT https://api.repull.dev/v1/listings/48211/content \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "locale": "it",
        "summary": "Luminoso bilocale a un isolato dalla spiaggia.",
        "policies": { "houseRules": "Vietato fumare. Silenzio dalle 22." }
      }'

This write does not reach a channel

PUT /v1/listings/{id}/contentwrites Repull's own copy of the listing and nothing else. No part of it is forwarded to Airbnb or Booking.com. What it does do is record which sections changed, so that a later publish knows what to send. Distribution is always a separate, explicit call — see publishing a listing.

Photos

If your images are already hosted somewhere public, put the URLs straight into the photosarray on the content write. If they are files on a device or a server of your own, mint an upload URL and send the bytes directly to storage — the file never passes through this API, so a 40 MB photo is not a 40 MB request body.

# 1. Mint a short-lived signed target
curl -X POST https://api.repull.dev/v1/listings/48211/photos/upload-url \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"fileName": "living-room.jpg", "fileType": "image/jpeg", "fileSize": 2184010}'

{
  "listingId": "48211",
  "uploadUrl": "https://storage.example.com/upload/...",
  "token": "...",
  "path": "listings/48211/living-room.jpg",
  "publicUrl": "https://cdn.repull.dev/listings/48211/living-room.jpg",
  "expiresIn": 3600
}

# 2. PUT the raw bytes to uploadUrl — not to this API
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: image/jpeg" \
  --data-binary @living-room.jpg

# 3. Attach publicUrl to the listing, keeping the photos you already had
curl -X PUT https://api.repull.dev/v1/listings/48211/content \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "photosMode": "append",
        "photos": [
          { "url": "https://cdn.repull.dev/listings/48211/living-room.jpg",
            "caption": "Living room" }
        ]
      }'

GET /v1/listings/{id}/photos lists what is stored and DELETE /v1/listings/{id}/photos removes one by the path the upload returned. expiresIn is in seconds; if the upload did not happen in time, mint another URL rather than retrying the old one.

Letting Repull write the copy

POST /v1/listings/{id}/generate-content produces a title, summary, description and amenity list from what the listing already knows. Pass reference photo URLs to ground the copy in the actual rooms.

curl -X POST https://api.repull.dev/v1/listings/48211/generate-content \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "style": "professional",
        "persist": false,
        "photos": ["https://cdn.repull.dev/listings/48211/living-room.jpg"]
      }'

style is warm, professional or concise. persist defaults to true and saves the result onto the listing; send false to get the draft back and show it to a human first, which is what you want if the listing already has copy someone wrote.

Check it is publishable before you publish

A publish that fails on a missing address field is a slow way to learn something you could have asked for. GET /v1/listings/{id}/publish-status answers it directly: addressReadiness is keyed by channel and names the exact fields that are missing.

curl -s https://api.repull.dev/v1/listings/48211/publish-status \
  -H "Authorization: Bearer sk_live_YOUR_KEY"

{
  "listingId": "48211",
  "addressReadiness": {
    "airbnb": {
      "ready": false,
      "missing": ["state", "postalCode"],
      "have": "street=1201 Ocean Drive, city=Miami Beach, state=∅, postalCode=∅, country=US"
    }
  },
  "channels": [],
  "connections": []
}

missing is the list to put in front of whoever can fill it in, field by field. Write the missing fields with the address block on the content write, read publish-status again, and only then publish.

Empty channels does not mean not connected

channels is sync activity and stays empty until the first push. Connection state is in connections, which is populated as soon as a channel is linked. A listing that is connected but has never been published shows an empty channels and a populated connections.

Next

The listing now exists, has content, and reports itself ready. Putting it on the market is a separate job with separate rules per channel:

AI