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

# Prefill Onboarding from a Work Email

> Cut signup friction. Take a work email at step one, then prefill the company logo, description, industry, and socials on the following onboarding step.

export const PrefillDemo = () => {
  const TRY = ["alex@stripe.com", "founder@vercel.com", "team@linear.app", "sam@notion.so"];
  const DEFAULT_EMAIL = "alex@stripe.com";
  const BRANDS = {
    "alex@stripe.com": {
      "domain": "stripe.com",
      "title": "Stripe",
      "description": "Stripe is a global financial-infrastructure platform that helps businesses of all sizes accept payments, manage subscriptions, issue cards, and build custom revenue models.",
      "colors": [{
        "hex": "#543cfb"
      }, {
        "hex": "#a498ec"
      }, {
        "hex": "#041424"
      }],
      "logos": [{
        "url": "https://media.brand.dev/ce80f5c0-45d8-4ee8-b0a4-dadedf4f83ec.svg",
        "mode": "light",
        "resolution": {
          "width": 512,
          "height": 512,
          "aspect_ratio": 1
        },
        "type": "icon"
      }],
      "industries": {
        "eic": [{
          "industry": "Finance",
          "subindustry": "Payments & Money Movement"
        }]
      }
    },
    "founder@vercel.com": {
      "domain": "vercel.com",
      "title": "Vercel",
      "description": "Vercel is a developer‑focused platform that provides cloud infrastructure and tools to build, deploy, and scale modern web applications. Its core services include instant CI/CD pipelines, a global edge CDN, and serverless “Fluid Compute” that automatically provisions the right resources from a single Git push. Vercel’s AI Cloud adds AI‑specific capabilities such as the AI Gateway for model access, an AI SDK for TypeScript, sandboxed code execution, and Vercel Agent that integrates with any stack. The platform also offers security features like DDoS protection, WAF, and BotID, plus workflow orchestration for long‑running tasks. By unifying development, deployment, and AI services, Vercel lets teams ship faster, deliver personalized experiences, and focus on product value rather than infrastructure.",
      "colors": [{
        "hex": "#040404"
      }, {
        "hex": "#7c7c7c"
      }, {
        "hex": "#cdcdcd"
      }],
      "logos": [{
        "url": "https://media.brand.dev/6ef82987-0f0e-4228-ba55-0d2d83ffe464.png",
        "mode": "has_opaque_background",
        "resolution": {
          "width": 512,
          "height": 512,
          "aspect_ratio": 1
        },
        "type": "icon"
      }],
      "industries": {
        "eic": [{
          "industry": "Technology",
          "subindustry": "Cloud Infrastructure & DevOps"
        }]
      }
    },
    "team@linear.app": {
      "domain": "linear.app",
      "title": "Linear",
      "description": "Linear is a product development platform designed for modern teams and AI agents. It provides a fast, purpose‑built system for planning, tracking, and building software products, integrating issue tracking, roadmaps, and project management into a single workflow. Leveraging AI, Linear automates tasks such as drafting PRDs, routing and labeling issues, and prioritizing work, helping teams move from idea to launch with high velocity. The tool emphasizes speed, reducing noise and friction, and supports collaborative features like Slack integration, real‑time updates, and agent‑assisted triage. By combining human and AI workflows, Linear aims to make product operations self‑driving, enabling teams to ship faster and stay aligned on strategic initiatives.",
      "colors": [{
        "hex": "#737ce3"
      }, {
        "hex": "#848488"
      }, {
        "hex": "#0c0c0d"
      }],
      "logos": [{
        "url": "https://media.brand.dev/30e62cfc-70ef-4506-b017-4179696a9a8f.jpg",
        "mode": "has_opaque_background",
        "resolution": {
          "width": 200,
          "height": 200,
          "aspect_ratio": 1
        },
        "type": "icon"
      }],
      "industries": {
        "eic": [{
          "industry": "Technology",
          "subindustry": "Software (B2B)"
        }]
      }
    },
    "sam@notion.so": {
      "domain": "notion.so",
      "title": "Notion",
      "description": "Notion is an AI‑powered workspace platform that unifies notes, docs, projects, and apps into a single collaborative environment. It offers built‑in AI agents that automate repetitive tasks, answer questions, route work, and generate reports, while AI features like Meeting Notes, Enterprise Search, and Writing Assistant enhance productivity. Users can create custom agents, connect external tools, and manage everything from calendars to CRM without additional infrastructure. Designed for teams of all sizes—from startups to Fortune 100 enterprises—Notion centralizes knowledge, streamlines workflows, and reduces tool sprawl, enabling faster decision‑making and continuous work flow 24/7.",
      "colors": [{
        "hex": "#f7e158"
      }, {
        "hex": "#7c6c32"
      }, {
        "hex": "#12110e"
      }],
      "logos": [{
        "url": "https://media.brand.dev/eb6f8842-909f-43d6-a6d5-7bccc44d35ae.png",
        "mode": "has_opaque_background",
        "resolution": {
          "width": 512,
          "height": 512,
          "aspect_ratio": 1
        },
        "type": "icon"
      }],
      "industries": {
        "eic": [{
          "industry": "Technology",
          "subindustry": "Software (B2B)"
        }]
      }
    }
  };
  const DEFAULT_BRAND = BRANDS[DEFAULT_EMAIL];
  const [email, setEmail] = useState(DEFAULT_EMAIL);
  const [brand, setBrand] = useState(DEFAULT_BRAND);
  const [loading, setLoading] = useState(false);
  const isDefault = brand === DEFAULT_BRAND && email === DEFAULT_EMAIL;
  function pickLogo(logos) {
    if (!logos || !logos.length) return null;
    return logos.find(l => l.type === "logo" && l.mode === "light") || logos.find(l => l.mode === "light") || logos.find(l => l.type === "logo") || logos[0];
  }
  function industryLabel(b) {
    const eic = b && b.industries && b.industries.eic && b.industries.eic[0];
    if (!eic) return "";
    return [eic.industry, eic.subindustry].filter(Boolean).join(" · ");
  }
  async function selectEmail(d) {
    if (d === email || loading) return;
    setEmail(d);
    setLoading(true);
    await new Promise(r => setTimeout(r, 700));
    setBrand(BRANDS[d]);
    setLoading(false);
  }
  function reset() {
    setEmail(DEFAULT_EMAIL);
    setBrand(DEFAULT_BRAND);
  }
  const logo = brand ? pickLogo(brand.logos) : null;
  const initial = brand && brand.title && brand.title[0].toUpperCase() || "?";
  const name = brand && brand.title || "";
  const description = brand && brand.description || "";
  const industry = industryLabel(brand);
  return <div className="pd-demo not-prose">
      <style>{`
        .pd-demo {
          --pd-blue: #1373e8;
          --pd-blue-press: #0e5cc0;
          --pd-pill-bd: #cfd9e8;
          --pd-line: #ececec;
          --pd-line-soft: #f4f4f5;
          --pd-ink: #0a0a0a;
          --pd-ink-2: #3f3f46;
          --pd-ink-3: #919191;
          --pd-bg: #ffffff;
          --pd-bg-soft: #fbfcfe;
          --pd-tint: #f4f8ff;
          --pd-tint-bd: #d3e3ff;
          --pd-sans: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
          --pd-mono: "Geist Mono", "IBM Plex Mono", ui-monospace, Menlo, monospace;
          position: relative;
          margin: 28px 0; padding: 22px 22px 20px;
          background: var(--pd-bg);
          border: 1px solid var(--pd-line);
          border-radius: 6px;
          font-family: var(--pd-sans);
          color: var(--pd-ink);
          overflow: hidden;
        }
        .pd-demo::before {
          content: ""; position: absolute; inset: 0; pointer-events: none;
          background: radial-gradient(800px 280px at 50% 100%, rgba(96, 165, 250, 0.10), transparent 70%);
        }
        .pd-demo > * { position: relative; }
        .pd-demo *, .pd-demo *::before, .pd-demo *::after { box-sizing: border-box; }

        .dark .pd-demo {
          --pd-blue: #3b82f6;
          --pd-blue-press: #2563eb;
          --pd-pill-bd: #2f3a4d;
          --pd-line: #26262d;
          --pd-line-soft: #1a1a1f;
          --pd-ink: #f5f5f7;
          --pd-ink-2: #c2c2cb;
          --pd-ink-3: #8b8b93;
          --pd-bg: #0a0a0c;
          --pd-bg-soft: #141418;
          --pd-tint: rgba(96, 165, 250, 0.08);
          --pd-tint-bd: #2f3a4d;
        }
        .dark .pd-overlay { background: rgba(10, 10, 12, 0.85); }
        .dark .pd-pflogo { background: #1a1a1f; }
        .dark .pd-pflogo-replace { background: #1a1a1f; }

        .pd-chrome {
          display: flex; align-items: center; justify-content: space-between;
          margin-bottom: 18px; min-height: 24px;
        }
        .pd-kicker {
          display: inline-flex; align-items: center; gap: 9px;
          font-family: var(--pd-mono);
          font-size: 11px; font-weight: 500;
          letter-spacing: 0.04em; text-transform: uppercase;
          color: var(--pd-ink-3);
        }
        .pd-kicker::before { content: ""; width: 5px; height: 5px; background: var(--pd-blue); border-radius: 1px; }
        .pd-logo-light, .pd-logo-dark { height: 22px; display: block; }
        .pd-logo-dark { display: none; }
        .dark .pd-logo-light { display: none; }
        .dark .pd-logo-dark { display: block; }
        .pd-reset {
          font-family: var(--pd-mono);
          font-size: 10.5px; letter-spacing: 0.10em;
          text-transform: uppercase; color: var(--pd-ink-2);
          background: var(--pd-bg);
          border: 1px solid var(--pd-line);
          border-radius: 0;
          padding: 5px 10px;
          cursor: pointer;
        }
        .pd-reset:hover { color: var(--pd-blue); border-color: var(--pd-blue); }

        .pd-form { display: flex; flex-direction: column; gap: 14px; margin-bottom: 14px; }
        .pd-field { display: flex; flex-direction: column; }
        .pd-label {
          font-family: var(--pd-mono);
          font-size: 10.5px; letter-spacing: 0.10em;
          text-transform: uppercase; color: var(--pd-ink-3);
          margin-bottom: 8px;
        }
        .pd-row { display: flex; gap: 8px; }
        .pd-input {
          flex: 1;
          padding: 11px 13px;
          background: var(--pd-bg);
          border: 1px solid var(--pd-line);
          border-radius: 0;
          font-family: var(--pd-mono);
          font-size: 14px; color: var(--pd-ink);
          outline: none; min-width: 0;
        }
        .pd-input:focus { border-color: var(--pd-blue); }
        .pd-btn {
          padding: 11px 18px;
          background: var(--pd-blue); color: #fff;
          border: 1px solid var(--pd-blue);
          border-radius: 0;
          font-family: var(--pd-mono);
          font-size: 13px; font-weight: 500;
          cursor: pointer; white-space: nowrap;
        }
        .pd-btn:hover { background: var(--pd-blue-press); }
        .pd-btn:disabled { opacity: 0.55; cursor: progress; }
        .pd-trys {
          display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap;
          font-family: var(--pd-mono);
          font-size: 11px; color: var(--pd-ink-3);
        }
        .pd-try {
          font-family: var(--pd-mono);
          font-size: 11px;
          padding: 2px 7px;
          background: var(--pd-bg);
          border: 1px solid var(--pd-line);
          color: var(--pd-ink-2);
          cursor: pointer;
        }
        .pd-try:hover { color: var(--pd-blue); border-color: var(--pd-blue); }
        .pd-divider { height: 1px; margin: 22px 0; background: var(--pd-line); border: 0; }

        .pd-card {
          position: relative;
          background: var(--pd-bg-soft);
          border: 1px solid var(--pd-line);
          padding: 24px 24px 22px;
        }
        .pd-card-eyebrow {
          font-family: var(--pd-mono);
          font-size: 10px; letter-spacing: 0.14em;
          text-transform: uppercase; color: var(--pd-ink-3);
          margin-bottom: 4px;
        }
        .pd-card-title {
          font-family: var(--pd-sans);
          font-size: 19px; font-weight: 600;
          letter-spacing: -0.02em;
          color: var(--pd-ink);
          margin: 0 0 16px;
        }
        .pd-banner {
          display: flex; align-items: center; gap: 9px;
          padding: 9px 12px;
          background: var(--pd-tint);
          border: 1px dashed var(--pd-tint-bd);
          font-family: var(--pd-sans);
          font-size: 12.5px;
          color: var(--pd-ink-2);
          margin-bottom: 14px;
        }
        .pd-banner .pip {
          width: 5px; height: 5px; border-radius: 50%;
          background: var(--pd-blue); flex: none;
        }
        .pd-banner strong { color: var(--pd-ink); font-weight: 600; }

        .pd-stack { display: flex; flex-direction: column; gap: 14px; }
        .pd-pfield { display: flex; flex-direction: column; gap: 5px; }
        .pd-pflabel {
          font-family: var(--pd-mono);
          font-size: 10px; letter-spacing: 0.10em;
          text-transform: uppercase; color: var(--pd-ink-3);
        }
        .pd-pfinput {
          padding: 10px 13px;
          background: var(--pd-tint);
          border: 1px solid var(--pd-tint-bd);
          border-radius: 0;
          font-family: var(--pd-sans);
          font-size: 14px;
          color: var(--pd-ink);
        }
        .pd-pftextarea {
          padding: 10px 13px;
          background: var(--pd-tint);
          border: 1px solid var(--pd-tint-bd);
          border-radius: 0;
          font-family: var(--pd-sans);
          font-size: 13px;
          line-height: 1.5;
          color: var(--pd-ink);
        }
        .pd-pflogorow {
          display: flex; gap: 12px; align-items: center;
          padding: 8px 11px;
          background: var(--pd-tint);
          border: 1px solid var(--pd-tint-bd);
        }
        .pd-pflogo {
          width: 38px; height: 38px;
          background: #fff;
          border: 1px solid var(--pd-line);
          overflow: hidden;
          display: flex; align-items: center; justify-content: center;
          color: var(--pd-ink-3);
          font-family: var(--pd-sans);
          font-weight: 600;
          font-size: 15px;
        }
        .pd-pflogo img { width: 100%; height: 100%; object-fit: contain; }
        .pd-pflogometa { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
        .pd-pflogometa-t { font-family: var(--pd-mono); font-size: 12.5px; color: var(--pd-ink); }
        .pd-pflogometa-s { font-family: var(--pd-mono); font-size: 10.5px; color: var(--pd-ink-3); }
        .pd-pflogo-replace {
          font-family: var(--pd-mono);
          font-size: 10px;
          letter-spacing: 0.10em;
          text-transform: uppercase;
          color: var(--pd-ink-3);
          padding: 5px 10px;
          border: 1px solid var(--pd-line);
          background: var(--pd-bg);
        }
        .pd-cta {
          display: inline-flex; align-items: center; justify-content: center;
          width: 100%;
          padding: 11px 18px;
          background: var(--pd-ink); color: var(--pd-bg);
          border: 1px solid var(--pd-ink);
          font-family: var(--pd-sans);
          font-size: 14px; font-weight: 500;
          margin-top: 4px;
        }

        .pd-overlay {
          position: absolute; inset: 0;
          background: rgba(255, 255, 255, 0.85);
          backdrop-filter: blur(2px);
          display: flex; flex-direction: column;
          align-items: center; justify-content: center;
          gap: 12px; z-index: 5;
        }
        .pd-spin {
          width: 22px; height: 22px; border-radius: 50%;
          border: 1.5px solid var(--pd-line);
          border-top-color: var(--pd-blue);
          animation: pd-spin 0.7s linear infinite;
        }
        @keyframes pd-spin { to { transform: rotate(360deg); } }
        .pd-line {
          font-family: var(--pd-mono);
          font-size: 10.5px; letter-spacing: 0.14em;
          text-transform: uppercase; color: var(--pd-ink-3);
        }
      `}</style>

      <div className="pd-chrome">
        <span className="pd-kicker">Live demo</span>
        {isDefault ? <span className="pd-logo">
              <img className="pd-logo-light" src="/logo/light.svg" alt="context.dev" />
              <img className="pd-logo-dark" src="/logo/dark.svg" alt="context.dev" />
            </span> : <button className="pd-reset" type="button" onClick={reset}>Reset</button>}
      </div>

      <form className="pd-form" onSubmit={e => e.preventDefault()}>
        <div className="pd-field">
          <label className="pd-label" htmlFor="pd-email-input">Work email</label>
          <div className="pd-row">
            <input id="pd-email-input" className="pd-input" type="email" value={email} readOnly placeholder="you@company.com" spellCheck={false} required />
          </div>
          <div className="pd-trys" style={{
    marginTop: 10
  }}>
            <span>Change demo</span>
            {TRY.map(d => <button key={d} type="button" className="pd-try" onClick={() => selectEmail(d)}>{d}</button>)}
          </div>
        </div>
      </form>

      <div className="pd-divider" />

      <article className="pd-card">
        <div className="pd-card-eyebrow">Onboarding</div>
        <h3 className="pd-card-title">About your company</h3>

        <div className="pd-banner">
          <span className="pip" />
          <span><strong>Prefilled from your work email:</strong> {email}. Verify before continuing.</span>
        </div>

        <div className="pd-stack">
          <div className="pd-pfield">
            <span className="pd-pflabel">Company logo</span>
            <div className="pd-pflogorow">
              <div className="pd-pflogo">
                {logo && logo.url ? <img src={logo.url} alt="" /> : <span>{initial}</span>}
              </div>
              <div className="pd-pflogometa">
                <span className="pd-pflogometa-t">{(name || "logo").toLowerCase().replace(/\s+/g, "-")}-icon.{logo && logo.url && logo.url.split(".").pop() || "png"}</span>
                <span className="pd-pflogometa-s">from {brand && brand.domain || email.split("@")[1]}</span>
              </div>
              <button type="button" className="pd-pflogo-replace">Replace</button>
            </div>
          </div>

          <div className="pd-pfield">
            <span className="pd-pflabel">Company name</span>
            <div className="pd-pfinput">{name}</div>
          </div>

          <div className="pd-pfield">
            <span className="pd-pflabel">Description</span>
            <div className="pd-pftextarea">{description.length > 280 ? description.slice(0, 280) + "…" : description}</div>
          </div>

          <div className="pd-pfield">
            <span className="pd-pflabel">Industry</span>
            <div className="pd-pfinput">{industry || "—"}</div>
          </div>

          <div className="pd-cta">Looks right · Continue</div>
        </div>

        {loading && <div className="pd-overlay">
            <div className="pd-spin" />
            <div className="pd-line">Resolving company</div>
          </div>}
      </article>
    </div>;
};

export const PromptBanner = ({prompt, message = "Use this pre-built prompt to get started faster."}) => {
  const [copied, setCopied] = useState(false);
  const timeoutRef = useRef(null);
  useEffect(() => {
    return () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
    };
  }, []);
  const handleCopy = useCallback(async () => {
    try {
      await navigator.clipboard.writeText(prompt);
      setCopied(true);
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }
      timeoutRef.current = setTimeout(() => {
        setCopied(false);
        timeoutRef.current = null;
      }, 2000);
    } catch (err) {
      console.error("Failed to copy:", err);
    }
  }, [prompt]);
  const handleOpenInCursor = useCallback(() => {
    const url = new URL('cursor://anysphere.cursor-deeplink/prompt');
    url.searchParams.set('text', prompt);
    window.location.href = url.toString();
  }, [prompt]);
  return <div className="not-prose my-6 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-950 overflow-hidden">
      <div className="p-4">
        <div className="mb-3">
          <div className="text-xs font-medium text-zinc-500 dark:text-zinc-400 mb-2">
            {message}
          </div>
          <div className="relative">
            <textarea readOnly value={prompt} className="w-full px-3 py-2 text-sm font-mono text-zinc-700 dark:text-zinc-300 bg-zinc-50 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-md resize-none focus:outline-none focus:ring-2 focus:ring-zinc-400 dark:focus:ring-zinc-600" rows={Math.min(prompt.split('\n').length, 6)} />
          </div>
        </div>

        <div className="flex items-center gap-2">
          <button onClick={handleOpenInCursor} className="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-zinc-950 dark:bg-white text-sm font-medium text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-200 transition-colors" aria-label="Try in Cursor">
            <img src="/images/cursor-light.svg" alt="Cursor logo" className="size-4 block dark:hidden" loading="lazy" width="16" height="16" />
            <img src="/images/cursor-dark.svg" alt="Cursor logo" className="size-4 hidden dark:block" loading="lazy" width="16" height="16" />
            Try in Cursor
          </button>

          <button onClick={handleCopy} className="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-zinc-100 dark:bg-zinc-800 text-sm font-medium text-zinc-950 dark:text-white hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors" aria-label={copied ? "Copied" : "Copy prompt"} aria-live="polite">
            {copied ? <>
                <svg className="size-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
                </svg>
                Copied
              </> : <>
                <svg className="size-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
                </svg>
                Copy prompt
              </>}
          </button>
        </div>
      </div>
    </div>;
};

Context.dev's [Brand API](/guides/get-brand-data#look-up-by-work-email) takes a work email and returns the company's logo, description, address, socials, and industry. You can use these values to prefill your B2B onboarding form.

This guide wires that into a multi-step signup. A user types their work email on sign-up and by the time they reach the onboarding step, the logo, company name, description, and industry are already filled in. They just need to confirm it and move ahead.

Here's a demo for you to try:

<PrefillDemo />

What you can prefill from a single work email:

| Signup field        | Brand response path                               |
| ------------------- | ------------------------------------------------- |
| Company Name        | `brand.title`                                     |
| Company Description | `brand.description`                               |
| Company Logo        | `brand.logos[].url`                               |
| Website             | `brand.domain`                                    |
| Address             | `brand.address.{street,city,country,postal_code}` |
| LinkedIn            | `brand.socials[type=linkedin].url`                |
| Industry            | `brand.industries.eic[0].industry`                |

<Tip>
  If you also theme the onboarding UI, fetch `/web/styleguide` for the color
  palette. Use Brand API for logos and company/profile fields.
</Tip>

This cookbook takes you through how to build it in 4 steps:

1. Set up a backend proxy
2. Call the Brand API on email submission
3. Render the prefilled fields with edit controls
4. Add prefetch to keep the cache warm

<PromptBanner
  message="Drop this prompt into your AI coding tool to wire up signup prefill."
  prompt="Wire prefilled onboarding into my signup flow using Context.dev:

1. Backend proxy: stand up POST /api/brand that calls POST /brand/retrieve with a JSON body containing type: by_email and the user's email, and returns the brand object. Read CONTEXT_DEV_API_KEY from env. Return brand: null on 422/400/408 instead of throwing.

2. Signup flow: when the user submits the email page, call /api/brand and stash the result in form state. Free-email users get null; fall through to generic onboarding. Put password and role on a following step (not yet prefillable), then logo, description, and socials on the prefilled step.

3. Render the prefilled step: editable inputs prepopulated from brand.title, brand.description, brand.logos (filter by mode), brand.socials. Fill blanks only; never overwrite a value the user has already typed. Always show an edit/replace affordance.

4. Add a second proxy POST /api/prefetch calling POST /utility/prefetch with type brand and an email identifier (free, subscribers only, fire-and-forget). Wire it to the email field's onBlur so the cache is warm by the time retrieve fires.

Full guide: https://docs.context.dev/use-cases/faster-onboarding-flows. Never expose CONTEXT_DEV_API_KEY to the browser."
/>

## Prerequisites

* A [Context.dev](https://context.dev) API key
* The Context.dev SDK for your backend:

<CodeGroup>
  ```bash npm theme={null}
  npm install context.dev
  ```

  ```bash pip theme={null}
  pip install context.dev
  ```

  ```bash gem theme={null}
  gem install context.dev
  ```

  ```bash go theme={null}
  go get github.com/context-dot-dev/context-go-sdk
  ```

  ```bash composer theme={null}
  composer require context-dev/context-dev-php
  ```
</CodeGroup>

## Step 1. Set up a backend proxy

The Brand API is authenticated with a secret key. Put that key in the browser and anyone who opens devtools can copy it and run up your bill. Every call from your signup form has to go through a backend route that holds the key server-side.

A minimal proxy route accepts an email from your frontend, calls `POST /brand/retrieve` with `type: "by_email"`, and returns the brand payload:

<CodeGroup>
  ```typescript TypeScript theme={null}
  // app/api/brand/route.ts
  import ContextDev from "context.dev";

  const client = new ContextDev({ apiKey: process.env.CONTEXT_DEV_API_KEY });

  // Gmail and friends have no company brand, so skip the call and the credit.
  const FREE_PROVIDERS = new Set([
    "gmail.com",
    "googlemail.com",
    "yahoo.com",
    "outlook.com",
    "hotmail.com",
    "icloud.com",
    "proton.me",
    "aol.com",
  ]);

  export async function POST(req: Request) {
    const { email } = await req.json();
    const domain = email.split("@")[1]?.toLowerCase();
    if (!domain || FREE_PROVIDERS.has(domain)) {
      return Response.json({ brand: null });
    }
    try {
      const { brand } = await client.brand.retrieve({ type: "by_email", email });
      return Response.json({ brand });
    } catch {
      // 422 free/disposable, 400 no brand, 408 cold-hit timeout: all survivable.
      return Response.json({ brand: null });
    }
  }
  ```

  ```python Python theme={null}
  # routes/brand.py
  import os
  from context.dev import ContextDev, APIError
  from flask import jsonify, request

  client = ContextDev(api_key=os.environ["CONTEXT_DEV_API_KEY"])

  # Gmail and friends have no company brand, so skip the call and the credit.
  FREE_PROVIDERS = {
      "gmail.com", "googlemail.com", "yahoo.com", "outlook.com",
      "hotmail.com", "icloud.com", "proton.me", "aol.com",
  }

  @app.post("/api/brand")
  def brand():
      email = request.json["email"]
      domain = email.rsplit("@", 1)[-1].lower()
      if domain in FREE_PROVIDERS:
          return jsonify(brand=None)
      try:
          result = client.brand.retrieve(type="by_email", email=email)
          return jsonify(brand=result.brand.to_dict())
      except APIError:
          # 422 free/disposable, 400 no brand, 408 timeout: fall through.
          return jsonify(brand=None)
  ```

  ```ruby Ruby theme={null}
  # config/routes.rb: post "/api/brand", to: "brand#show"
  class BrandController < ApplicationController
    CLIENT = ContextDev::Client.new(api_key: ENV.fetch("CONTEXT_DEV_API_KEY"))
    # Gmail and friends have no company brand, so skip the call and the credit.
    FREE_PROVIDERS = %w[gmail.com googlemail.com yahoo.com outlook.com
                        hotmail.com icloud.com proton.me aol.com].freeze

    def show
      email = params.require(:email)
      domain = email.split("@").last&.downcase
      return render json: { brand: nil } if FREE_PROVIDERS.include?(domain)

      result = CLIENT.brand.retrieve(body: { type: :by_email, email: email })
      render json: { brand: result.brand }
    rescue ContextDev::Errors::APIError
      render json: { brand: nil }
    end
  end
  ```

  ```go Go theme={null}
  // POST /api/brand
  package main

  import (
  	"encoding/json"
  	"net/http"
  	"os"
  	"strings"

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

  var client = contextdev.NewClient(option.WithAPIKey(os.Getenv("CONTEXT_DEV_API_KEY")))

  // Gmail and friends have no company brand, so skip the call and the credit.
  var freeProviders = map[string]bool{
  	"gmail.com": true, "googlemail.com": true, "yahoo.com": true, "outlook.com": true,
  	"hotmail.com": true, "icloud.com": true, "proton.me": true, "aol.com": true,
  }

  func brandHandler(w http.ResponseWriter, r *http.Request) {
  	var body struct {
  		Email string `json:"email"`
  	}
  	json.NewDecoder(r.Body).Decode(&body)
  	at := strings.LastIndex(body.Email, "@")
  	if at == -1 {
  		json.NewEncoder(w).Encode(map[string]any{"brand": nil})
  		return
  	}
  	domain := strings.ToLower(body.Email[at+1:])

  	if freeProviders[domain] {
  		json.NewEncoder(w).Encode(map[string]any{"brand": nil})
  		return
  	}
  	resp, err := client.Brand.Get(r.Context(), contextdev.BrandGetParams{
  		OfByEmail: &contextdev.BrandGetParamsBodyByEmail{Email: body.Email},
  	})
  	if err != nil {
  		// 422 free/disposable, 400 no brand, 408 timeout: fall through.
  		json.NewEncoder(w).Encode(map[string]any{"brand": nil})
  		return
  	}
  	json.NewEncoder(w).Encode(map[string]any{"brand": resp.Brand})
  }
  ```

  ```php PHP theme={null}
  <?php
  // routes/brand.php — POST /api/brand
  use ContextDev\Client;
  use ContextDev\Core\Exceptions\APIStatusException;

  $client = new Client(apiKey: getenv('CONTEXT_DEV_API_KEY'));

  // Gmail and friends have no company brand, so skip the call and the credit.
  $FREE_PROVIDERS = [
      'gmail.com', 'googlemail.com', 'yahoo.com', 'outlook.com',
      'hotmail.com', 'icloud.com', 'proton.me', 'aol.com',
  ];

  $body = json_decode(file_get_contents('php://input'), true);
  $email = $body['email'] ?? '';
  $domain = strtolower(explode('@', $email)[1] ?? '');

  if (!$domain || in_array($domain, $FREE_PROVIDERS, true)) {
      header('Content-Type: application/json');
      echo json_encode(['brand' => null]);
      exit;
  }

  try {
      $response = $client->brand->retrieve(type: 'by_email', email: $email);
      header('Content-Type: application/json');
      echo json_encode(['brand' => $response->brand]);
  } catch (APIStatusException) {
      // 422 free/disposable, 400 no brand, 408 cold-hit timeout: all survivable.
      header('Content-Type: application/json');
      echo json_encode(['brand' => null]);
  }
  ```
</CodeGroup>

<Badge color="orange">Brand API · 10 credits per successful call</Badge>

The route reads the key from `CONTEXT_DEV_API_KEY`, calls Context.dev's Brand API, and returns the brand object untouched; you pick fields in [step 3](#step-3-display-the-prefilled-values), so new fields land without a backend deploy. Anything that isn't a clean hit (free email, unknown domain, cold-hit timeout, or a Gmail-class address caught before the call) comes back as `brand: null`, which the frontend reads as "nothing to prefill, so fall through to generic onboarding."

## Step 2. Call retrieve on email submission

When the user submits the email page, hit the proxy and stash the result in form state. The next step reads from it.

```typescript theme={null}
// In your signup form, when the user submits step one.
async function onEmailSubmit(email: string) {
  const res = await fetch("/api/brand", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ email }),
  });
  const { brand } = await res.json(); // may be null
  setSignupState({ email, brand });
  advanceToStep(2);
}
```

Two things shape this call:

* **Free and disposable emails have no brand.** The proxy short-circuits the well-known free providers (Gmail, Yahoo, Outlook, ProtonMail, and friends) before spending a call; anything that slips past the list (disposable forwarders, lesser-known providers) returns a 422 the proxy swallows. Either way you get `brand: null` and render the generic form.
* **First-time domains are slow.** A domain Context.dev hasn't crawled yet runs several seconds on the first hit: 7s at p50, up to a minute at p99 (see the [latency table](/optimization/prefetching#latency)). [Step 4](#step-4-prefetch-on-email-typing-optional) warms the cache so the call lands in under a second.

### Understand the response

A successful retrieve returns `status`, `code`, and a `brand` object. Here's a real response for `dev@stainless.com`:

```json theme={null}
{
  "status": "ok",
  "code": 200,
  "brand": {
    "domain": "stainless.com",
    "title": "Stainless",
    "description": "Stainless helps API teams deliver world-class developer interfaces...",
    "logos": [
      {
        "type": "icon",
        "mode": "has_opaque_background",
        "url": "https://media.brand.dev/c5e3...png"
      },
      {
        "type": "icon",
        "mode": "dark",
        "url": "https://media.brand.dev/0380...png"
      },
      {
        "type": "logo",
        "mode": "light",
        "url": "https://media.brand.dev/fe14...svg"
      }
    ],
    "colors": [{ "hex": "#adadad", "name": "Robo Master" }],
    "socials": [
      {
        "type": "linkedin",
        "url": "https://linkedin.com/company/stainless-api"
      },
      { "type": "x", "url": "https://x.com/StainlessAPI" }
    ],
    "address": {
      "street": "180 Varick St",
      "city": "New York",
      "country": "United States",
      "postal_code": "10014"
    },
    "industries": {
      "eic": [
        { "industry": "Technology", "subindustry": "Developer Tools & APIs" }
      ]
    }
  }
}
```

The fields this guide reads off `brand`:

| Field              | Type   | Notes                                                                                            |
| ------------------ | ------ | ------------------------------------------------------------------------------------------------ |
| `title`            | string | Company name                                                                                     |
| `description`      | string | One-paragraph summary                                                                            |
| `domain`           | string | Extracted from the email                                                                         |
| `logos[]`          | array  | Each has `url`, `type` (`logo` or `icon`), and `mode` (`light`, `dark`, `has_opaque_background`) |
| `colors[]`         | array  | Each has `hex` and `name`                                                                        |
| `socials[]`        | array  | Each has `type` (e.g. `linkedin`) and `url`                                                      |
| `address`          | object | `street`, `city`, `country`, `postal_code`, plus state and country codes                         |
| `industries.eic[]` | array  | Each has `industry` and `subindustry`                                                            |

<Note>
  **Structure your form to favor prefill.** Put fields the API *can* return *after* fields it can't. The gap buys the retrieve a few seconds to land, so the response is usually waiting by the time the user reaches the company step:

  1. **Email**: triggers retrieve.
  2. **Password or OAuth**: the API can't return this.
  3. **User's role at the company**: the API can't return this either.
  4. **Company logo, description, and socials**: prefilled.
</Note>

## Step 3. Display the prefilled values

On the prefilled step, render the response with editable inputs. The user's job is to glance, fix anything wrong, and continue.

```tsx theme={null}
function CompanyStep({ brand, onSubmit }) {
  if (!brand) return <GenericCompanyStep onSubmit={onSubmit} />;

  // Rule 2: pick the logo variant for where you'll show it; fall back to the first.
  const logo =
    brand.logos?.find((l) => l.type === "logo" && l.mode === "light")?.url ??
    brand.logos?.[0]?.url;
  const linkedin = brand.socials?.find((s) => s.type === "linkedin")?.url;

  return (
    <form onSubmit={onSubmit}>
      {/* Rule 1: defaultValue (not value) fills blanks only, never clobbers typed input. */}
      <Field label="Company name" defaultValue={brand.title} />
      <Field label="Description" defaultValue={brand.description} multiline />
      <LogoField src={logo} alt={brand.title} onReplace={uploadLogo} />
      <Field label="LinkedIn" defaultValue={linkedin} />
      {/* Rule 3: public-facing data needs an explicit confirm before it's accepted. */}
      <label>
        <input type="checkbox" name="confirmed" required /> These details look
        right
      </label>
      <button>Continue</button>
    </form>
  );
}
```

Three rules apply to every prefilled field:

**Fill blanks only.** If the user backtracked and typed a company name before retrieve resolved, leave their text alone. Use `defaultValue`, not `value`; React's uncontrolled inputs do this correctly out of the box.

**Pick the right logo variant.** `brand.logos[]` returns multiple variants, each with a `type` (`logo` or `icon`) and `mode` (`light`, `dark`, or `has_opaque_background`). Filter by where you'll display it:

| Use case                             | Filter                                                   |
| ------------------------------------ | -------------------------------------------------------- |
| Logo on a white-background dashboard | `type === "logo"` and `mode === "light"`                 |
| Avatar bubble                        | `type === "icon"` and `mode === "has_opaque_background"` |
| Dark-mode app                        | `type === "logo"` and `mode === "dark"`                  |

When a brand only ships one mode, fall back to `logos[0]`. Always show a replace affordance. The API's pick is a default, not a verdict.

**Require an explicit confirm for public-facing data.** If the logo or company name will be visible to teammates or shown on a public profile, don't auto-accept; gate it behind a "Looks right" click. The wrong logo on a competitor's profile is worse than no logo at all.

## Step 4. Prefetch on email typing (optional)

The basic version works, but the first hit on an uncrawled domain may take up to 60 seconds. To make the step transition feel instant, fire a free prefetch call while the user is still typing their email.

`POST /utility/prefetch` queues the same crawl that retrieve would, then returns right away with `{ status, message, type, domain, key_metadata }`. It costs zero credits and is available on paid plans, so it's safe to call on every email-field blur.

Add a second proxy route:

<CodeGroup>
  ```typescript TypeScript theme={null}
  // app/api/prefetch/route.ts
  import ContextDev from "context.dev";

  const client = new ContextDev({ apiKey: process.env.CONTEXT_DEV_API_KEY });

  export async function POST(req: Request) {
    const { email } = await req.json();
    // Fire-and-forget. 422 for free emails is ignored.
    client.utility
      .prefetch({ type: "brand", identifier: { email } })
      .catch(() => {});
    return new Response(null, { status: 202 });
  }
  ```

  ```python Python theme={null}
  # routes/prefetch.py
  from context.dev import APIError
  from flask import jsonify, request

  @app.post("/api/prefetch")
  def prefetch():
      try:
          # Fire-and-forget. 422 for free emails is ignored.
          client.utility.prefetch(
              type="brand",
              identifier={"email": request.json["email"]},
          )
      except APIError:
          pass
      return "", 202
  ```

  ```ruby Ruby theme={null}
  # config/routes.rb: post "/api/prefetch", to: "brand#prefetch"
  def prefetch
    # Fire-and-forget. 422 for free emails is ignored.
    CLIENT.utility.prefetch(
      type: "brand",
      identifier: { email: params.require(:email) },
    )
  rescue ContextDev::Errors::APIError
    nil
  ensure
    head :accepted
  end
  ```

  ```go Go theme={null}
  // POST /api/prefetch
  // Same file as the step 1 handler; add "context" to its imports.
  import "context"

  func prefetchHandler(w http.ResponseWriter, r *http.Request) {
  	var body struct {
  		Email string `json:"email"`
  	}
  	json.NewDecoder(r.Body).Decode(&body)
  	// Fire-and-forget. 422 for free emails is ignored.
  	go client.Utility.Prefetch(context.Background(), contextdev.UtilityPrefetchParams{
  		Type: "brand",
  		Identifier: contextdev.UtilityPrefetchParamsIdentifier{
  			Email: body.Email,
  		},
  	})
  	w.WriteHeader(http.StatusAccepted)
  }
  ```

  ```php PHP theme={null}
  <?php
  // routes/prefetch.php — POST /api/prefetch
  use ContextDev\Client;
  use ContextDev\Core\Exceptions\APIStatusException;

  $client = new Client(apiKey: getenv('CONTEXT_DEV_API_KEY'));

  $body = json_decode(file_get_contents('php://input'), true);
  $email = $body['email'] ?? '';

  // Fire-and-forget. 422 for free emails is ignored.
  try {
      $client->utility->prefetch(type: 'brand', identifier: ['email' => $email]);
  } catch (APIStatusException) {
  }

  http_response_code(202);
  ```
</CodeGroup>

<Badge color="green">Prefetch · 0 credits</Badge>
<Badge color="purple">Subscribers only</Badge>

Then wire it into the email field's blur event:

```tsx theme={null}
function EmailField({ value, onChange }) {
  return (
    <input
      type="email"
      value={value}
      onChange={(e) => onChange(e.target.value)}
      onBlur={() => {
        if (!isValidEmail(value)) return;
        fetch("/api/prefetch", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ email: value }),
        }); // intentionally not awaited
      }}
    />
  );
}
```

Now, by the time the user finishes step one and the retrieve in [step 2](#step-2-call-retrieve-on-email-submission) fires, the cache is warm and the response returns in under a second.

### Handle the cold hits that slip through

Even with prefetch, the occasional cold hit will outrun the user. Three options:

1. **Skeleton.** Render the prefilled step with a loading state, fill in when retrieve resolves. Best when the step has no other interactive elements.
2. **Fill on-arrive.** Render the editable form with blanks; when retrieve returns, fill empty fields without animating. Don't overwrite anything the user typed in the meantime.
3. **Skip silently.** Show empty fields. Surface a soft "we couldn't auto-fill this" notice if you care.

Option 2 is the most forgiving for a fast typist; option 1 looks more polished on a slow form.

## Learn more

<CardGroup cols={2}>
  <Card title="Brand API Guide" icon="book-open" href="/guides/get-brand-data">
    How the Brand API works end to end, every lookup variant, and the full
    profile shape.
  </Card>

  <Card title="API Reference" icon="address-card" href="/api-reference/brand-intelligence/brand">
    The `POST /brand/retrieve` endpoint: parameters, response schema, and error
    codes.
  </Card>

  <Card title="Prefetching" icon="bolt" href="/optimization/prefetching">
    Latency curves, cache TTLs, and the mechanics behind the prefetch endpoint.
  </Card>

  <Card title="Best Practices" icon="list-check" href="/optimization/best-practices">
    Caching TTLs, override rules, never-expose-keys.
  </Card>
</CardGroup>
