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.com

Swap 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.

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

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.