Developer docs menu

Advanced filtering and paging

Intermediate

Know which filters the API applies and which you apply, page efficiently and cache to save requests.

Filters the API applies

EndpointServer-side filters
/api/v1/metricscity or a rectangle (north, south, east, west), past_days, forward_days
/api/v1/trendscity, metric, source (all, gathern, airbnb), days
/api/v1/listingscity, limit, cursor
/api/v1/estimatelocation and radiusKm, bedrooms, typeId

Everything else, such as listing source, district, rating or price band, you filter in your own code after fetching.

Page through a city efficiently

Use limit=500, the maximum, to minimise requests. Stop when nextCursor is null.

JavaScript (Node 18+)
import { api } from "./jokerprice.mjs";   // helper from the quickstart

export async function allListings(city) {
  const out = [];
  let cursor = null;
  do {
    const page = await api("/api/v1/listings", { query: { city, limit: 500, ...(cursor ? { cursor } : {}) } });
    out.push(...page.listings);
    cursor = page.nextCursor;
  } while (cursor);
  return out;
}

Cache for the day

Listings change slowly. Cache each city once a day and run as many filters as you like against the file.

JavaScript (Node 18+)
import { existsSync, readFileSync, writeFileSync } from "node:fs";

export async function cachedListings(city) {
  const file = city + "-" + new Date().toISOString().slice(0, 10) + ".json";
  if (existsSync(file)) return JSON.parse(readFileSync(file, "utf8"));
  const listings = await allListings(city);
  writeFileSync(file, JSON.stringify(listings));
  return listings;
}

A composable filter

JavaScript (Node 18+)
const where = {
  source: s => l => l.source === s,
  district: d => l => (l.district || "").toLowerCase().includes(d.toLowerCase()),
  type: id => l => String(l.typeId) === String(id),
  bedrooms: (min, max = min) => l => l.bedrooms >= min && l.bedrooms <= max,
  price: (min, max) => l => l.nightly >= min && l.nightly <= max,
  rating: min => l => (l.rating ?? 0) >= min,
  reviews: min => l => (l.reviews ?? 0) >= min,
  seenWithin: days => l => Date.now() - Date.parse(l.updatedAt) <= days * 86400000,
};
const all = (...tests) => l => tests.every(t => t(l));

const listings = await cachedListings("riyadh");
const picks = listings.filter(all(where.type(6), where.bedrooms(2, 3), where.price(300, 700), where.rating(9), where.reviews(10), where.seenWithin(14)));
console.log(picks.length, "matching listings");

Scope metrics the same way

Metrics cannot filter by bedrooms or type through the API, but a rectangle around the listings you kept narrows performance to the same area.

JavaScript (Node 18+)
const lat = picks.map(l => l.latitude), lng = picks.map(l => l.longitude);
const box = { north: Math.max(...lat), south: Math.min(...lat), east: Math.max(...lng), west: Math.min(...lng) };
const { metrics } = await api("/api/v1/metrics", { query: { ...box, past_days: 60 } });
console.log(metrics.maturity, metrics.past.occupancy);

The rectangle must be non-empty (south below north, west below east) and at most 5 degrees across.

Compare sources in trends

Shell
for source in all gathern airbnb; do
  curl -s -H "Authorization: Bearer $JOKERPRICE_API_KEY" \
    "https://jokerprice.com/api/v1/trends?city=riyadh&metric=asking&source=$source&days=30" | jq -r '.trends.points[-1] | "\(.day) \(.value)"'
done