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"Search and filter discovered sites.
One discovered site in full.
Curated sets that power the home page.
Extensions with showcased-site counts.
Verticals with counts.
Totals and trends.
Keep a copy in sync.
API status.
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).
<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>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.
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.
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));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 return { "error": { "code", "message", "status" } }. Every response has an X-Request-Id; include it when reporting a problem.
invalid_parameterA parameter is missing or malformed; the message says which.not_foundNo such site or endpoint.method_not_allowedThe API is read-only; use GET.rate_limitedMore than 60 requests per minute; wait for Retry-After seconds.server_errorOur fault. Quote X-Request-Id if you report it.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.
v1 only ever adds fields; breaking changes would ship as /v2.