JupiterFetch

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

Requestcurl
# 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"
Response200 · application/json
{
  "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.

brand.js
// 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.

Authenticated request
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 parameterTypeMeaning
waitinteger, msHow 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.

HeaderMeaning
X-Fetch-IdThe 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-ControlAlways no-store. Do not put a shared cache in front of the API.
Retry-AfterSeconds 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.

FieldTypeNotes
domainstringThe normalized domain the profile is keyed on.
urlstringThe URL actually fetched, after redirects.
namestring | nullThe brand's name; when the page does not say, the first part of the domain, capitalized.
descriptionstring | nullThe site's own description, such as its meta description, or null.
colorSchemelight | dark | bothThe color schemes the page declares (light, dark or both); light when it declares none.
logos[]arrayEach 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.rolesobjectEach key is always present, and each is nullable: primary, secondary, accent, background, surface, textPrimary, textSecondary and link.
colors.palette[]arrayThe 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[]arrayEach 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.
imagesobjectog: 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.
linksobjectwebsite 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.
companyobjectFacts 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.
themeobjectDesign 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.
metaobjecttitle, 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.
qualityobjectscore 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.

CodeStatusWhen
invalid_url400The domain could not be parsed or normalized, or it is a personal email address.
unauthorized401Missing, malformed or unrecognized bearer key.
quota_exceeded402Your account has no credits left. Successful calls spend a credit, cached or not; errors never do. Buy credits on /billing.
forbidden_domain403The domain resolves to a private, loopback or link-local address, another reserved range, or localhost, or a redirect leads to one.
domain_unresolvable404DNS returned nothing for the domain.
site_blocked424The 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_limited429Too many requests. Honor the retry-after header.
fetch_timeout424The site did not answer within the wait budget. The fetch continues in the background and fills the cache; retry once.
internal_error500Something 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.
Problem document
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.

Add JupiterFetch to Claude

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 Codeone time
claude mcp add --transport http jupiterfetch \
  https://mcp.jupiterfetch.com

In 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:

Codexone time
codex mcp add jupiterfetch --url \
  https://mcp.jupiterfetch.com

The 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:

Claude APImcp_servers
{
  "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:

ToolWhat it doesCost
get_brandGet a brand profile for a domain.1 credit per successful lookup, cached results included. A partial result from a blocked page spends none.
get_creditsCheck your credit balance.Free.