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

# Add company logos to CRM records

> Make every account recognizable at a glance. Show company logos in CRM lists and record pages, served from one Logo.dev image URL per domain.

export const CrmRecordsDemo = () => {
  const TOKEN = "live_6a1a28fd-6420-4492-aeb0-b297461d9de2";
  const ACCOUNTS = [{
    name: "Stripe",
    domain: "stripe.com",
    stage: "Customer",
    description: "A financial services platform to accept payments, build billing, and manage money movement.",
    colors: [{
      hex: "#000001"
    }, {
      hex: "#ffffff"
    }, {
      hex: "#533afd"
    }]
  }, {
    name: "Figma",
    domain: "figma.com",
    stage: "Negotiation",
    description: "The collaborative design platform for building products: design, prototype, and gather feedback in one place.",
    colors: [{
      hex: "#000000"
    }, {
      hex: "#12c6bf"
    }, {
      hex: "#dc5e80"
    }]
  }, {
    name: "Notion",
    domain: "notion.so",
    stage: "Qualified",
    description: "The AI workspace where teams build agents, search across apps, and automate busywork.",
    colors: [{
      hex: "#ffffff"
    }, {
      hex: "#000000"
    }, {
      hex: "#727272"
    }]
  }, {
    name: "Slack",
    domain: "slack.com",
    stage: "Customer",
    description: "The AI work platform for managing projects, automating workflows, and connecting teams.",
    colors: [{
      hex: "#000000"
    }, {
      hex: "#32beb7"
    }, {
      hex: "#e66844"
    }]
  }, {
    name: "Airbnb",
    domain: "airbnb.com",
    stage: "Discovery",
    description: "Vacation rentals for every kind of trip, with millions of stays across 220+ countries.",
    colors: [{
      hex: "#ff385c"
    }, {
      hex: "#ff9bad"
    }, {
      hex: "#fffdfd"
    }]
  }];
  const [selected, setSelected] = useState(0);
  const account = ACCOUNTS[selected];
  const logoSrc = (domain, size) => `https://img.logo.dev/${domain}?token=${TOKEN}&size=${size}&retina=true&format=webp`;
  const pickAccent = colors => {
    for (const c of colors ?? []) {
      const hex = c.hex;
      const r = Number.parseInt(hex.slice(1, 3), 16);
      const g = Number.parseInt(hex.slice(3, 5), 16);
      const b = Number.parseInt(hex.slice(5, 7), 16);
      const max = Math.max(r, g, b);
      const min = Math.min(r, g, b);
      if (max - min < 40 || max < 24 || min > 232) {
        continue;
      }
      return hex;
    }
    return null;
  };
  const accent = pickAccent(account.colors);
  return <div className="not-prose my-8 rounded-2xl border border-zinc-950/10 bg-gradient-to-br from-zinc-50 to-white p-6 shadow-sm sm:p-8 dark:border-white/10 dark:from-zinc-900 dark:to-zinc-950">
      <div className="mx-auto grid max-w-2xl gap-4 sm:grid-cols-2">
        <ul className="divide-y divide-zinc-950/5 rounded-xl border border-zinc-950/10 bg-white py-1 dark:divide-white/5 dark:border-white/10 dark:bg-zinc-900">
          {ACCOUNTS.map((acct, i) => <li key={acct.domain}>
              <button aria-pressed={i === selected} className={`flex w-full items-center gap-2.5 px-3 py-2.5 text-left transition-[background-color] duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-zinc-950/20 dark:focus-visible:ring-white/20 ${i === selected ? "bg-zinc-100 dark:bg-zinc-800" : "hover:bg-zinc-50 dark:hover:bg-zinc-800/50"}`} onClick={() => setSelected(i)} type="button">
                <img alt="" className="h-5 w-5 rounded object-contain" height="20" loading="lazy" src={logoSrc(acct.domain, 20)} width="20" />
                <span className="text-sm font-medium text-zinc-950 dark:text-white">
                  {acct.name}
                </span>
                <span className="ml-auto text-xs text-zinc-400">
                  {acct.stage}
                </span>
              </button>
            </li>)}
        </ul>
        <div className="rounded-xl border border-zinc-950/10 bg-white p-4 dark:border-white/10 dark:bg-zinc-900">
          <div className="flex items-center gap-3">
            <img alt={`${account.name} logo`} className="h-14 w-14 rounded-xl object-contain" height="56" key={account.domain} src={logoSrc(account.domain, 56)} width="56" />
            <div className="min-w-0">
              <div className="truncate text-base font-semibold text-zinc-950 dark:text-white">
                {account.name}
              </div>
              <div className="truncate text-xs text-zinc-400">
                {account.domain}
              </div>
            </div>
          </div>
          <div className="mt-3 text-xs leading-relaxed text-zinc-600 dark:text-zinc-400">
            {account.description}
          </div>
          <dl className="mt-3 space-y-2 border-t border-zinc-950/5 pt-3 text-xs dark:border-white/5">
            <div className="flex items-center justify-between">
              <dt className="text-zinc-400">Stage</dt>
              <dd className="font-medium text-zinc-700 dark:text-zinc-300">
                {account.stage}
              </dd>
            </div>
            {accent && <div className="flex items-center justify-between">
                <dt className="text-zinc-400">Brand color</dt>
                <dd className="flex items-center gap-1.5 font-medium text-zinc-700 dark:text-zinc-300">
                  <span className="h-3.5 w-3.5 rounded-full ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10" style={{
    backgroundColor: accent
  }} />
                  {accent}
                </dd>
              </div>}
          </dl>
        </div>
      </div>
    </div>;
};

Reps recognize a company's logo faster than its name. Putting logos in the account list and on every record page turns a CRM from a wall of text into something a sales team can scan.

In the list, each account gets a small icon next to its name. Open a record and the full logo sits in the header. Both come from the same [Logo API](/docs/logo-images/introduction) URL, built from the domain already stored on the account.

## Demo

Click an account to open its record:

<CrmRecordsDemo />

Every logo loads live from `img.logo.dev`: the list rows at `size=20`, the record header at `size=56`. The open record also shows a description and brand color, the kind of data the Brand API returns.

<Note>
  Using shadcn/ui? `npx shadcn@latest add
      https://www.logo.dev/r/logo-avatar.json` installs a ready-made account logo
  with initials fallback. See [shadcn/ui components](/docs/integrations/shadcn).
</Note>

## Build it with AI

<Prompt description="Want logo-branded account lists and records in your app? This prompt gives your AI coding tool everything it needs to build it in your framework." actions={["copy", "cursor"]}>
  Add company logos to a CRM account list and record page using Logo.dev.

  * Logos are plain image URLs: [https://img.logo.dev/:domain?token=LOGO\_DEV\_PUBLISHABLE\_KEY\&format=webp\&retina=true\&size=20](https://img.logo.dev/:domain?token=LOGO_DEV_PUBLISHABLE_KEY\&format=webp\&retina=true\&size=20) for list rows, and the same URL with size=56 for the record header.
  * Use the domain already stored on each account as the lookup key.
  * Use a Logo.dev publishable key (safe in client code).
  * Give every logo fixed dimensions so rows never shift, lazy-load the list images so long tables only fetch what is on screen, round the corners, and put alt text on the record-header logo.
  * Reference implementation: [https://www.logo.dev/docs/use-cases/crm-logos](https://www.logo.dev/docs/use-cases/crm-logos)
</Prompt>

## Code

This example is in React, but you can get the same result in any frontend framework: each logo is a plain image URL with a `size` parameter. `AccountLogo.tsx` holds the list row and the record header, and the Usage tab renders both with your [publishable key](/docs/platform/api-keys).

<CodeGroup>
  ```tsx AccountLogo.tsx expandable theme={null}
  const logoUrl = (domain: string, token: string, size: number) =>
    `https://img.logo.dev/${domain}?token=${token}&size=${size}&retina=true&format=webp`;

  export function AccountRow({
    name,
    domain,
    token,
  }: {
    name: string;
    domain: string;
    token: string;
  }) {
    return (
      <div className="flex items-center gap-2.5 px-3 py-2.5">
        <img
          alt=""
          className="h-5 w-5 rounded object-contain"
          height={20}
          loading="lazy"
          src={logoUrl(domain, token, 20)}
          width={20}
        />
        <span className="text-sm font-medium">{name}</span>
      </div>
    );
  }

  export function AccountHeader({
    name,
    domain,
    token,
  }: {
    name: string;
    domain: string;
    token: string;
  }) {
    return (
      <div className="flex items-center gap-3">
        <img
          alt={`${name} logo`}
          className="h-14 w-14 rounded-xl object-contain"
          height={56}
          src={logoUrl(domain, token, 56)}
          width={56}
        />
        <div>
          <div className="text-base font-semibold">{name}</div>
          <div className="text-xs text-zinc-400">{domain}</div>
        </div>
      </div>
    );
  }
  ```

  ```tsx Usage theme={null}
  import { AccountHeader, AccountRow } from "./AccountLogo";

  export function Accounts() {
    return (
      <div>
        <AccountRow
          domain="stripe.com"
          name="Stripe"
          token="LOGO_DEV_PUBLISHABLE_KEY"
        />
        <AccountHeader
          domain="stripe.com"
          name="Stripe"
          token="LOGO_DEV_PUBLISHABLE_KEY"
        />
      </div>
    );
  }
  ```
</CodeGroup>

<Note>
  Your [publishable key](/docs/platform/api-keys) is built for client-side code, so
  you can ship it in the browser as-is.
</Note>

## How it works

* **One image URL serves every size.** `img.logo.dev/:domain` returns the company's logo, and the `size` parameter scales it: 20 for table rows, 56 for the record header. `retina=true` keeps both crisp. See all [image parameters](/docs/logo-images/introduction#parameters).
* **Accounts without a logo still render.** When Logo.dev doesn't have a logo for a domain, it returns a generated monogram instead of a broken image, so sparse CRM data never shows a gap. See [fallback images](/docs/logo-images/introduction#fallback-images).
* **The domain is the key.** Most CRMs already store a website on the account or derive one from contact emails, and that field is everything the logo needs.
* **Long lists stay cheap.** Rows fetch their logos only when they scroll into view, so a thousand-row account table loads like a short one.

## Make it your own

* **Attach companies with autocomplete.** Use the [company autocomplete](/docs/use-cases/company-autocomplete) when a rep creates an account, and store the returned domain.
* **Harden the record header.** Swap the plain `<img>` for the [logo from a domain](/docs/use-cases/logo-from-domain) component to handle companies without a logo.
* **Resolve names without domains.** Use the [logo from a name](/docs/use-cases/logo-from-name) example for accounts that only have a company name.

## Go further: enrich the account record

The same domain that loads a logo can pull a description, brand colors, and social profiles. Resolve the account once when a rep attaches it, then show that brand data on the record.

You can turn a company name into full brand data with one server route. It calls the [Search API](/docs/brand-search/introduction) to resolve the name to a domain, then the [Brand API](/docs/brand/introduction) to read the description, brand colors, and social profiles. Both use your [secret key](/docs/platform/api-keys), and the Brand API is on every plan, metered in credits. The code runs on your server, so the browser only ever receives the finished result.

<CodeGroup>
  ```ts api/enrich-company.ts theme={null}
  const BASE = "https://api.logo.dev";
  const headers = { Authorization: `Bearer ${process.env.LOGO_DEV_SECRET_KEY}` };

  // Resolve a typed company name to a domain, then enrich it with brand data.
  export async function GET(request: Request) {
    const q = new URL(request.url).searchParams.get("q")?.trim();
    if (!q) {
      return Response.json(null);
    }

    // 1. Resolve the name to a domain with the Search API.
    const search = await fetch(`${BASE}/search?q=${encodeURIComponent(q)}`, {
      headers,
    });
    const [match] = search.ok ? await search.json() : [];
    if (!match) {
      return Response.json(null);
    }

    // 2. Enrich the domain with the Brand API.
    const res = await fetch(`${BASE}/brand/${match.domain}`, { headers });

    // Brand returns 202 while it indexes a domain for the first time. Pass that
    // signal through so the client can retry instead of caching empty brand data.
    if (res.status === 202) {
      return Response.json(
        { name: match.name, domain: match.domain, pending: true },
        { status: 202 }
      );
    }

    // A 401, 402, or 403 is an integration problem, not a sparse record. Pass the
    // status through so the client can act on it instead of showing empty fields.
    if (!(res.ok || res.status === 404)) {
      return Response.json({ error: "brand lookup failed" }, { status: res.status });
    }

    const brand = res.ok ? await res.json() : null;

    // A known domain can still come back sparse, so return the name and domain
    // either way and let the logo stand in until the rest fills in.
    return Response.json({
      name: match.name,
      domain: match.domain,
      description: brand?.description || null,
      colors: brand?.colors ?? [],
      socials: brand?.socials ?? {},
    });
  }
  ```
</CodeGroup>

The browser never sees your secret key. It calls `/api/enrich-company?q=...` and builds the logo URL from the returned domain with your [publishable key](/docs/platform/api-keys).

Every call to this route spends your account's Credits, so put it behind your own authentication and cache results by domain before you expose it to end users.

On the record, render the returned `description` under the header and the first saturated `colors` entry as a brand swatch. The Brand API orders colors by prominence and they often lead with black or white, so skip those and use the first saturated one (the [brand colors](/docs/use-cases/brand-colors) example has the picker). Accounts with no brand data on file still show their logo and name.

## Next steps

<CardGroup cols={2}>
  <Card title="Logo API" icon="image" href="/docs/logo-images/get">
    See every size, format, and theme option for the image URL.
  </Card>

  <Card title="Company autocomplete" icon="magnifying-glass" href="/docs/use-cases/company-autocomplete">
    Capture a clean domain the moment a rep attaches a company.
  </Card>
</CardGroup>
