Skip to main content
Every Context.dev failure surfaces as a typed exception in the SDK or a structured error envelope from the HTTP API. The envelope carries a status, a human-readable message, and a machine-readable error_code (429 responses use code instead):
{
  "message": "Domain branding not present (DNS resolution failed) [acme-nonexistent.com]",
  "status": "error",
  "error_code": "WEBSITE_ACCESS_ERROR"
}
The most common status codes:
StatusMeaningWhere to look
400Invalid input, or a well-formed domain that can’t be resolvedBad Request
401Missing or invalid API keyUnauthorized
408Cold-hit timeout or custom timeoutMS exceededRequest Timeout
422Invalid, disposable, or free email (only on /brand/retrieve-by-email)Invalid Email
429Rate limit exceeded — wait the seconds given in the Retry-After headerHandle Rate Limits
500Transient server errorServer Error
For the current API status, see status.context.dev.

Bad Request (400)

Good news first: you don’t have to sanitize the domain yourself. The API strips the protocol and the www. subdomain for you, so stripe.com, https://stripe.com, and www.stripe.com all resolve to the same brand.
// All three resolve to stripe.com — the API normalizes protocol and www.
await client.brand.retrieve({ domain: "stripe.com" });
await client.brand.retrieve({ domain: "https://stripe.com" });
await client.brand.retrieve({ domain: "www.stripe.com" });

// 400 INPUT_VALIDATION_ERROR — no TLD, fails the domain format check
await client.brand.retrieve({ domain: "stripe" });
A 400 actually comes back in three distinct flavors, told apart by error_code:
  • INPUT_VALIDATION_ERROR — the request itself is malformed. A domain with no TLD (stripe), a missing required parameter, an out-of-range value (company name on /brand/retrieve-by-name must be 3–30 characters), or both/neither of an either-or pair (/web/styleguide, /web/fonts, and /web/screenshot each take domain or directUrl, never both and never neither).
  • WEBSITE_ACCESS_ERROR — the domain is well-formed but the resolver couldn’t reach it (DNS failure, dead site, hostile WAF). This is effectively “no brand here,” and on /brand/retrieve it comes back as a 400, not a 404.
  • NOT_FOUND — no brand matched the identifier you passed. The request is not billed.
Surface an INPUT_VALIDATION_ERROR as a user-facing validation message, not a retryable failure. A WEBSITE_ACCESS_ERROR means the brand doesn’t exist or can’t be crawled, so treat it as a clean “not found.”

Unauthorized (401)

The API key is missing, invalid, expired, or deleted. Start by confirming the key is actually being loaded, without ever logging the key itself:
console.log("API key present:", !!process.env.CONTEXT_DEV_API_KEY);
console.log("API key length:", process.env.CONTEXT_DEV_API_KEY?.length);

const client = new ContextDev({ apiKey: process.env.CONTEXT_DEV_API_KEY });
Common gotchas:
  • Env var name mismatch. The expected name is CONTEXT_DEV_API_KEY. Watch for typos like CONTEXTAPIKEY or CONTEXT_DEV_APIKEY.
  • .env not loaded. In Node, import "dotenv/config" (or require("dotenv").config()) must run before the SDK is initialized. In Python, load_dotenv() from python-dotenv does the same.
  • Key was rotated. Generate a new key in the dashboard and update the environment variable everywhere the application runs.

Request Timeout (408)

A 408 comes back when either your timeoutMS budget elapsed before the API finished, or the cold-hit crawl ran past the 5-minute platform maximum. For reference, cold-hit latency is around 7 seconds at p50 and up to a minute at p99; cached hits return in around 250ms. Three recovery strategies, in order of preference: 1. Prefetch the domain or email. A warmed cache lands the eventual /brand/retrieve in under a second. See Prefetch for Faster Response. 2. Raise the timeoutMS budget. Minimum 1,000 ms, maximum 300,000 ms (5 minutes). 60 seconds is a sane default for non-time-sensitive paths:
const { brand } = await client.brand.retrieve({
  domain: "example.com",
  timeoutMS: 60000,
});
3. Retry with exponential backoff. A second attempt frequently lands on the now-warm cache from the first crawl. These snippets assume a configured client from the Quickstart:
async function fetchWithRetry(domain: string, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await client.brand.retrieve({ domain });
    } catch (err: any) {
      if (err.status === 408 && i < maxRetries - 1) {
        await new Promise((r) => setTimeout(r, 2 ** i * 1000));
        continue;
      }
      throw err;
    }
  }
}

Invalid Email (422)

Returned only by /brand/retrieve-by-email. The three causes:
  • Disposable email service (e.g. tempmail.com, guerrillamail.com).
  • Free email provider (gmail.com, yahoo.com, hotmail.com, outlook.com, aol.com, icloud.com, proton.me). The error_code is FREE_EMAIL_DETECTED.
  • Malformed email format.
Filter known-bad providers client-side to skip the round trip:
const FREE_PROVIDERS = new Set([
  "gmail.com", "yahoo.com", "hotmail.com", "outlook.com",
  "aol.com", "icloud.com", "proton.me",
]);

function isWorkEmail(email: string): boolean {
  const domain = email.split("@")[1]?.toLowerCase();
  return Boolean(domain) && !FREE_PROVIDERS.has(domain);
}

if (isWorkEmail(email)) {
  await client.brand.retrieveByEmail({ email });
}
The /brand/prefetch-by-email endpoint applies a broader 10,000+ disposable-provider filter automatically.

Server Error (500)

A 500 is transient. Two recovery patterns: 1. Retry with exponential backoff (same shape as the 408 retry above). 2. Fall back to a different lookup method. If /brand/retrieve-by-email 500s, extract the domain and call /brand/retrieve instead:
async function getBrandWithFallback(email: string) {
  try {
    return await client.brand.retrieveByEmail({ email });
  } catch (err: any) {
    if (err.status === 500) {
      const domain = email.split("@")[1];
      return await client.brand.retrieve({ domain });
    }
    throw err;
  }
}
If 500s persist across retries, check status.context.dev for an open incident, then email support with the domain, timestamp, and full error body.

Missing or partial brand data (200)

A successful 200 does not guarantee every field is populated. Not every brand has every asset (a private company has no stock; some sites have no discovered logos). Always provide defaults:
const logo = brand.logos[0]?.url ?? "/placeholder-logo.svg";
const primaryColor = brand.colors[0]?.hex ?? "#6B7280";
const description = brand.description ?? `${brand.title} (description unavailable)`;
Render the fallback in the same shape so your layout does not shift. Don’t conditionally hide whole sections.

Debugging recipes

Isolate the issue with a raw cURL request. If a cURL call works and the SDK call does not, the SDK or env-var setup is the suspect:
curl -G https://api.context.dev/v1/brand/retrieve \
  -H "Authorization: Bearer $CONTEXT_DEV_API_KEY" \
  --data-urlencode "domain=stripe.com" \
  -v
Enable verbose SDK logging when chasing 4xx mysteries. Each SDK exposes a different mechanism: Python’s logging.basicConfig(level=logging.DEBUG), Node’s console.log around the call, or running through a local proxy that captures the request.

Rate limits

The full 429 recovery contract.

Prefetch

The fastest fix for 408 cold-hit timeouts.

Best practices

Caching, fallbacks, and error handling at the integration level.

Status page

Live API status.