Arthabase Data API

Bali villa market data for developers: what is for sale, how Airbnbs perform, and nearby sale and rental comparables. Returns Pack bundles comps and market stats into one deal analysis call.

Quick start

Building an agent? See MCP docs for copy-paste server config.

  1. Sign in to Arthabase Studio

    Open studio.arthabase.com.

  2. Create an API key

    Settings → API. Create a key, then copy the secret once.

  3. Try it

bash
export ARTHABASE_API_KEY="ab_live_…"
export API="https://data.arthabase.com"

curl -sS -H "Authorization: Bearer $ARTHABASE_API_KEY" "$API/health"
json · response
{ "ok": true, "service": "arthabase-data-api", "env": "production" }

Auth & errors

Studio credits: Authorization: Bearer ab_live_… debits your IDR wallet.

x402: omit Authorization, pay USDC on Base when you get 402. Discovery: GET /v1/x402. MCP setup: /api/mcp/.

Errors return JSON like { "error": "…" }.

StatusMeaning
401Bad or missing key
403Key not allowed for this route
400Bad request / filters
402x402 payment required, or insufficient wallet credits
404Not found
429Too many calls — respect Retry-After (20/min per API key or x402 IP)
500 / 502 / 503Temporary failure — retry

Which call should I use?

I want…Call
Area name for a map pinPOST /v1/geo/resolve
Sale market trends over monthsPOST /v1/analytics/supply
Airbnb market trends over monthsPOST /v1/analytics/str
Compare a few marketsPOST /v1/analytics/compare
Nearby sale peers onlyPOST /v1/comps/sale
Nearby Airbnb peers onlyPOST /v1/comps/str
Full deal analysis pocketPOST /v1/returns-pack/raw or /elaborated
Browse villas for saleGET /v1/supply/listings
Browse many AirbnbsGET /v1/airbnb/listings
Deep dive one AirbnbGET /v1/airbnb/listings/:id
Airbnb financials for 1–10 idsPOST /v1/airbnb/listings/history

Filters

Shared fields on many POST bodies. Each endpoint lists which it accepts.

Field Plain English Supply STR
location Map pin (lat / lng). Required for geo, comps, Returns Pack. Yes Yes
bedrooms Bed count 1–12. Required for comps / Returns Pack. Yes Yes
contract leasehold or freehold. Yes No
ready_offplan offplan or ready. Yes No
market_id Area slug like canggu. Yes Yes
months History depth for monthly series. Default 12, max 60. Yes Yes
bedrooms_min / bedrooms_max Bedroom range. Analytics and GET listings only (not comps). Yes Yes
sale_price_usd · land_m2 · building_m2 USD / m² ranges as { "min"?, "max"? }. Sale tools. Yes No
adr_usd · occupancy_pct · revpar_usd STR ranges as { "min"?, "max"? }. Also rating, revenue_30d_usd. No Yes

GET listings use flat query params, e.g. sale_price_usd_min=150000.

Markets

Valid market_id values: canggu · uluwatu · mengwi_coast · seminyak · jimbaran · sanur · ubud · kedungu · other

1. Health

GET /health
Example response
json
{ "ok": true, "service": "arthabase-data-api", "env": "production" }

Paid uptime check. Same auth as other catalog calls: API key (Rp 100) or x402 ($0.001). Counts toward the global 20 req/min bucket.

2. Geo resolve

POST /v1/geo/resolve

Send a map pin (lat / lng). Arthabase returns market, village, district, and regency labels. Use this when you only have a click on the map and need a market_id for analytics or other calls.

Filters / params

FieldNotes
location.latRequiredLatitude of the map pin.
location.lngRequiredLongitude of the map pin.
Request
bash
curl -sS -X POST "$API/v1/geo/resolve" \
  -H "Authorization: Bearer $ARTHABASE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "location": { "lat": -8.6472, "lng": 115.1386 } }'
Example response
json
{
  "product": "Geo Resolve",
  "location": { "lat": -8.6472, "lng": 115.1386 },
  "area": {
    "market": "Canggu",
    "village": "Berawa",
    "district": "North Kuta",
    "regency": "Badung"
  }
}

3. Supply analytics

POST /v1/analytics/supply

For-sale market stats for one area: today’s price and days-on-market bands, month-by-month history, plus a short pulse of newly listed similar villas across Bali. Optional filters: bedrooms, contract, off-plan.

Pass filters.market_id or a location pin.

Filters / params

FieldNotes
filters.market_idArea slug (e.g. canggu). Required if no location.
location.lat / lngMap pin. Required if no market_id.
filters.bedrooms1–12.
filters.contractleasehold or freehold.
filters.ready_offplanoffplan or ready.
filters.monthsHistory depth. Default 12, max 60.
Request
bash
curl -sS -X POST "$API/v1/analytics/supply" \
  -H "Authorization: Bearer $ARTHABASE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "filters": {
      "market_id": "canggu",
      "bedrooms": 2,
      "contract": "leasehold",
      "ready_offplan": "offplan",
      "months": 24
    }
  }
Example response
json
{
  "product": "Supply Analytics",
  "currency": "USD",
  "snapshot": {
    "active_count": 84,
    "sale_price_usd": {
      "p10": 120000, "p25": 145000, "median": 178000,
      "p75": 210000, "p90": 265000, "avg": 182400, "count": 84
    },
    "days_on_market": {
      "p10": 12, "p25": 28, "median": 45,
      "p75": 72, "p90": 110, "avg": 51, "count": 84
    }
  },
  "monthly": [
    {
      "month": "2025-09",
      "active_count": 80,
      "new_count": 6,
      "removed_count": 3,
      "sale_price_usd": { "p25": 140000, "median": 175000, "p75": 205000 },
      "days_on_market": { "p25": 25, "median": 44, "p75": 70 }
    }
  ],
  "pulse": {
    "window": "last_4_weeks",
    "newly_listed_total": 42,
    "by_market": [
      { "market_id": "canggu", "label": "Canggu · this area", "count": 8, "share": 0.19 }
    ]
  }
}

4. STR analytics

POST /v1/analytics/str

Airbnb market stats for one area: snapshot and month-by-month bands for ADR, occupancy, RevPAR, and revenue. Optional bedroom filter. Contract and off-plan do not apply.

Filters / params

FieldNotes
filters.market_idArea slug. Required if no location.
location.lat / lngMap pin. Required if no market_id.
filters.bedrooms1–12.
filters.monthsHistory depth. Default 12, max 60.
Request
bash
curl -sS -X POST "$API/v1/analytics/str" \
  -H "Authorization: Bearer $ARTHABASE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "filters": { "market_id": "canggu", "bedrooms": 2, "months": 24 }
  }
Example response
json
{
  "product": "STR Analytics",
  "currency": "USD",
  "snapshot": {
    "listing_count": 210,
    "adr_usd": { "p25": 140, "median": 175, "p75": 220 },
    "occupancy_pct": { "p25": 48, "median": 61, "p75": 72 },
    "revpar_usd": { "median": 107 },
    "revenue_30d_usd": { "median": 3200 },
    "revenue_90d_usd": { "median": 9100 }
  },
  "monthly": [
    {
      "month": "2025-09",
      "listing_count": 198,
      "adr_usd": { "p25": 135, "median": 168, "p75": 210 },
      "occupancy_pct": { "p25": 45, "median": 58, "p75": 70 },
      "revpar_usd": { "median": 97 },
      "revenue_usd": { "median": 2900 }
    }
  ]
}

5. Compare markets

POST /v1/analytics/compare

Runs supply and STR analytics for 2–5 markets with the same filters. Use it to compare areas side by side (for example Canggu vs Uluwatu vs Seminyak).

Filters / params

FieldNotes
market_idsRequiredArray of 2–5 market slugs.
filters.bedrooms1–12. Applied to every market.
filters.contractleasehold or freehold (sale side only).
filters.ready_offplanoffplan or ready (sale side only).
filters.monthsHistory depth. Default 12, max 60.
Request
bash
curl -sS -X POST "$API/v1/analytics/compare" \
  -H "Authorization: Bearer $ARTHABASE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "market_ids": ["canggu", "uluwatu", "seminyak"],
    "filters": { "bedrooms": 2, "contract": "leasehold", "months": 24 }
  }
Example response
json
{
  "product": "Analytics Compare",
  "filters_applied": { "bedrooms": 2, "markets": ["canggu", "uluwatu", "seminyak"] },
  "areas": [
    {
      "market_id": "canggu",
      "supply": {
        "snapshot": { "active_count": 84, "sale_price_usd": { "median": 178000 } },
        "monthly": []
      },
      "str": {
        "snapshot": { "listing_count": 210, "adr_usd": { "median": 175 } },
        "monthly": []
      }
    },
    {
      "market_id": "uluwatu",
      "supply": { "snapshot": {}, "monthly": [] },
      "str": { "snapshot": {}, "monthly": [] }
    }
  ]
}

6. Sale comps

POST /v1/comps/sale

Active for-sale peers around a coordinate. Search starts at 1.5 km and expands to 3 km if fewer than 20 matches. Arthabase returns up to 20 closest listings.

Filters / params

FieldNotes
location.lat / lngRequiredMap pin for the search center.
filters.bedroomsRequired1–12.
filters.contractleasehold or freehold.
filters.ready_offplanoffplan or ready.
Request
bash
curl -sS -X POST "$API/v1/comps/sale" \
  -H "Authorization: Bearer $ARTHABASE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "location": { "lat": -8.6472, "lng": 115.1386 },
    "filters": {
      "bedrooms": 2,
      "contract": "leasehold",
      "ready_offplan": "offplan"
    }
  }
Example response
json
{
  "product": "Sale Comps",
  "currency": "USD",
  "comp_search": {
    "radius_m_used": 1500,
    "expanded": false,
    "count": 14,
    "fixed_count": 20
  },
  "comps": [
    {
      "listing_id": "l9f2a1",
      "listing_url": "https://…",
      "image": null,
      "distance_m": 180,
      "bedrooms": 2,
      "contract": "Leasehold",
      "ready_offplan": "Off-Plan",
      "sale_price_usd": 248000,
      "days_on_market": 42,
      "agency": "Example Agency"
    }
  ]
}

7. STR comps

POST /v1/comps/str

Active Airbnb peers near the pin, with live financials and month history. Same radius rule as sale comps (1.5→3 km, up to 20). Weak listings are dropped (for example occupancy under ~40%, or ADR too low for the bedroom count).

Filters / params

FieldNotes
location.lat / lngRequiredMap pin for the search center.
filters.bedroomsRequired1–12.
Request
bash
curl -sS -X POST "$API/v1/comps/str" \
  -H "Authorization: Bearer $ARTHABASE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "location": { "lat": -8.6472, "lng": 115.1386 },
    "filters": { "bedrooms": 2 }
  }
Example response
json
{
  "product": "STR Comps",
  "currency": "USD",
  "comp_search": { "radius_m_used": 3000, "expanded": true, "count": 20 },
  "comps": [
    {
      "airbnb_id": "676363015807687743",
      "listing_url": "https://airbnb.com/rooms/…",
      "title": "2BR villa near Berawa",
      "image": "https://…",
      "distance_m": 210,
      "bedrooms": 2,
      "financials": {
        "adr_usd": 188,
        "occupancy_pct": 61,
        "revpar_usd": 115,
        "revenue_30d_usd": 3420,
        "revenue_90d_usd": 9800
      },
      "monthly_history": [
        {
          "month": "2025-09",
          "occupancy_pct": 58,
          "adr_usd": 175,
          "revenue_usd": 3045,
          "revpar_usd": 102
        }
      ]
    }
  ]
}

8. Returns Pack

POST /v1/returns-pack/raw
POST /v1/returns-pack/elaborated

One call for a deal pin: nearby sale comps, nearby STR comps (with history), and area supply + STR analytics (month bands). Use raw for the data ingredients, or elaborated when you also want yield scenarios.

ModePriceWhat you get
rawRp 25,000The ingredients only
elaboratedRp 50,000Same + Baseline / Optimal / Elite income, costs, yield (needs subject.sale_price_usd)

Filters / params

FieldNotes
location.lat / lngRequiredMap pin for the deal.
filters.bedroomsRequired1–12.
filters.contractleasehold or freehold.
filters.ready_offplanoffplan or ready.
filters.market_idArea slug to scope area analytics.
subject.sale_price_usdRequired for elaborated yield scenarios.
Request
bash
curl -sS -X POST "$API/v1/returns-pack/raw" \
  -H "Authorization: Bearer $ARTHABASE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "location": { "lat": -8.6472, "lng": 115.1386 },
    "filters": {
      "bedrooms": 2,
      "contract": "leasehold",
      "ready_offplan": "offplan",
      "market_id": "canggu"
    }
  }
Example response
json
{
  "product": "Returns Pack",
  "mode": "raw",
  "currency": "USD",
  "area": { "market": "Canggu", "village": "Canggu", "district": "North Kuta", "regency": "Badung" },
  "comp_search": {
    "sale": { "radius_m_used": 1500, "count": 12 },
    "str": { "radius_m_used": 3000, "count": 18 }
  },
  "sale_comps": [{ "listing_id": "l9f2a1", "sale_price_usd": 248000, "distance_m": 180 }],
  "str_comps": [{
    "airbnb_id": "6763…",
    "financials": { "adr_usd": 188, "occupancy_pct": 61 },
    "monthly_history": [{ "month": "2025-09", "adr_usd": 175 }]
  }],
  "area_analytics": {
    "supply": { "snapshot": { "sale_price_usd": { "median": 178000 } }, "monthly": [] },
    "str": { "snapshot": { "adr_usd": { "median": 175 } }, "monthly": [] }
  }
}

9. Supply listing search

GET /v1/supply/listings
GET /v1/supply/listings/:id

Search or fetch one for-sale villa. Rp 2,500 / $0.10 per call. Paginate with limit / offset (max 50).

Filters / params

FieldNotes
marketArea slug, e.g. canggu.
bedroomsInteger bed count.
contractleasehold or freehold.
ready_offplanoffplan or ready.
sale_price_usd_min / _maxAsk USD range.
land_m2_min / _maxLand m² range.
building_m2_min / _maxBuilding m² range.
limitPage size (max 50).
offsetPagination offset.
Request
bash
curl -sS -H "Authorization: Bearer $ARTHABASE_API_KEY" \
  "$API/v1/supply/listings?market=canggu&bedrooms=2&ready_offplan=offplan&limit=20"
Example search response
json
{
  "count": 84,
  "listings": [
    {
      "listing_id": "l7acb3e7c",
      "title": "2BR off-plan villa",
      "market_id": "canggu",
      "bedrooms": 2,
      "ready_offplan": "Off-Plan",
      "contract": "Leasehold",
      "latest_price_usd": 160849,
      "lat": -8.65,
      "lng": 115.14
    }
  ]
}
Example by id
json
{
  "listing": {
    "listing_id": "l7acb3e7c",
    "bedrooms": 3,
    "ready_offplan": "Off-Plan",
    "latest_price_usd": 160849
  }
}

10. Airbnb listing search

GET /v1/airbnb/listings

Thin browse by area name and bedrooms (live estimates, no month history). Max 30 per call. Rp 5,000 / $0.25. Use by id or history for full financials.

Filters / params

FieldNotes
locationArea name, e.g. Canggu.
bedroomsInteger.
adr_usd_min / _maxNightly ADR USD range.
occupancy_pct_min / _maxOccupancy percent 0–100.
include_removed1 to include removed listings.
limitPage size (max 30).
offsetPagination offset.
Request
bash
curl -sS -H "Authorization: Bearer $ARTHABASE_API_KEY" \
  "$API/v1/airbnb/listings?location=Canggu&bedrooms=2&limit=20"
Example response
json
{
  "count": 210,
  "listings": [
    {
      "airbnb_id": "676363015807687743",
      "title": "2BR villa near Berawa",
      "area": "Canggu",
      "bedrooms": 2,
      "adr_est": 188,
      "occupancy_est": 0.61,
      "thumbnail": "https://…"
    }
  ]
}

11. Airbnb financial history

GET /v1/airbnb/listings/:id
POST /v1/airbnb/listings/history

Live financials + month history for one listing (GET) or up to 10 ids (POST). Flat price Rp 10,000 / $0.50 per request (not per listing).

Filters / params

FieldNotes
:id / airbnb_idRequiredNumeric Airbnb id (path or POST body).
airbnb_idsPOST body: 1–10 ids (alternative to single id).
monthsHistory depth (default 12, max 60).
month_from / month_toOptional inclusive bounds YYYY-MM.
Request
bash
# Single listing
curl -sS -H "Authorization: Bearer $ARTHABASE_API_KEY" \
  "$API/v1/airbnb/listings/1433898042832596830?months=12"

# Batch (same price for 1–10 ids)
curl -sS -X POST "$API/v1/airbnb/listings/history" \
  -H "Authorization: Bearer $ARTHABASE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "airbnb_ids": ["1433898042832596830", "1751587283212405948"], "months": 12 }'
Example response
json
{
  "product": "Airbnb Financial History",
  "currency": "USD",
  "history_query": { "months": 12, "listings_max": 10 },
  "count": 1,
  "listings": [
    {
      "airbnb_id": "1433898042832596830",
      "title": "Luxe 2BR Villa with Pool - Near Canggu Beach",
      "bedrooms": 2,
      "rating": 4.9,
      "financials": {
        "adr_usd": 117,
        "occupancy_pct": 53.3,
        "revpar_usd": 63,
        "revenue_30d_usd": 1877
      },
      "monthly_history": [
        { "month": "2026-08", "occupancy_pct": 53.3, "adr_usd": 117, "revenue_usd": 1877 }
      ]
    }
  ],
  "not_found": []
}

GET by id returns the same fields under listing instead of listings[].

Pricing

API key: prepaid IDR wallet (same as Studio). Low balance → 402 { "error": "Insufficient funds" }.

x402: USDC per call on Base. No wallet. Live list: GET /v1/x402.

CallIDR (wallet)USDC (x402)In plain words
Geo resolve1,000$0.01Pin → area names
Analytics supply10,000$0.50Sale market stats over time
Analytics STR10,000$0.50Airbnb market stats over time
Analytics compare15,000$0.50Same stats for 2–5 markets
Sale comps20,000$0.20Nearby for-sale peers
STR comps25,000$0.25Nearby Airbnb peers + history
Airbnb search5,000$0.25Thin browse (max 30, no month history)
Airbnb by id / history10,000$0.50One listing or batch POST history (1–10 ids)
Returns Pack raw25,000$1.50Full investment ingredients
Returns Pack elaborated50,000$3.00Same + yield scenarios
Supply search2,500$0.10Browse for-sale villas (max 50)
Supply by id2,500$0.10One villa listing
x402 catalog1,000$0.01Machine-readable price list
Health100$0.001Uptime check (API key or x402)

Paid responses do not echo a pricing block. Use this table or GET /v1/x402.

Rate limit: 20 requests / minute per API key (credits) or per IP (x402). On 429, wait for Retry-After.