BuiltOn

Built On API

Free, read-only JSON access to every site we've discovered on new domain extensions — enough to show a handful on your page or rebuild the whole showcase.

curl "https://builton.fyi/api/public/v1/sites?vertical=Food%20%26%20Drink&limit=5"

Base URL https://builton.fyi/api/public/v1 · JSON only · UTF-8 · ISO 8601 UTC dates · OpenAPI 3.1 (import into Postman, Insomnia or a code generator).

Embed three top sites

<div id="builton"></div>
<script>
fetch("https://builton.fyi/api/public/v1/sites?limit=3&min_score=70&fields=domain,url,title,screenshot_url")
  .then(r => r.json())
  .then(({ data }) => {
    document.getElementById("builton").innerHTML = data.map(s =>
      `<a href="${s.url}"><img src="${s.screenshot_url}" width="320" alt=""><br>${s.title} — ${s.domain}</a>`
    ).join("");
  });
</script>

Authentication

None. Every endpoint is public and CORS-enabled, so you can call it straight from a browser. Only showcased sites are returned. Please credit "Built On" with a link to builton.fyi.

Pagination

List endpoints return { data, meta }. Pass meta.next_cursor as cursor, or just follow meta.next / the Link: rel="next" header, until it's null. limit is 1–100. offset still works for jumping to a page. Use fields=domain,title,score to trim responses.

Keeping a copy in sync

sort=updated pages by change time with a stable cursor that never skips or repeats rows as new sites arrive. After a full copy, poll /changes?since=… to receive added, updated and removed sites.

// Full copy, then incremental updates
let cursor = null, since = "1970-01-01T00:00:00Z";
do {
  const url = cursor ? `https://builton.fyi/api/public/v1/sites?sort=updated&limit=100&cursor=${cursor}`
                     : `https://builton.fyi/api/public/v1/sites?sort=updated&limit=100`;
  const { data, meta } = await (await fetch(url)).json();
  data.forEach(save);                 // upsert by domain
  cursor = meta.next_cursor;
} while (cursor);

// Later, every few minutes:
const { data } = await (await fetch(`https://builton.fyi/api/public/v1/changes?since=${lastSync}`)).json();
for (const c of data) c.change === "removed" ? remove(c.domain) : save(await getSite(c.domain));

Caching

Responses carry ETag, Last-Modified and Cache-Control (5 minutes for lists, 15 for highlights). Send If-None-Match with the ETag to get a cheap 304 Not Modified. Screenshots are cached for 24 hours.

Errors

Errors return { "error": { "code", "message", "status" } }. Every response has an X-Request-Id; include it when reporting a problem.

400invalid_parameterA parameter is missing or malformed; the message says which.
404not_foundNo such site or endpoint.
405method_not_allowedThe API is read-only; use GET.
429rate_limitedMore than 60 requests per minute; wait for Retry-After seconds.
500server_errorOur fault. Quote X-Request-Id if you report it.

Rate limits

60 requests per minute per visitor. Check X-RateLimit-Remaining and X-RateLimit-Reset; on 429 wait Retry-After seconds. For larger needs, cache responses and use /changes instead of re-fetching everything.

Changelog

  • 2026-10-30 · v1.1 — Cursor paging, fields, updated_since, sort=updated, /changes, /summary, /highlights, /health, ETag caching, request IDs.
  • 2026-10-30 · v1.0 — First release: /sites, /sites/{domain}, /extensions, /verticals.

v1 only ever adds fields; breaking changes would ship as /v2.