API reference
One endpoint, one object, eight errors.
Everything below is version 1. Fields are added, never repurposed. A new key is not a breaking change, so parse permissively.
Need a key? Start free with 100 credits and no card, then create one on the API keys page.
Quick start
# Blocking. Returns when the profile is ready, or at the wait budget.
curl -H "Authorization: Bearer $JUPITERFETCH_API_KEY" \
"https://jupiterfetch.com/v1/brands/example.com"{
"domain": "example.com",
"name": "Example",
"colorScheme": "both",
"logos": [{
"kind": "wordmark", "theme": "light",
"sourceType": "inline-svg",
"source": "https://cdn.jupiterfetch.com/example.com/….png",
"format": "png", "width": 2048, "height": 512,
"vectorUrl": "https://cdn.jupiterfetch.com/example.com/….svg",
"originUrl": null
}],
"colors": {
"roles": { "primary": "#635bff" },
"palette": [{ "hex": "#635bff",
"type": "brand" }]
},
"quality": { "score": 86 },
"fetch": { "path": "fast" }
}
// Trimmed. Full object shape in the API reference.Code samples
The same short program in five languages, each using only its standard library, so there is nothing to install. It reads your key from JUPITERFETCH_API_KEY, retries a fetch_timeout once, and prints the best logo and the primary color.
// Node 18+, no dependencies. Run with JUPITERFETCH_API_KEY set: node brand.js
// Server-side only: the API sends no CORS headers, and your key must never reach a browser.
const JUPITERFETCH_URL = 'https://jupiterfetch.com/v1'
async function getBrand(domain, attempt = 0) {
const res = await fetch(`${JUPITERFETCH_URL}/brands/${encodeURIComponent(domain)}`, {
headers: { Authorization: `Bearer ${process.env.JUPITERFETCH_API_KEY}` },
signal: AbortSignal.timeout(70_000), // a little over the longest wait the API allows
})
if (res.ok) return res.json() // a Brand object
// Errors are RFC 9457 problem documents. Branch on `code`, not on the status.
const type = res.headers.get('content-type') ?? ''
const problem = type.includes('application/problem+json')
? await res.json()
: { code: 'unknown', detail: await res.text() }
if (attempt === 0 && problem.code === 'fetch_timeout') {
// The fetch usually keeps running, so a retry often comes back from the cache.
return getBrand(domain, 1)
}
// Wait out a short rate limit and retry once. A longer one is reported, not waited on.
const seconds = Number(res.headers.get('retry-after') ?? 1)
if (attempt === 0 && problem.code === 'rate_limited' && seconds <= 10) {
await new Promise((resolve) => setTimeout(resolve, seconds * 1000))
return getBrand(domain, 1)
}
const fetchId = problem.fetchId ? ` (fetchId ${problem.fetchId})` : ''
throw Object.assign(new Error(`${problem.code}: ${problem.detail}${fetchId}`), {
status: res.status,
code: problem.code,
fetchId: problem.fetchId,
})
}
// Wrapped in a function because top-level await is not allowed in a CommonJS file.
async function main() {
try {
const brand = await getBrand('stripe.com')
// logos is ranked best first and can be empty. Each source URL works for at least 30 days,
// so download the file if you want to keep it.
console.log('logo:', brand.logos[0]?.source ?? 'none')
// Lowercase hex such as "#1a2b3c", or null.
console.log('primary color:', brand.colors.roles.primary ?? 'none')
} catch (err) {
console.error(err.message)
process.exitCode = 1
}
}
main()Base URL and authentication
Bearer keys that start with jf_. Every response is JSON, and timestamps are ISO-8601 UTC with lowercase hex colors.
curl -H "Authorization: Bearer $JUPITERFETCH_API_KEY" \
"https://jupiterfetch.com/v1/brands/example.com?wait=20000"
# Missing, malformed or wrong key → 401 unauthorized.GET /v1/brands/{domain}
The only implemented endpoint. It blocks: a fresh cache entry returns immediately, a miss runs the fetch inline within the wait budget.
On success: 200 with the Brand object. On a budget overrun: 504 fetch_timeout, and the fetch keeps running, so the cache is populated by the time you retry.
| Query parameter | Type | Meaning |
|---|---|---|
wait | integer, ms | How long to block before giving up. Default 20000, maximum 60000; larger values are clamped. Exceeding it returns 504, and the fetch continues in the background. |
refresh | true | false | Ignore any cached profile and fetch again. |
Response headers
Present on every /v1 response, success or failure, except where noted.
| Header | Meaning |
|---|---|
Cache-Status | One of hit, miss, stale, refresh. |
X-Fetch-Id | The ft_… id for this attempt. Quote it in a bug report. |
X-RateLimit-Limit | Requests allowed in the current window. |
X-RateLimit-Remaining | Requests left in the current window. |
X-RateLimit-Reset | When the window resets. |
X-Credits-Remaining | Credits left on your account after this call. One credit is one successful call; there is no limit header and nothing resets. Absent for internal keys. |
Cache-Control | Always no-store. Do not put a shared cache in front of the API. |
Retry-After | Seconds to wait. Sent only with rate_limited. |
The Brand object
One document per domain. Every enum below is closed. An unrecognized value means you are talking to a newer version than you think.
| Field | Type | Notes |
|---|---|---|
domain | string | The normalized domain the profile is keyed on. |
url | string | The URL actually fetched, after redirects. |
name | string | Brand name. |
description | string | One-line description, where the site offers one. |
colorScheme | light | dark | both | Which background the brand is designed against. |
logos[] | array | Each entry has kind (logo, wordmark, symbol, icon, favicon, app-icon, og), theme (light, dark, any: the background the logo is drawn for, read from its file name or its colors, so a white wordmark is dark; any when it reads on both or the file does not say), sourceType, a confidence, and source: the logo hosted by JupiterFetch, a raster image URL valid for at least 30 days after you receive it. format (png, jpeg, webp, gif) is the hosted file’s format and is never null: an SVG logo is rasterized before hosting, and its sanitized SVG is at vectorUrl (null when the logo was not SVG). originUrl is where the image came from, or null when the page embedded it; never fetch it and use what comes back. width and height are the hosted file’s pixel size and are never null. See images. |
colors.roles | object | Eight keys, each nullable: primary, secondary, accent, background, surface, textPrimary, textSecondary, link. |
colors.palette[] | array | Every color found, each typed brand, accent, dark or light, with brightness, usage share and the sources it was seen in. Hex is always lowercase. |
typography.fonts[] | array | Family, role (heading, body, display, code, ui), origin (google, adobe, custom, system), weights, sources and fallback. |
images | object | og: the page’s social image, hosted the same way as a logo (source, originUrl, format, width, height; no vectorUrl, since a social image is never SVG), or null. twitter: the image the page names for X cards, hosted the same way, or null when it names none, or when that image fails our check or could not be hosted. When it is the same image as og, both carry the same URL. banners is empty today. |
links | object | Website and social profiles: twitter, linkedin, github, youtube, instagram, facebook, tiktok, crunchbase. Absent ones are null. |
meta | object | Title, language, theme color, the web app manifest, and any schema.org Organization block. |
quality | object | score out of 100, coverage flags per section, and warnings[]. A thin result is a 200 with a low score, never a 404. Apply your own bar. |
fetch | object | id, fetchedAt, expiresAt, path (fast or browser), durationMs, cached and sourceUrl. |
Images
Every image in a Brand is hosted by JupiterFetch, so it works in your product with no conversion of your own.
Every image in a brand profile is hosted by JupiterFetch at https://cdn.jupiterfetch.com/… and stays valid for at least 30 days after you receive it. Download what you want to keep; hotlinking works during those 30 days and then breaks. format is always png, jpeg, webp or gif. SVG logos are rasterized (longest edge 2,048 px) and the sanitized SVG is at vectorUrl, safe to use in <img>. Serve stored copies with Content-Type from format and X-Content-Type-Options: nosniff. originUrl tells you where the image came from; never fetch it and use what comes back.
Render images with <img>. images.og follows the same shape as a logo, minus vectorUrl, because a social image is never SVG.
Errors
RFC 9457 problem documents, served as application/problem+json. Branch on code, not on the status: statuses are reused, codes are not.
| Code | Status | When |
|---|---|---|
invalid_url | 400 | The domain could not be parsed or normalized. |
unauthorized | 401 | Missing, malformed or unrecognized bearer key. |
quota_exceeded | 402 | Your account has no credits left. Successful calls spend a credit, cached or not; errors never do. Buy credits on /billing. |
forbidden_domain | 403 | The domain resolves to a private range, link-local address or localhost, or is on the blocklist. |
domain_unresolvable | 404 | DNS returned nothing for the domain. |
site_blocked | 424 | The site refused us, usually with a WAF. A partial Brand may be attached to the problem document. It is answered even when an expired profile exists, and a repeat within ten minutes of a definite block is answered from a negative cache unless refresh=true. |
rate_limited | 429 | Too many requests. Honor the retry-after header. |
fetch_timeout | 504 | The wait budget expired or the fetch failed. After a timeout the fetch continues in the background and fills the cache; retry once. |
HTTP/1.1 504 Gateway Timeout
content-type: application/problem+json; charset=utf-8
{
"type": "https://jupiterfetch.com/problems/fetch_timeout",
"title": "Fetch timed out",
"status": 504,
"code": "fetch_timeout",
"detail": "the fetch did not complete within 20000 ms; …",
"fetchId": "ft_01J9…"
}Also available as an MCP connector
The same brand lookup, for Claude, ChatGPT and any other MCP client, at https://mcp.jupiterfetch.com.
Once connected, asking for a company’s brand runs the same lookup as GET /v1/brands/{domain}, and each successful call spends one credit from your account.
Or add it by hand: put https://mcp.jupiterfetch.com in as a custom connector in Claude, ChatGPT or any other MCP client, sign in with your JupiterFetch account, and approve the connection.
In Claude Code, add it with one command, then run /mcp to authenticate:
claude mcp add --transport http jupiterfetch \
https://mcp.jupiterfetch.comThe Claude API’s MCP connector takes a bearer token directly, with no sign-in flow. Use a per-user key from /api-keys as authorization_token, in the header only, never in a URL:
{
"mcp_servers": [{
"type": "url",
"url": "https://mcp.jupiterfetch.com",
"name": "jupiterfetch",
"authorization_token": "jf_..."
}],
"tools": [{ "type": "mcp_toolset",
"mcp_server_name": "jupiterfetch" }]
}Two tools, both read-only:
| Tool | What it does | Cost |
|---|---|---|
get_brand | Get a brand profile for a domain. | 1 credit per successful lookup, cached results included. A partial result from a blocked page spends none. |
get_credits | Check your credit balance. | Free. |