Skip to content

For partner agencies

Partner API

Read-only access to every available villa, apartment and land listing in The Bali Homes catalogue, with full descriptions, specs, prices and photo galleries.

Access
Read only
Data
Available listings
Format
JSON over HTTPS
Listing ID
KES code

The API returns listings and nothing else. A listing appears only while it is available on thebalihomes.com. Once it is sold, withdrawn or unpublished it disappears from the API, and the changes feed tells you so.

All requests go to this base URL:

https://www.thebalihomes.com/api/partner/v1

Authentication

The Bali Homes issues each partner agency an API key. Send it in the Authorization header of every request.

Header
Authorization: Bearer tbh_pk_xxxxxxxxxxxxxxxxxxxxxxxx
  • You see the key once, when it is issued. Store it in a secret manager or environment variable.
  • Call the API from your server only. Keys placed in browser or mobile app code can be copied.
  • Every request is logged against your key. If a key leaks, ask us to revoke it and we will issue a new one.
  • Requests without a valid key get 401. Revoked keys get 403.

List listings

GET/listings

Returns available listings, newest first, one page at a time. Each item is a full listing object, including the description and the complete gallery.

Query paramTypeDescription
limitintegerListings per page, 1–100. Default 50.
offsetintegerNumber of listings to skip. Default 0.
locationstringLocation slug, e.g. canggu or uluwatu. See reference values.
typestringoff-plan, ready, renovation-project.
contractstringleasehold or freehold.
minPriceUsdintegerLowest asking price in USD.
maxPriceUsdintegerHighest asking price in USD.
bedroomsintegerMinimum number of bedrooms.
Request
curl "https://www.thebalihomes.com/api/partner/v1/listings?location=canggu&limit=50" \
  -H "Authorization: Bearer $TBH_API_KEY"
Response · 200
{
  "listings": [ /* listing objects */ ],
  "page": { "limit": 50, "offset": 0, "total": 155 }
}

To download the whole catalogue, increase offset by limit until you have total listings.

Get one listing

GET/listings/{code}

Returns one listing by its KES code, for example KES539. Codes are case-insensitive and stay the same when a listing's title or URL changes. Units inside a development can carry a suffix, such as KES601-2.

Request
curl "https://www.thebalihomes.com/api/partner/v1/listings/KES539" \
  -H "Authorization: Bearer $TBH_API_KEY"

The response is { "listing": { … } }. If the listing is no longer available you get 404. Treat that as removed and take it off your site.

Changes feed

GET/listings/changes?since={timestamp}

Returns everything that changed after since, an ISO 8601 timestamp such as 2026-10-23T08:00:00Z. Use it to keep your copy up to date without downloading the full catalogue again.

FieldTypeDescription
updatedListing[]Listings that became available or were edited since that time (price, photos, description or anything else). Each is a full listing object. Replace your copy with it.
removedstring[]KES codes that are no longer available: sold, withdrawn, unpublished or deleted. Take them off your site.
nextSincestringThe server time of this response. Pass it as since on your next call.
Request
curl "https://www.thebalihomes.com/api/partner/v1/listings/changes?since=2026-10-23T08:00:00Z" \
  -H "Authorization: Bearer $TBH_API_KEY"
Response · 200
{
  "updated": [ /* listing objects */ ],
  "removed": ["KES487", "KES513"],
  "nextSince": "2026-10-23T08:15:02.311Z"
}

Reference values

GET/meta

Returns the values used in listing fields and filters: location slugs with display names and the number of available listings, property types and contract types.

Response · 200
{
  "locations": [
    { "slug": "uluwatu", "name": "Uluwatu", "listings": 61 },
    { "slug": "canggu", "name": "Canggu", "listings": 50 }
  ],
  "propertyTypes": ["off-plan", "ready", "renovation-project"],
  "contractTypes": ["leasehold", "freehold"]
}

Listing object

Fields are null when we don't have the value. Media URLs are absolute and served from media.thebalihomes.com.

FieldTypeDescription
codestringKES code, the permanent ID. Example KES539.
titlestringListing headline.
propertyTypestringoff-plan, ready, renovation-project.
contractTypestringleasehold or freehold.
leaseholdYearsstring | nullLease term for leasehold listings, as written by the seller, e.g. "25 + 25".
locationstring | nullLocation slug. See /meta for display names.
locationDetailstring | nullArea or street-level detail, e.g. "Pererenan".
latitude, longitudenumber | nullApproximate coordinates.
priceUsdinteger | nullAsking price in US dollars.
priceIdrinteger | nullAsking price in Indonesian rupiah, when the seller set one.
bedroomsinteger | null
bathroomsinteger | null
landSizeM2integer | nullLand area in square metres.
buildSizeM2integer | nullBuilt area in square metres.
poolstring | nullPool description, e.g. "Private".
parkingLotsinteger | null
furniturestring | nullFurnished status.
deliveryDatestring | nullExpected handover for off-plan listings.
shortDescriptionstring | nullOne or two sentence summary, plain text.
bodystring | nullFull description as HTML. Sanitise it before you render it.
featuredImagestring | nullMain photo URL.
thumbnailstring | nullSmaller photo URL for cards.
gallerystring[]All photo URLs, in display order.
urlstringThe listing page on thebalihomes.com.
publishedAtstring | nullWhen the listing first went live, ISO 8601 UTC.
updatedAtstring | nullLast edit, ISO 8601 UTC.
Example
{
  "code": "KES539",
  "title": "Three-bedroom villa with rice field views",
  "propertyType": "ready",
  "contractType": "leasehold",
  "leaseholdYears": "27",
  "location": "canggu",
  "locationDetail": "Pererenan",
  "latitude": -8.6412,
  "longitude": 115.1268,
  "priceUsd": 385000,
  "priceIdr": 6300000000,
  "bedrooms": 3,
  "bathrooms": 3,
  "landSizeM2": 300,
  "buildSizeM2": 210,
  "pool": "Private",
  "parkingLots": 1,
  "furniture": "Fully furnished",
  "deliveryDate": null,
  "shortDescription": "Modern tropical villa a short drive from Pererenan beach.",
  "body": "<p>…</p>",
  "featuredImage": "https://media.thebalihomes.com/…/cover.jpg",
  "thumbnail": "https://media.thebalihomes.com/…/thumb.jpg",
  "gallery": ["https://media.thebalihomes.com/…/01.jpg"],
  "url": "https://www.thebalihomes.com/listing/three-bedroom-villa-pererenan",
  "publishedAt": "2026-09-14T03:20:00.000Z",
  "updatedAt": "2026-10-20T11:02:45.000Z"
}

The example shows the format. The listing and its values are illustrative.

Keeping in sync

  1. Note the current time, then download the full catalogue by paging through /listings.
  2. Every 15–60 minutes, call /listings/changes with that time as since. Store nextSince for the next run.
  3. Upsert every listing in updated, using code as the key. Remove every code in removed.
  4. Sync at least once every 24 hours so sold properties come off your site quickly.

Photo URLs can change when a gallery is edited. That edit shows up in the changes feed, so refresh images when a listing is updated.

Rate limits

Each key may make 120 requests per minute. Every response tells you where you stand:

HeaderDescription
x-ratelimit-limitRequests allowed per minute.
x-ratelimit-remainingRequests left in the current minute.
retry-afterOn a 429, seconds to wait before retrying.

A full sync of the catalogue takes a handful of requests, so normal use stays well below the limit. Contact us if you need more.

Errors

Errors use standard HTTP status codes and return a JSON body with a machine-readable code and a message.

Response · 404
{
  "error": {
    "code": "listing_not_found",
    "message": "No available listing with code KES999."
  }
}
StatusCodeWhen
400invalid_parameterA query parameter is invalid, e.g. an unknown type or a bad since timestamp.
401missing_key / invalid_keyThe Authorization header is missing or the key is not recognised.
403revoked_keyThe key has been revoked.
404listing_not_foundThe listing does not exist or is no longer available.
405The method is not GET. The API is read only.
429rate_limitedRate limit reached. Wait for the seconds in retry-after.
5xxserver_error / unavailableSomething went wrong on our side. Retry with exponential backoff.
Partner API v1. Breaking changes are announced at least 90 days in advance and ship under a new version path. Questions or key requests: agency@thebalihomes.com