Quickstart
Sign up, mint a key, make one request, and read what comes back.
1. Get a key
Sign up for a free account, then mint a key from the first card in the dashboard.
The new key is shown once, in the dialog that mints it. What we store is a
hash, so nothing can show it to you a second time — copy it while it is in
front of you, or mint a replacement. Every key starts with eb_.
That dialog also carries the command in step 2 with your real key already in it. Copy that one and skip the edit below.
2. Make your first call
curl -H "Authorization: Bearer YOUR_API_KEY" https://api.extractbrand.dev/v1/brands/stripe.comSwap YOUR_API_KEY for the key from step 1. The header is
Authorization: Bearer YOUR_API_KEY, and a missing or wrong key
answers 401.
The same call in JavaScript, in Python, and as an MCP server an agent can call is in that dialog too, and every field it can return is in the reference.
3. What came back
One request has two possible answers and both are success. A domain someone
has read before comes out of the shared cache as a 200 with the brand in
it; a domain nobody has read yet answers 202, which means the read has
started, not that the call was refused.
A cached domain: 200
{
"object": "brand",
"domain": "stripe.com",
"cache_status": "hit",
"extracted_at": "2026-07-19T11:04:07.000Z",
"expires_at": "2026-08-18T11:04:07.000Z",
"quality_verdict": "complete",
"missing_fields": [],
"brand": {
"name": "Stripe",
"logo": {
"url": "https://stripe.com/img/logo.svg",
"source": "inline-svg"
},
"colors": [
{ "hex": "#635bff", "role": "primary", "on": "#ffffff",
"names": ["--brand"], "coverage": 0.08 },
{ "hex": "#0a2540", "role": "text", "on": "#ffffff",
"names": [], "coverage": 0.11 }
],
"fonts": [
{ "family": "Sohne", "role": "display", "availability": "custom-webfont" }
]
},
"provenance": {
"source": "measured",
"owner_fields": []
},
"quota": {
"used": 7,
"limit": 10,
"period_end": "2026-08-01T00:00:00.000Z"
}
}The sample is trimmed to the fields named below; every field the payload carries is in the reference.
object— what this payload is.brandhere,jobfor the202below.domain— the canonical key the entry is cached under.cache_status—hitfor an entry inside its window,stalefor one past it that is being re-read behind your response.extracted_atandexpires_at— when the site was last read, and when the entry stops counting as current.quality_verdict— how much of the brand this read came back with:complete,partial, orthinwhen so little resolved that the payload has stopped being usable.nullmeans the entry predates this field and was never assessed — read it as unknown, never ascomplete.missing_fields— the fields this read did not find, so a blank is a fact rather than a guess.[]when nothing is missing,nullwhen the verdict isnulland nothing was assessed. It says what we did not find, not what the company does not have.brand.colors— every colour of the brand as one ranked list, each with itshex, a text colour that is legibleonit, thenamesthe site's own stylesheet gives it, and thecoverageit paints of the rendered page. The entries with aroleare the brand's paint kit; an entry withrole: nullis a colour the page paints that fills no role.brand.fonts— the faces that rendered, display first, each marked with how it can be loaded.brand.logo— the mark at the URL the site itself serves it from, and where on the page it was found.provenance— which fields here the brand's owner authored, and which were measured from their site.owner_fieldsnames the authored ones and every field it does not name was measured, sosourcereadsowneronly when that list is non-empty.quota— units spent this period, the ceiling, and when the period rolls over.
A site that publishes no logo still answers 200. Whatever no specialist
could fill is named in gaps and, for the fields this contract publishes, in
missing_fields beside the verdict — so a thin payload reads as a thin read
rather than a failed call, and you never have to guess whether an empty field
means we looked and found nothing or never looked at all.
A first read: 202
{
"object": "job",
"job_id": "job_0f6d8c1e-4a2b-4d7c-9e13-5b0a2c8f4d61",
"domain": "stripe.com",
"status": "queued",
"poll_url": "/v1/brands/stripe.com",
"quota": {
"used": 8,
"limit": 10,
"period_end": "2026-08-01T00:00:00.000Z"
}
}poll_url is the domain URL you just called — poll the domain, not a job id.
Adding ?wait=25 holds the request open until the read
finishes, so one call returns the brand.
4. What the free tier includes
10answered calls a month. Every call that returns a brand counts, whether we extract it for you or serve one we already hold.- An explicit refresh costs more than a read, because it always performs a fresh extraction — more than the free tier holds, so refreshing is a paid-tier call.
120 requests per minute, per key. Over that the API answers429.
5. Point it at your own domain
Your own domain will not be in the cache yet, so the first call answers the
202 from step 3 — the read has to open the site. Add
?wait=25 to the URL and that first call returns the brand
instead of the job.
After that the brand stays cached for 30 days. Past that it is
still served immediately for the same one unit any call costs, and the re-read
running behind your response is free — it is our freshness policy acting, not
a call you made. POST /v1/brands/{domain}/refresh forces that re-read on
demand, for a site that has changed since we last picked it up.
Swap stripe.com for your own domain and run it again.