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.
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 get403.
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 param | Type | Description |
|---|---|---|
| limit | integer | Listings per page, 1–100. Default 50. |
| offset | integer | Number of listings to skip. Default 0. |
| location | string | Location slug, e.g. canggu or uluwatu. See reference values. |
| type | string | off-plan, ready, renovation-project. |
| contract | string | leasehold or freehold. |
| minPriceUsd | integer | Lowest asking price in USD. |
| maxPriceUsd | integer | Highest asking price in USD. |
| bedrooms | integer | Minimum number of bedrooms. |
curl "https://www.thebalihomes.com/api/partner/v1/listings?location=canggu&limit=50" \ -H "Authorization: Bearer $TBH_API_KEY"
{
"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.
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.
| Field | Type | Description |
|---|---|---|
| updated | Listing[] | 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. |
| removed | string[] | KES codes that are no longer available: sold, withdrawn, unpublished or deleted. Take them off your site. |
| nextSince | string | The server time of this response. Pass it as since on your next call. |
curl "https://www.thebalihomes.com/api/partner/v1/listings/changes?since=2026-10-23T08:00:00Z" \ -H "Authorization: Bearer $TBH_API_KEY"
{
"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.
{
"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.
| Field | Type | Description |
|---|---|---|
| code | string | KES code, the permanent ID. Example KES539. |
| title | string | Listing headline. |
| propertyType | string | off-plan, ready, renovation-project. |
| contractType | string | leasehold or freehold. |
| leaseholdYears | string | null | Lease term for leasehold listings, as written by the seller, e.g. "25 + 25". |
| location | string | null | Location slug. See /meta for display names. |
| locationDetail | string | null | Area or street-level detail, e.g. "Pererenan". |
| latitude, longitude | number | null | Approximate coordinates. |
| priceUsd | integer | null | Asking price in US dollars. |
| priceIdr | integer | null | Asking price in Indonesian rupiah, when the seller set one. |
| bedrooms | integer | null | |
| bathrooms | integer | null | |
| landSizeM2 | integer | null | Land area in square metres. |
| buildSizeM2 | integer | null | Built area in square metres. |
| pool | string | null | Pool description, e.g. "Private". |
| parkingLots | integer | null | |
| furniture | string | null | Furnished status. |
| deliveryDate | string | null | Expected handover for off-plan listings. |
| shortDescription | string | null | One or two sentence summary, plain text. |
| body | string | null | Full description as HTML. Sanitise it before you render it. |
| featuredImage | string | null | Main photo URL. |
| thumbnail | string | null | Smaller photo URL for cards. |
| gallery | string[] | All photo URLs, in display order. |
| url | string | The listing page on thebalihomes.com. |
| publishedAt | string | null | When the listing first went live, ISO 8601 UTC. |
| updatedAt | string | null | Last edit, ISO 8601 UTC. |
{
"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
- Note the current time, then download the full catalogue by paging through
/listings. - Every 15–60 minutes, call
/listings/changeswith that time assince. StorenextSincefor the next run. - Upsert every listing in
updated, usingcodeas the key. Remove every code inremoved. - 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:
| Header | Description |
|---|---|
| x-ratelimit-limit | Requests allowed per minute. |
| x-ratelimit-remaining | Requests left in the current minute. |
| retry-after | On 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.
{
"error": {
"code": "listing_not_found",
"message": "No available listing with code KES999."
}
}| Status | Code | When |
|---|---|---|
| 400 | invalid_parameter | A query parameter is invalid, e.g. an unknown type or a bad since timestamp. |
| 401 | missing_key / invalid_key | The Authorization header is missing or the key is not recognised. |
| 403 | revoked_key | The key has been revoked. |
| 404 | listing_not_found | The listing does not exist or is no longer available. |
| 405 | The method is not GET. The API is read only. | |
| 429 | rate_limited | Rate limit reached. Wait for the seconds in retry-after. |
| 5xx | server_error / unavailable | Something went wrong on our side. Retry with exponential backoff. |