API reference
One endpoint, one object, nine 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 }
}
// 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.The same contract as an OpenAPI 3.1 document: /openapi.json.
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: 424 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 424 fetch_timeout, and the fetch continues in the background. |
Response headers
Present on every /v1 response, success or failure, except where noted.
| Header | Meaning |
|---|---|
X-Fetch-Id | The ft_… id of the fetch behind this response. On a cached answer it is the fetch that produced the profile. Quote it in a bug report. |
Cache-Control | Always no-store. Do not put a shared cache in front of the API. |
Retry-After | Seconds to wait before retrying. Sent with every 429. |
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 | null | The brand's name; when the page does not say, the first part of the domain, capitalized. |
description | string | null | The site's own description, such as its meta description, or null. |
colorScheme | light | dark | both | The color schemes the page declares (light, dark or both); light when it declares none. |
logos[] | array | Each entry has an id, a kind (logo, wordmark, symbol, icon, favicon, app-icon or og), a theme (light, dark or 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 or 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 | Each key is always present, and each is nullable: primary, secondary, accent, background, surface, textPrimary, textSecondary and link. |
colors.palette[] | array | The main colors found, most significant first, near-duplicates merged (capped, so not every color the page uses). Each has a hex (always lowercase), a type (brand, accent, dark or light), a brightness from 0 to 100, a usage (a ranking weight from 0 to 1, not a share of the page), the sources it was seen in, and a confidence. |
typography.fonts[] | array | Each has a family, a role (heading, body, display, code or ui), an origin (google, adobe, custom or system), an originId (Google’s own spelling of the family, or null for every other origin), weights, sources (each a file url and its format), a fallback and a confidence. |
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 is always the fetched URL, the same as url. Each social profile (twitter, linkedin, github, youtube, instagram, facebook, tiktok and crunchbase) is a URL, or null when the page links none. Each main page on the company’s own site (careers, pricing, contact, blog, docs, privacy and terms) is a URL, or null when the page links none; careers can also be the company’s page on its job board. Only social profiles count toward quality.coverage.links. |
company | object | Facts the homepage publishes, each null when the page does not say: the legalName, a role contact email (such as a support or sales address; a person’s own address is never returned), a postal address from its structured data (street, locality, region, postalCode and country, each nullable) or null, and the foundedYear. The tagline and the phone number are null in every profile today; we fill them once they pass our accuracy checks. |
theme | object | Design tokens from the site’s CSS: fonts (heading and body, each a CSS font-family stack), the base corner radius as a CSS length, one shadow value, and the main button (its background, text and border colors, and its own radius). Each token is null in every profile today; we fill each one once it passes our accuracy checks. Colors are not repeated here; they are in colors.roles. css is a ready-to-paste block of CSS custom properties for the brand colors from colors.roles, or null when no color role was found. Font, radius, shadow and button tokens are added to it when they pass the same checks. |
meta | object | title, language and themeColor, each nullable; the web app manifest (its name, themeColor and backgroundColor), null when the site has none or its manifest declares no name and no colors; and schemaOrg (its type and name): the page’s first top-level schema.org block of any type (such as WebSite), or null when it has none. Check type before treating it as the organization. |
quality | object | score out of 100, coverage flags (logo, colors, fonts and links; logo is false when only a favicon-grade image was found, unless it is a vector icon 128 pixels or more across), and warnings[]. When the best logo is a raster favicon, a vector icon under 128 pixels across, or none, the score is at most 60, and a logo image under 24 pixels on its shorter side adds small-logo. A thin result is a 200 with a low score, never a 404. Apply your own bar. |
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, or it is a personal email address. |
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, loopback or link-local address, another reserved range, or localhost, or a redirect leads to one. |
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. |
rate_limited | 429 | Too many requests. Honor the retry-after header. |
fetch_timeout | 424 | The site did not answer within the wait budget. The fetch continues in the background and fills the cache; retry once. |
internal_error | 500 | Something failed on our side, not on the site. No credit is spent. A retry may succeed; if it keeps happening, send us the X-Fetch-Id. |
HTTP/1.1 424 Failed Dependency
content-type: application/problem+json; charset=utf-8
{
"type": "https://jupiterfetch.com/problems/fetch_timeout",
"title": "Fetch timed out",
"status": 424,
"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, Codex 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 or any other MCP client, sign in with your JupiterFetch account, and approve the connection.
In ChatGPT on the web, open Plugins and add a custom MCP server with https://mcp.jupiterfetch.com as its URL. Choose OAuth and leave the client ID and secret blank, then sign in and approve the connection. If you don’t see the option, turn on developer mode in ChatGPT’s settings, or ask your workspace admin.
In Claude Code, add it with one command, then run /mcp to authenticate:
claude mcp add --transport http jupiterfetch \
https://mcp.jupiterfetch.comIn Codex, add it with one command. Codex opens your browser to sign in and approve the connection; run codex mcp login jupiterfetch to do that again later:
codex mcp add jupiterfetch --url \
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. |