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

# Integration Best Practices

> Integration patterns for Context.dev: caching, prefetching, retries, choosing endpoints, filtering emails, and staying within plan rate limits.

If you're building on top of Context.dev's APIs and SDKs, this page contains some best practices to give your users a good experience.

## 1. Prefetch as soon as you know the domain or email

60% of Brand API requests return within 1 second. But for the other 40%, when Context.dev hasn't seen the domain before, it can take up to a minute.

Prefetching fixes this. It's a free API that you can call a few moments before your app needs the brand data. It makes sure the results come back typically in under a second when you actually run the Brand API call.

See [Prefetch for Faster Response](/optimization/prefetching) for the canonical pattern.

## 2. Cache API responses

If you need brand or classification API outputs frequently, it's a good idea to cache them in your DB or in the browser's localStorage. This saves your rate limit quota and API credits usage.

Brand records (logos, descriptions, addresses, classifications, etc.) change very infrequently. Context.dev itself only updates a brand in cache every 3 months.

Some recommended TTLs:

| Data                         | Suggested TTL | Why                                                                      |
| ---------------------------- | ------------- | ------------------------------------------------------------------------ |
| Brand response               | 30 days       | Identity barely moves; Context.dev itself only refreshes every 3 months. |
| Product extractions          | 7 days        | Catalogs change more often than identity.                                |
| Industry codes (NAICS / SIC) | Indefinite    | Classification systems move on regulator timescales.                     |

## 3. Set `timeoutMS` for cold hits

Warm domains come back in around 250ms. A domain Context.dev has never seen has to be crawled live, which can take up to 60 seconds. If your client timeout is set to something tight like 5 seconds, you'll cut off the exact requests that needed the most patience.

So on any path that can tolerate the wait, set a generous timeout. The `timeoutMS` parameter lets you say it explicitly:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const { brand } = await client.brand.retrieve({
    type: "by_domain",
    domain: "example.com",
    timeoutMS: 60000, // 60s of headroom for cold hits
  });
  ```

  ```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}
  // Continuing from the client setup in the Quickstart. `ctx` is your
  // context.Context; `param` is github.com/context-dot-dev/context-go-sdk/packages/param.
  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, // 60s of headroom for cold hits
  );
  $brand = $response->brand;
  ```
</CodeGroup>

`timeoutMS` tops out at 300,000ms (5 minutes); past that the server gives up and returns a 408. See [Troubleshooting](/optimization/troubleshooting) for how to handle a 408 when it does happen.

## 4. Run bulk jobs in the background

Some calls are inherently slow. A full-site crawl (`/web/crawl`) or a storefront product sweep (`/brand/ai/products`) walks many pages, and on a large site that can run long.

Don't block a user's request on it. Kick the work off, return immediately, and write the results back when they land.

<CodeGroup>
  ```typescript TypeScript theme={null}
  const response = await client.web.webCrawlMd({
    url: "https://context.dev",
    maxPages: 50,
    maxDepth: 2,
  });
  console.log(`${response.results.length} pages crawled`);
  ```

  ```python Python theme={null}
  response = client.web.web_crawl_md(
      url="https://context.dev",
      max_pages=50,
      max_depth=2,
  )
  print(f"{len(response.results)} pages crawled")
  ```

  ```ruby Ruby theme={null}
  response = client.web.web_crawl_md(
    url: "https://context.dev",
    max_pages: 50,
    max_depth: 2,
  )
  puts "#{response.results.length} pages crawled"
  ```

  ```go Go theme={null}
  response, err := client.Web.WebCrawlMd(ctx, contextdev.WebWebCrawlMdParams{
      URL:      "https://context.dev",
      MaxPages: param.NewOpt[int64](50),
      MaxDepth: param.NewOpt[int64](2),
  })
  if err != nil {
      panic(err)
  }
  fmt.Printf("%d pages crawled\n", len(response.Results))
  ```

  ```php PHP theme={null}
  $response = $client->web->webCrawlMd(
      url: 'https://context.dev',
      maxPages: 50,
      maxDepth: 2,
  );
  echo count($response->results), ' pages crawled', PHP_EOL;
  ```
</CodeGroup>

Wrap that call in whatever worker queue you already run (like BullMQ, Sidekiq, Cloud Tasks, or a goroutine, etc.), show the user a status field while it runs, and notify them when the result is ready.

## 5. Retry transient failures with backoff

Three error classes are worth retrying: `429` (rate limit), `408` (cold-hit timeout), and `500` (transient server error). Everything else (a `401`, a `422`) is a bug in the request, and retrying it just wastes calls.

The retry contract (honor the `Retry-After` header on 429s, otherwise doubling delay, capped attempts) and copy-paste implementations in every SDK live on the [Rate limits](/optimization/rate-limits) page. Use that pattern rather than rolling your own.

## 6. Skip free and disposable emails before you look them up

A brand-by-email lookup against `gmail.com`, `yahoo.com`, `outlook.com`, and the 10,000+ disposable services out there will never resolve to a company: there's no brand behind a personal inbox. `POST /brand/retrieve` with `type: "by_email"` returns a `422` for these. You can save the round trip by filtering the obvious ones up front:

<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 shouldEnrich(email: string): boolean {
    const domain = email.split("@")[1]?.toLowerCase();
    return Boolean(domain) && !FREE_PROVIDERS.has(domain);
  }
  ```

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

  def should_enrich(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
  ```

  ```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 should_enrich(email)
    domain = email.split("@")[1]&.downcase
    !domain.nil? && !FREE_PROVIDERS.include?(domain)
  end
  ```

  ```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 shouldEnrich(email string) bool {
      parts := strings.Split(email, "@")
      if len(parts) != 2 {
          return false
      }
      return !freeProviders[strings.ToLower(parts[1])]
  }
  ```

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

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

**You don't have to maintain the full disposable-domain list yourself**: `POST /utility/prefetch` detects 10,000+ free and disposable providers when you pass `identifier.email`, so calling it skips the manual guard entirely. Read [how to prefetch](/optimization/prefetching).

## 7. Set up fallbacks for missing datapoints

A `200` doesn't promise a complete record. A private company has no stock ticker, a brand-new domain may have no logo on file yet, and any optional field can come back `null` or empty. Fall back to a default value instead of branching your whole layout around what's present:

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

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

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

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

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

Defensive defaults keep the page from collapsing when the API hands back a sparse record.

## 8. Show a real loading state on cold paths

Cold brand calls and full-site crawls take seconds, not milliseconds, long enough that a frozen screen can seem "broken." Some recommended UI fixes:

* Use skeleton boxes shaped like the content, not a generic spinner.
* Echo the user's input back optimistically while the fetch runs.
* Have a graceful "we couldn't find this one" state ready for 400 responses (`NOT_FOUND` or `WEBSITE_ACCESS_ERROR`) and sparse records.

Build it once and the wait stops feeling like a bug.

## 9. Keep your API key on the server

Your API key is a bearer credential: whoever holds it can spend your credits and burn your rate limit. Treat it exactly like a database password. That means it never ships in front-end code, where anyone with devtools can read it.

The one exception is Logo Link, which uses a separate `publicClientId` credential built to be safe in the client-side browser. See [Get logos from a domain](/guides/get-logo-from-url).

## Related resources

<CardGroup cols={2}>
  <Card title="Prefetch" icon="bolt" href="/optimization/prefetching">
    Warm the cache before the user-facing call.
  </Card>

  <Card title="Rate limits" icon="gauge" href="/optimization/rate-limits">
    Stay under the per-minute request cap.
  </Card>

  <Card title="Tag requests" icon="tag" href="/optimization/tag-requests">
    Attribute credit usage to environments, teams, or features.
  </Card>

  <Card title="Fair use" icon="scale-balanced" href="/optimization/fair-use">
    Responsible use of brand data and scraped content.
  </Card>

  <Card title="Troubleshooting" icon="triangle-exclamation" href="/optimization/troubleshooting">
    Error codes and recovery patterns.
  </Card>
</CardGroup>
