Properties

Properties represent vacation rental listings. Each property has an address, location, pricing, and custom fields. Only active properties are readable. An inactive property is not billed and keeps syncing, but every endpoint answers `403 listing_inactive` for it (and for its reservations, conversations, reviews and availability) until it is activated with `POST /v1/listings/status`. List endpoints leave inactive properties out unless you pass `?status=inactive` or `?status=all`. See https://repull.dev/docs/manage-listings.

Activate or deactivate properties in bulk — the billing and visibility switch.

POST/v1/listings/status

Note

All or nothing: if any id is not yours, nothing changes and the response names it. Activating past your plan limit returns 402 and changes nothing. Works while the account is over its limit, so you can always trim back.

Request

curl -X POST https://api.repull.dev/v1/listings/status \
  -H "Authorization: Bearer $REPULL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"listingIds": ["23900", "23901"], "active": false}'

Response

{
  "active": false,
  "updated": ["23900", "23901"],
  "unchanged": ["23902"]
}

Body Parameters

listingIdsarrayRequired

Between 1 and 500 unique property ids, as integers or digit strings.

activebooleanRequired

`false` stops managing the properties: not billed, not readable through the API, no webhooks. Their data keeps syncing and returns when you activate them again.

The physical rooms under a listing (Mews, Cloudbeds room types)

GET/v1/listings/:id/units

Note

For a hotel-model PMS (Mews, Cloudbeds) a listing is a room type: prices, restrictions and availability are set on the room type, and each reservation is assigned one of these rooms (reservation.unit.id). availability reports availableUnits per night. Any other listing is a single home and returns an empty list. Guides: /docs/pms/mews, /docs/pms/cloudbeds

Request

curl https://api.repull.dev/v1/listings/30594/units \
  -H "Authorization: Bearer $REPULL_API_KEY"

Response

{
  "listingId": "30594",
  "total": 5,
  "data": [
    { "id": "9868b6d9-1e6d-4e85-a64a-b731628a0da2", "name": "101", "active": true, "parentId": null, "housekeepingStatus": "Dirty", "floor": "2", "source": "mews" }
  ]
}

Get the markup each channel adds to a listing's price

GET/v1/listings/:id/markups

Note

A channel's price is the listing's own price plus its markup: 35 = +35%. Airbnb markups are per listing. A Booking.com markup belongs to the property and is shared by every listing on it — `listingIds` names them.

Request

curl https://api.repull.dev/v1/listings/30198/markups \
  -H "Authorization: Bearer $REPULL_API_KEY"

Response

{
  "id": "30198",
  "airbnb": [{ "airbnbId": "1781522409437931174", "markupPercent": 35 }],
  "booking": [{ "hotelId": "17325248", "markupPercent": 18, "listingIds": ["30198"] }]
}

Set a listing's markup on one channel

PUT/v1/listings/:id/markups

Note

When the value changes, the affected listings' prices are re-sent to that channel straight away (`pricesResent`). On Booking.com the markup belongs to the property, so every listing on it is repriced (`affectedListingIds`). A listing on several Booking.com properties without `hotelId` returns 409 `ambiguous_booking_mapping` listing them, rather than repricing a property it guessed.

Request

curl -X PUT https://api.repull.dev/v1/listings/30198/markups \
  -H "Authorization: Bearer $REPULL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"channel": "booking", "markupPercent": 18}'

Response

{
  "id": "30198",
  "channel": "booking",
  "markupPercent": 20,
  "changed": true,
  "hotelId": "17325248",
  "affectedListingIds": ["30198"],
  "pricesResent": true
}

Body Parameters

channelstringRequired

`airbnb` or `booking`

markupPercentnumberRequired

Percent added on that channel: `15` for 15%. `null` removes the markup. A value between 0 and 1 is refused as a probable fraction.

hotelIdstring

Booking.com property — required when the listing is on more than one.

List listings

GET/v1/listings

Note

Cursor-paginated. Each row has the same shape as GET /v1/listings/{id}, which also carries capabilities.reservations for that listing.

Request

curl https://api.repull.dev/v1/listings?limit=50 \
  -H "Authorization: Bearer $REPULL_API_KEY"

Response

{
  "data": [{ "id": "4118", "name": "Lakeview Loft", "active": true }],
  "pagination": { "nextCursor": null, "hasMore": false }
}

Query Parameters

cursorstring

Opaque cursor from the previous response's `pagination.nextCursor`. Omit for the first page.

offsetinteger

Alias for shallow paging (0..10000). Mutually exclusive with `cursor`.

limitinteger

Page size. Hard cap 100.

qstring

Case-insensitive substring search on name, street or city.

statusstring

Defaults to `active`. Pass `inactive` for listings you can activate, `archived` for archived ones.

channelstring

Only listings published on this channel (`airbnb`, `booking`, `vrbo`, …).

includestring

Comma-separated expansions: `content`, `details`, `thumbnail`.

Get a listing, with what the API can do with it

GET/v1/listings/:id

Note

Same shape as one element of GET /v1/listings, plus capabilities.reservations: managedBy (pms or repull), provider, create, modify, cancel, quote, customPrice, notes and verifiedAgainst (sandbox or vendor_docs). A flag is true only when the PMS supports it, the workspace is still connected to the PMS, and the connection write policy allows API bookings. A listing a connected PMS manages also carries capabilities.pms — what else the API writes through that PMS: reservations.respond / preapprove, reviews.read / reply, the listing content sections (listings.title … photoCaptions), guests.create / update, conversations.send / attachments / channelSelect, calendar.write, payments.read, tasks, plus notes. A false flag is a 422 pms_write_unsupported naming the PMS. Returns 403 listing_inactive for an inactive listing. Guides: /docs/reservations/create#capabilities, /docs/pms-writes#capabilities

Request

curl https://api.repull.dev/v1/listings/4118 \
  -H "Authorization: Bearer $REPULL_API_KEY"

Response

{
  "id": "4118",
  "name": "Lakeview Loft",
  "active": true,
  "capabilities": {
      "reservations": {
        "managedBy": "pms",
        "provider": "hostaway",
        "create": true,
        "modify": true,
        "cancel": true,
        "quote": true,
        "customPrice": true,
        "notes": "Direct-channel bookings: create (at a set total, or priced by Hostaway's calculator), quote, change dates, guest count, guest details and notes, cancel. … Verified against Hostaway's API documentation only; not yet run against a live Hostaway account.",
        "verifiedAgainst": "vendor_docs"
      },
      "pms": {
        "provider": "hostaway",
        "connected": true,
        "reservations": { "respond": false, "preapprove": false },
        "reviews": { "read": true, "reply": false },
        "listings": { "title": true, "descriptions": true, "times": true, "capacity": true, "amenities": true, "houseRules": false, "address": true, "photosAdd": true, "photosDelete": true, "photosReorder": true, "photoCaptions": true },
        "guests": { "create": false, "update": false },
        "conversations": { "send": true, "attachments": false, "channelSelect": true },
        "calendar": { "write": true },
        "payments": { "read": false },
        "tasks": { "read": true, "write": true },
        "notes": { "listings": "Guest-facing and internal name, descriptions, check-in/out times (whole hours), … House-rule flags (pets, smoking, events, children) are not writable; …" }
      }
    }
}

Query Parameters

includestring

Comma-separated expansions: `amenities`, `content`, `details`.

List all properties in the account — the older name for GET /v1/listings

GET/v1/properties

Request

curl https://api.repull.dev/v1/properties?limit=50 \
  -H "Authorization: Bearer $REPULL_API_KEY"

Response

{
  "data": [
    {
      "id": "123",
      "name": "Example",
      "address": "Example text",
      "city": "Example text",
      "currency": "USD",
      "status": "active",
      "lifecycleStatus": "live",
      "channels": [],
      "updatedAt": "2026-06-01T12:00:00.000Z"
    }
  ],
  "pagination": {
    "nextCursor": "eyJpZCI6MTIzfQ==",
    "hasMore": true,
    "total": 0
  }
}

Captured from a real response. Regenerated on every release, and CI fails if this shape stops matching the API.

Query Parameters

accountstring

Only one connected account's records, as `provider:externalAccountId` (e.g. `airbnb:79730216`, `booking:10167991`, `vrbo:36`, `hostaway:…`) — the pair on every webhook `account` block, on `connect.session.completed`, and on each record's `account`. See /for-platforms.

limitinteger

Page size (max 100). Requests over the cap return 422.

cursorstring

Opaque cursor returned in the previous response's `pagination.nextCursor`. Omit to fetch the first page.

offsetinteger

First-class alias for cursor-based pagination. Mutually exclusive with `cursor` — passing both returns 422. Accepts integers in `[0, 10000]`; deeper walks must use `cursor` (constant per-page cost). The response always includes `pagination.nextCursor` so consumers can switch from offset → cursor mid-walk for deep pagination without re-keying.

qstring

Case-insensitive substring search on name, street, or city.

statusstring

Filter by status. Default returns active only; pass `inactive` to invert or `all` to include both. Inactive properties carry identity fields only — `id`, `name`, `status`, `lifecycleStatus`, `channels` and `updatedAt` — never `address`, `city` or `currency`.

lifecycle_statusstring

Filter by lifecycle status (e.g. `live`, `draft`, `archived`). Pass `all` to disable the filter.

channelstring

Filter to properties with an active link on the given OTA/channel (airbnb, booking). Omit to include every channel. Each property also returns a `channels` array listing the OTAs it is published on.

updated_sincedate

Incremental sync: return only records whose `updatedAt` is at or after this instant. This is the only filter on record **mutation** time — every `check_*` filter targets guest **stay** dates. **Accepted formats.** ISO 8601, with `Z` or a numeric offset — both work: - `2026-08-01T00:00:00Z` - `2026-08-01T00:00:00.123Z` - `2026-08-01T00:00:00+00:00` - `2026-08-01T02:30:00-07:00` (offset colon optional: `-0700`) - `2026-08-01T00:00` (seconds optional) - `2026-08-01T00:00:00` — no zone designator, interpreted as **UTC** - `2026-08-01` — date only, means midnight UTC Anything else returns 422 `invalid_params` naming the field; the value is never silently ignored. **Ordering changes when you pass this.** Results are ordered `updatedAt ASC, id ASC` (instead of the endpoint default) and the cursor keys on the same pair. That is required for correctness: under the default ordering a record amended mid-walk can move behind the cursor and never be emitted — which is exactly the event you are polling for. Ascending mutation time is monotonic with the cursor, so anything touched during a walk resurfaces later in it or on the next poll. **Cursors are not interchangeable between the two orderings.** Keep `updated_since` on every page of an incremental walk; replaying a cursor from the other ordering returns 422 rather than a page that silently skips rows. **Watermark.** The bound is inclusive (`updatedAt >= value`), so the last row of the final page is the watermark for the next poll — re-polling with it re-emits that row. Delivery is at-least-once; upsert by `id`.

include_totalboolean

When `true` (default), the response's `pagination.total` carries the count of rows matching the current filter, across all pages. Pass `false` to skip the count for very large workspaces where the per-page COUNT(*) cost matters.

Get a single property by ID — the older name for GET /v1/listings/{id}

GET/v1/properties/:id

Request

curl https://api.repull.dev/v1/properties/123 \
  -H "Authorization: Bearer $REPULL_API_KEY"

Response

{
  "id": "123",
  "name": "Example",
  "address": "Example text",
  "city": "Example text",
  "latitude": "Example text",
  "longitude": "Example text",
  "currency": "USD",
  "status": "active",
  "lifecycleStatus": "live",
  "createdAt": "2026-06-01T12:00:00.000Z"
}

Captured from a real response. Regenerated on every release, and CI fails if this shape stops matching the API.

AI