Skip to main content
Context.dev’s Brand API 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: What you can prefill from a single work email:
Signup fieldBrand response path
Company Namebrand.title
Company Descriptionbrand.description
Company Logobrand.logos[].url
Websitebrand.domain
Addressbrand.address.{street,city,country,postal_code}
LinkedInbrand.socials[type=linkedin].url
Industrybrand.industries.eic[0].industry
If you also theme the onboarding UI, fetch /web/styleguide for the color palette. Use Brand API for logos and company/profile fields.
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

Prerequisites

  • A Context.dev API key
  • The Context.dev SDK for your backend:
npm install context.dev

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 /brand/retrieve-by-email, and returns the brand payload:
// 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.retrieveByEmail({ email });
    return Response.json({ brand });
  } catch {
    // 422 free/disposable, 400 no brand, 408 cold-hit timeout: all survivable.
    return Response.json({ brand: null });
  }
}
Brand API · 10 credits per successful call 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, 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.
// 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). Step 4 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 [email protected]:
{
  "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:
FieldTypeNotes
titlestringCompany name
descriptionstringOne-paragraph summary
domainstringExtracted from the email
logos[]arrayEach has url, type (logo or icon), and mode (light, dark, has_opaque_background)
colors[]arrayEach has hex and name
socials[]arrayEach has type (e.g. linkedin) and url
addressobjectstreet, city, country, postal_code, plus state and country codes
industries.eic[]arrayEach has industry and subindustry
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.

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.
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 caseFilter
Logo on a white-background dashboardtype === "logo" and mode === "light"
Avatar bubbletype === "icon" and mode === "has_opaque_background"
Dark-mode apptype === "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 /brand/prefetch-by-email queues the same crawl that retrieve would, then returns right away with { status, message, domain }. 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:
// 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.prefetchByEmail({ email }).catch(() => {});
  return new Response(null, { status: 202 });
}
Prefetch · 0 credits Subscribers only Then wire it into the email field’s blur event:
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 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

Brand API Guide

How the Brand API works end to end, every lookup variant, and the full profile shape.

API Reference

The retrieve-by-email endpoint: parameters, response schema, and error codes.

Prefetching

Latency curves, cache TTLs, and the mechanics behind the prefetch endpoint.

Best Practices

Caching TTLs, override rules, never-expose-keys.