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

> Status codes, common failures, and recovery patterns 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 `/brand/retrieve-by-email`)  | [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({ 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" });
  ```

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

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

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

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

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

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

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:

<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))
  ```
</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 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](/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({
    domain: "example.com",
    timeoutMS: 60000,
  });
  ```

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

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

  ```go Go theme={null}
  response, err := client.Brand.Get(ctx, contextdev.BrandGetParams{
      Domain:    "example.com",
      TimeoutMs: param.NewOpt[int64](60000),
  })
  ```
</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({ 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(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(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{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")
  }
  ```
</CodeGroup>

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

<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.retrieveByEmail({ 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_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_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.GetByEmail(ctx, contextdev.BrandGetByEmailParams{Email: email})
  }
  ```
</CodeGroup>

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:

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

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

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

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

  ```go Go theme={null}
  func brandWithFallback(ctx context.Context, email string) (any, error) {
      r, err := client.Brand.GetByEmail(ctx, contextdev.BrandGetByEmailParams{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{Domain: domain})
          }
          return nil, err
      }
      return r, nil
  }
  ```
</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)"
  }
  ```
</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>
