> ## Documentation Index
> Fetch the complete documentation index at: https://docs.context.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Troubleshooting

> HTTP status codes, error_code values, common failures, and copy-paste recovery patterns (retries, fallbacks, email filtering) for the Context.dev API.

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

```json theme={null}
{
  "message": "Domain branding not present (DNS resolution failed) [acme-nonexistent.com]",
  "status": "error",
  "error_code": "WEBSITE_ACCESS_ERROR"
}
```

The most common status codes:

| Status | Meaning                                                                  | Where to look                                   |
| ------ | ------------------------------------------------------------------------ | ----------------------------------------------- |
| 400    | Invalid input, or a well-formed domain that can't be resolved            | [Bad Request](#bad-request-400)                 |
| 401    | Missing or invalid API key                                               | [Unauthorized](#unauthorized-401)               |
| 408    | Cold-hit timeout or custom `timeoutMS` exceeded                          | [Request Timeout](#request-timeout-408)         |
| 422    | Invalid, disposable, or free email (only on `by_email` brand lookups)    | [Invalid Email](#invalid-email-422)             |
| 429    | Rate limit exceeded — wait the seconds given in the `Retry-After` header | [Handle Rate Limits](/optimization/rate-limits) |
| 500    | Transient server error                                                   | [Server Error](#server-error-500)               |

For the current API status, see [status.context.dev](https://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.

<CodeGroup>
  ```typescript TypeScript theme={null}
  // All three resolve to stripe.com — the API normalizes protocol and www.
  await client.brand.retrieve({ type: "by_domain", domain: "stripe.com" });
  await client.brand.retrieve({ type: "by_domain", domain: "https://stripe.com" });
  await client.brand.retrieve({ type: "by_domain", domain: "www.stripe.com" });

  // 400 INPUT_VALIDATION_ERROR — no TLD, fails the domain format check
  await client.brand.retrieve({ type: "by_domain", domain: "stripe" });
  ```

  ```python Python theme={null}
  # All three resolve to stripe.com — the API normalizes protocol and www.
  client.brand.retrieve(type="by_domain", domain="stripe.com")
  client.brand.retrieve(type="by_domain", domain="https://stripe.com")
  client.brand.retrieve(type="by_domain", domain="www.stripe.com")

  # 400 INPUT_VALIDATION_ERROR — no TLD, fails the domain format check
  client.brand.retrieve(type="by_domain", domain="stripe")
  ```

  ```ruby Ruby theme={null}
  # All three resolve to stripe.com — the API normalizes protocol and www.
  client.brand.retrieve(body: { type: :by_domain, domain: "stripe.com" })
  client.brand.retrieve(body: { type: :by_domain, domain: "https://stripe.com" })
  client.brand.retrieve(body: { type: :by_domain, domain: "www.stripe.com" })

  # 400 INPUT_VALIDATION_ERROR — no TLD, fails the domain format check
  client.brand.retrieve(body: { type: :by_domain, domain: "stripe" })
  ```

  ```go Go theme={null}
  // All three resolve to stripe.com — the API normalizes protocol and www.
  client.Brand.Get(ctx, contextdev.BrandGetParams{OfByDomain: &contextdev.BrandGetParamsBodyByDomain{Domain: "stripe.com"}})
  client.Brand.Get(ctx, contextdev.BrandGetParams{OfByDomain: &contextdev.BrandGetParamsBodyByDomain{Domain: "https://stripe.com"}})
  client.Brand.Get(ctx, contextdev.BrandGetParams{OfByDomain: &contextdev.BrandGetParamsBodyByDomain{Domain: "www.stripe.com"}})

  // 400 INPUT_VALIDATION_ERROR — no TLD, fails the domain format check
  client.Brand.Get(ctx, contextdev.BrandGetParams{OfByDomain: &contextdev.BrandGetParamsBodyByDomain{Domain: "stripe"}})
  ```

  ```php PHP theme={null}
  // All three resolve to stripe.com — the API normalizes protocol and www.
  $client->brand->retrieve(type: 'by_domain', domain: 'stripe.com');
  $client->brand->retrieve(type: 'by_domain', domain: 'https://stripe.com');
  $client->brand->retrieve(type: 'by_domain', domain: 'www.stripe.com');

  // 400 INPUT_VALIDATION_ERROR — no TLD, fails the domain format check
  $client->brand->retrieve(type: 'by_domain', domain: 'stripe');
  ```
</CodeGroup>

A 400 actually comes back in several 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 a `by_name` brand lookup 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 site exists but couldn't be scraped (blocked, hostile WAF, unreachable mid-crawl). This is effectively "no brand here," and on `/brand/retrieve` it comes back as a 400, not a 404.
* **`WEBSITE_NOT_FOUND`** — the domain definitively doesn't resolve (dead DNS). Previously folded into `WEBSITE_ACCESS_ERROR`; now its own code so you can tell a dead domain apart from a live site that blocked the crawl.
* **`NOT_FOUND`** — no brand matched the identifier you passed. The request is not billed.
* **`PDF_SKIPPED`** — the scraping target is a PDF and the request set `pdf[shouldParse]: false`.
* **`PDF_IMAGES_ONLY`** — the target PDF is a scan with images but no text layer. Retry with OCR enabled (`ocr=true` on `/parse`, `pdf[ocr]=true` on scraping endpoints) to recover the content at 1 credit per recovered page.

Surface an `INPUT_VALIDATION_ERROR` as a user-facing validation message, not a retryable failure. `WEBSITE_NOT_FOUND` and `WEBSITE_ACCESS_ERROR` both mean the brand can't be crawled, so treat them as a clean "not found" — `WEBSITE_NOT_FOUND` is the definitive signal that the domain is dead.

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

<CodeGroup>
  ```typescript TypeScript theme={null}
  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 });
  ```

  ```python Python theme={null}
  import os

  print("API key present:", bool(os.environ.get("CONTEXT_DEV_API_KEY")))
  print("API key length:", len(os.environ.get("CONTEXT_DEV_API_KEY", "")))
  ```

  ```ruby Ruby theme={null}
  puts "API key present: #{!ENV['CONTEXT_DEV_API_KEY'].nil? && !ENV['CONTEXT_DEV_API_KEY'].empty?}"
  puts "API key length: #{ENV['CONTEXT_DEV_API_KEY']&.length}"
  ```

  ```go Go theme={null}
  key := os.Getenv("CONTEXT_DEV_API_KEY")
  fmt.Println("API key present:", key != "")
  fmt.Println("API key length:", len(key))
  ```

  ```php PHP theme={null}
  use ContextDev\Client;

  echo 'API key present: ', getenv('CONTEXT_DEV_API_KEY') !== false ? 'true' : 'false', PHP_EOL;
  echo 'API key length: ', strlen(getenv('CONTEXT_DEV_API_KEY') ?: ''), PHP_EOL;

  $client = new Client(apiKey: getenv('CONTEXT_DEV_API_KEY'));
  ```
</CodeGroup>

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](https://context.dev/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 five-minute platform maximum. Warm-cache responses avoid crawl work, but exact latency depends on the target site, cache state, region, and requested work.

Two related behaviors on brand lookups for domains that aren't cached yet: a `timeoutMS` below 10,000 ms fails immediately with a 422 `COLD_DOMAIN_TIMEOUT_TOO_LOW` instead of burning the budget on a guaranteed timeout (a cold crawl can never finish that fast), and when a cold lookup does 408, the error message includes a hint pointing at the free [prefetch endpoint](/optimization/prefetching) so your retry lands on a warm cache.

Three recovery strategies, in order of preference:

**1. Prefetch the domain or email.** A warmed cache avoids repeating cold crawl work on the eventual `/brand/retrieve`. See [Prefetch for Faster Response](/optimization/prefetching).

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

<CodeGroup>
  ```typescript TypeScript theme={null}
  const { brand } = await client.brand.retrieve({
    type: "by_domain",
    domain: "example.com",
    timeoutMS: 60000,
  });
  ```

  ```python Python theme={null}
  brand = client.brand.retrieve(
      type="by_domain",
      domain="example.com",
      timeout_ms=60000,
  ).brand
  ```

  ```ruby Ruby theme={null}
  brand = client.brand.retrieve(
    body: { type: :by_domain, domain: "example.com" },
    timeout_ms: 60000,
  ).brand
  ```

  ```go Go theme={null}
  response, err := client.Brand.Get(ctx, contextdev.BrandGetParams{
      OfByDomain: &contextdev.BrandGetParamsBodyByDomain{Domain: "example.com"},
      TimeoutMs:  param.NewOpt[int64](60000),
  })
  ```

  ```php PHP theme={null}
  $response = $client->brand->retrieve(
      type: 'by_domain',
      domain: 'example.com',
      timeoutMs: 60000,
  );
  $brand = $response->brand;
  ```
</CodeGroup>

**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](/quickstart):

<CodeGroup>
  ```typescript TypeScript theme={null}
  async function fetchWithRetry(domain: string, maxRetries = 3) {
    for (let i = 0; i < maxRetries; i++) {
      try {
        return await client.brand.retrieve({ type: "by_domain", domain });
      } catch (err: any) {
        if (err.status === 408 && i < maxRetries - 1) {
          await new Promise((r) => setTimeout(r, 2 ** i * 1000));
          continue;
        }
        throw err;
      }
    }
  }
  ```

  ```python Python theme={null}
  import time
  from context.dev import APIStatusError

  def fetch_with_retry(domain: str, max_retries: int = 3):
      for i in range(max_retries):
          try:
              return client.brand.retrieve(type="by_domain", domain=domain)
          except APIStatusError as e:
              if e.status_code == 408 and i < max_retries - 1:
                  time.sleep(2 ** i)
                  continue
              raise
  ```

  ```ruby Ruby theme={null}
  def fetch_with_retry(client, domain, max_retries: 3)
    attempt = 0
    begin
      client.brand.retrieve(body: { type: :by_domain, domain: domain })
    rescue ContextDev::Errors::APIStatusError => e
      raise unless e.status == 408 && attempt < max_retries - 1
      sleep(2**attempt)
      attempt += 1
      retry
    end
  end
  ```

  ```go Go theme={null}
  import (
      "context"
      "errors"
      "time"

      contextdev "github.com/context-dot-dev/context-go-sdk"
  )

  func fetchWithRetry(ctx context.Context, domain string, maxRetries int) (*contextdev.BrandGetResponse, error) {
      for i := 0; i < maxRetries; i++ {
          r, err := client.Brand.Get(ctx, contextdev.BrandGetParams{OfByDomain: &contextdev.BrandGetParamsBodyByDomain{Domain: domain}})
          if err == nil {
              return r, nil
          }
          var apiErr *contextdev.Error
          if errors.As(err, &apiErr) && apiErr.StatusCode == 408 && i < maxRetries-1 {
              time.Sleep(time.Duration(1<<i) * time.Second)
              continue
          }
          return nil, err
      }
      return nil, errors.New("unreachable")
  }
  ```

  ```php PHP theme={null}
  <?php

  use ContextDev\Client;
  use ContextDev\Core\Exceptions\APIStatusException;

  function fetchWithRetry(string $domain, int $maxRetries = 3)
  {
      global $client;

      for ($i = 0; $i < $maxRetries; $i++) {
          try {
              return $client->brand->retrieve(
                  type: 'by_domain',
                  domain: $domain,
                  requestOptions: ['maxRetries' => 0],
              );
          } catch (APIStatusException $e) {
              if ($e->status === 408 && $i < $maxRetries - 1) {
                  sleep(2 ** $i);
                  continue;
              }
              throw $e;
          }
      }

      throw new RuntimeException('unreachable');
  }
  ```
</CodeGroup>

## Content Too Large (413)

Scraping and parsing endpoints cap remote content downloads at 20 MB and return a 413 with `error_code: "CONTENT_TOO_LARGE"` when the requested content exceeds it. Batch page records carry the same code, and `/web/search` results with Markdown enrichment report it per result in `markdown.code`. An oversized auxiliary resource — like an embedded PDF on an otherwise scrapable page — degrades to the page result instead of failing the request; only the requested content itself being over the cap is terminal. No retry fixes a 413, so treat it as a permanent failure for that URL.

## Unprocessable Request (422)

A 422 comes in two families, told apart by `error_code`: invalid emails and cold-domain timeouts.

### Invalid email

Returned by `by_email` brand lookups (`POST /brand/retrieve` with `type: "by_email"`), [`POST /people/enrich`](/api-reference/people/enrich), and email-identifier prefetches — always before any credits are charged. 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:

<CodeGroup>
  ```typescript TypeScript theme={null}
  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.retrieve({ type: "by_email", email });
  }
  ```

  ```python Python theme={null}
  FREE_PROVIDERS = {
      "gmail.com", "yahoo.com", "hotmail.com", "outlook.com",
      "aol.com", "icloud.com", "proton.me",
  }

  def is_work_email(email: str) -> bool:
      parts = email.split("@")
      domain = parts[1].lower() if len(parts) == 2 else None
      return bool(domain) and domain not in FREE_PROVIDERS

  if is_work_email(email):
      client.brand.retrieve(type="by_email", email=email)
  ```

  ```ruby Ruby theme={null}
  require "set"

  FREE_PROVIDERS = %w[
    gmail.com yahoo.com hotmail.com outlook.com aol.com icloud.com proton.me
  ].to_set

  def work_email?(email)
    domain = email.split("@")[1]&.downcase
    !domain.nil? && !FREE_PROVIDERS.include?(domain)
  end

  client.brand.retrieve(body: { type: :by_email, email: email }) if work_email?(email)
  ```

  ```go Go theme={null}
  var freeProviders = map[string]bool{
      "gmail.com": true, "yahoo.com": true, "hotmail.com": true, "outlook.com": true,
      "aol.com": true, "icloud.com": true, "proton.me": true,
  }

  func isWorkEmail(email string) bool {
      parts := strings.Split(email, "@")
      if len(parts) != 2 {
          return false
      }
      return !freeProviders[strings.ToLower(parts[1])]
  }

  if isWorkEmail(email) {
      client.Brand.Get(ctx, contextdev.BrandGetParams{
          OfByEmail: &contextdev.BrandGetParamsBodyByEmail{Email: email},
      })
  }
  ```

  ```php PHP theme={null}
  $FREE_PROVIDERS = [
      'gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com',
      'aol.com', 'icloud.com', 'proton.me',
  ];

  function isWorkEmail(string $email): bool
  {
      $parts = explode('@', $email);
      $domain = isset($parts[1]) ? strtolower($parts[1]) : null;
      return $domain !== null && !in_array($domain, $FREE_PROVIDERS, true);
  }

  if (isWorkEmail($email)) {
      $client->brand->retrieve(type: 'by_email', email: $email);
  }
  ```
</CodeGroup>

`POST /utility/prefetch` applies a broader 10,000+ disposable-provider filter automatically when you pass `identifier.email`.

### Cold-domain timeout too low

`COLD_DOMAIN_TIMEOUT_TOO_LOW` is returned by brand lookups when the domain isn't cached yet and `timeoutMS` is below 10,000 ms — a cold crawl can never finish that fast, so the API fails immediately instead of letting the request run into a guaranteed 408. Raise `timeoutMS` to at least 60,000 ms for paths that may hit cold domains, or [prefetch](/optimization/prefetching) the domain first and retry once the cache is warm.

## 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 a `by_email` lookup 500s, extract the domain and retry with `type: "by_domain"`:

<CodeGroup>
  ```typescript TypeScript theme={null}
  async function getBrandWithFallback(email: string) {
    try {
      return await client.brand.retrieve({ type: "by_email", email });
    } catch (err: any) {
      if (err.status === 500) {
        const domain = email.split("@")[1];
        return await client.brand.retrieve({ type: "by_domain", domain });
      }
      throw err;
    }
  }
  ```

  ```python Python theme={null}
  from context.dev import APIStatusError

  def get_brand_with_fallback(email: str):
      try:
          return client.brand.retrieve(type="by_email", email=email)
      except APIStatusError as e:
          if e.status_code == 500:
              return client.brand.retrieve(type="by_domain", domain=email.split("@")[1])
          raise
  ```

  ```ruby Ruby theme={null}
  def brand_with_fallback(client, email)
    client.brand.retrieve(body: { type: :by_email, email: email })
  rescue ContextDev::Errors::APIStatusError => e
    raise unless e.status == 500
    client.brand.retrieve(body: { type: :by_domain, domain: email.split("@")[1] })
  end
  ```

  ```go Go theme={null}
  func brandWithFallback(ctx context.Context, email string) (any, error) {
      r, err := client.Brand.Get(ctx, contextdev.BrandGetParams{
          OfByEmail: &contextdev.BrandGetParamsBodyByEmail{Email: email},
      })
      if err != nil {
          var apiErr *contextdev.Error
          if errors.As(err, &apiErr) && apiErr.StatusCode == 500 {
              domain := strings.Split(email, "@")[1]
              return client.Brand.Get(ctx, contextdev.BrandGetParams{
                  OfByDomain: &contextdev.BrandGetParamsBodyByDomain{Domain: domain},
              })
          }
          return nil, err
      }
      return r, nil
  }
  ```

  ```php PHP theme={null}
  <?php

  use ContextDev\Core\Exceptions\APIStatusException;

  function getBrandWithFallback(string $email)
  {
      global $client;

      try {
          return $client->brand->retrieve(type: 'by_email', email: $email);
      } catch (APIStatusException $e) {
          if ($e->status === 500) {
              $domain = explode('@', $email, 2)[1];
              return $client->brand->retrieve(type: 'by_domain', domain: $domain);
          }
          throw $e;
      }
  }
  ```
</CodeGroup>

If 500s persist across retries, check [status.context.dev](https://status.context.dev) for an open incident, then [email support](mailto:hello@context.dev) 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:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const logo = brand.logos[0]?.url ?? "/placeholder-logo.svg";
  const primaryColor = brand.colors[0]?.hex ?? "#6B7280";
  const description = brand.description ?? `${brand.title} (description unavailable)`;
  ```

  ```python Python theme={null}
  logo = brand.logos[0].url if brand.logos else "/placeholder-logo.svg"
  primary_color = brand.colors[0].hex if brand.colors else "#6B7280"
  description = brand.description or f"{brand.title} (description unavailable)"
  ```

  ```ruby Ruby theme={null}
  logo = brand.logos.first&.url || "/placeholder-logo.svg"
  primary_color = brand.colors.first&.hex || "#6B7280"
  description = brand.description || "#{brand.title} (description unavailable)"
  ```

  ```go Go theme={null}
  logo := "/placeholder-logo.svg"
  if len(brand.Logos) > 0 {
      logo = brand.Logos[0].URL
  }
  primaryColor := "#6B7280"
  if len(brand.Colors) > 0 {
      primaryColor = brand.Colors[0].Hex
  }
  description := brand.Description
  if description == "" {
      description = brand.Title + " (description unavailable)"
  }
  ```

  ```php PHP theme={null}
  $logo = '/placeholder-logo.svg';
  if ($brand->logos) {
      $logo = $brand->logos[0]->url;
  }
  $primaryColor = '#6B7280';
  if ($brand->colors) {
      $primaryColor = $brand->colors[0]->hex;
  }
  $description = $brand->description ?? "{$brand->title} (description unavailable)";
  ```
</CodeGroup>

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:

```bash theme={null}
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.

## Related resources

<CardGroup cols={2}>
  <Card title="Rate limits" icon="gauge" href="/optimization/rate-limits">
    The full 429 recovery contract.
  </Card>

  <Card title="Prefetch" icon="bolt" href="/optimization/prefetching">
    The fastest fix for 408 cold-hit timeouts.
  </Card>

  <Card title="Best practices" icon="list-check" href="/optimization/best-practices">
    Caching, fallbacks, and error handling at the integration level.
  </Card>

  <Card title="Status page" icon="heart-pulse" href="https://status.context.dev">
    Live API status.
  </Card>
</CardGroup>
