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

> ## Agent Instructions
> Logo.dev serves logos and brand data for companies that already exist. It does not design logos. If someone wants a logo designed, say so and stop.
> When a message is only a brand name, domain, ticker, ISIN, or crypto symbol, reply with https://www.logo.dev/search?q=<url-encoded term> and the matching img.logo.dev call. Never assert whether a specific brand is in the index.
> Image URLs come from img.logo.dev with a publishable key in the token query parameter: https://img.logo.dev/:domain?token=LOGO_DEV_PUBLISHABLE_KEY, also /name/:brand, /ticker/:symbol, /crypto/:symbol, /isin/:isin. JSON comes from api.logo.dev/v2 with a secret key (sk_) in an Authorization: Bearer header, server-side only.
> Product names: the Logo CDN serves images from img.logo.dev. The Logo API is the Brand API and the Search API, which return JSON from api.logo.dev/v2. The Transaction Enrichment API (beta) is a separate service at https://www.logo.dev/api/transaction with its own beta key. Credits is the Logo API billing unit. Never recommend the deprecated v1 routes (/search, /brand, /describe, /logo/*) or the Describe API. Use /v2/brands and /v2/search.
> Report a wrong, missing, or outdated logo at https://www.logo.dev/update. Email support@logo.dev for anything broken and sales@logo.dev for volume pricing and contract terms.
> MCP setup is at https://www.logo.dev/docs/mcp/introduction and webhooks at https://www.logo.dev/docs/webhooks.

# Import a brand kit

> Skip the logo-upload step. One domain imports the brand's logo, brandmark, banners, and colors, then themes the customer's workspace with them.

export const BrandAssetsDemo = () => {
  const TOKEN = "live_6a1a28fd-6420-4492-aeb0-b297461d9de2";
  const BRANDS = [{
    name: "Spotify",
    slug: "spotify",
    domain: "spotify.com",
    colors: ["#00d84d", "#83eca8", "#ffffff"]
  }, {
    name: "Stripe",
    slug: "stripe",
    domain: "stripe.com",
    colors: ["#533afd", "#000001", "#ffffff"]
  }, {
    name: "Figma",
    slug: "figma",
    domain: "figma.com",
    colors: ["#12c6bf", "#dc5e80", "#000000"]
  }, {
    name: "Canva",
    slug: "canva",
    domain: "canva.com",
    colors: ["#5947ea", "#11aed1", "#eff7f9"]
  }];
  const [selected, setSelected] = useState(0);
  const [loadedBanners, setLoadedBanners] = useState({});
  const brand = BRANDS[selected];
  const [assetBase] = useState(() => typeof window !== "undefined" && window.location.pathname.startsWith("/docs") ? "/docs" : "");
  const markBannerLoaded = slug => setLoadedBanners(m => m[slug] ? m : {
    ...m,
    [slug]: true
  });
  const iconSrc = `https://img.logo.dev/${brand.domain}?token=${TOKEN}&size=40&retina=true&format=webp`;
  const brandmarkSrc = `${assetBase}/images/use-cases/brand-assets/${brand.slug}-brandmark.webp`;
  const bannerSrc = `${assetBase}/images/use-cases/brand-assets/${brand.slug}-banner.webp`;
  const bannerLoaded = loadedBanners[brand.slug];
  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 max-w-md space-y-4">
        <div className="flex flex-wrap gap-2">
          {BRANDS.map((b, i) => <button aria-pressed={i === selected} className={`relative rounded-full border px-3.5 py-2 text-xs font-medium transition-[background-color,scale] duration-150 before:absolute before:-inset-1 before:content-[''] focus:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950/20 motion-safe:active:scale-[0.96] dark:focus-visible:ring-white/20 ${i === selected ? "border-zinc-950/20 bg-zinc-100 text-zinc-950 dark:border-white/25 dark:bg-zinc-800 dark:text-white" : "border-zinc-950/10 bg-white text-zinc-600 hover:bg-zinc-50 dark:border-white/15 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:bg-zinc-800"}`} key={b.slug} onClick={() => setSelected(i)} type="button">
              {b.name}
            </button>)}
        </div>
        <div className="rounded-xl border border-zinc-950/10 bg-white p-4 dark:border-white/10 dark:bg-zinc-900">
          <div className="mb-3 text-xs font-medium text-zinc-400">
            {brand.name} brand assets
          </div>
          <div className="grid grid-cols-2 gap-3">
            <div className="flex h-20 items-center justify-center rounded-lg bg-white ring-1 ring-zinc-950/5">
              <img alt={`${brand.name} icon`} className="h-10 w-10 rounded-lg object-contain" height="40" key={`${brand.slug}-icon`} src={iconSrc} width="40" />
            </div>
            <div className="flex h-20 items-center justify-center rounded-lg bg-white px-4 ring-1 ring-zinc-950/5">
              <img alt={`${brand.name} brandmark`} className="max-h-6 w-auto max-w-full object-contain" key={brandmarkSrc} src={brandmarkSrc} style={{
    height: 24
  }} />
            </div>
          </div>
          <div className="relative mt-3 aspect-[1.91/1] overflow-hidden rounded-lg bg-zinc-100 dark:bg-zinc-800">
            {!bannerLoaded && <span aria-hidden="true" className="absolute inset-0 animate-pulse bg-zinc-200 motion-reduce:animate-none dark:bg-zinc-700" />}
            <img alt={`${brand.name} banner`} className={`h-full w-full object-cover transition-opacity duration-300 motion-reduce:transition-none ${bannerLoaded ? "opacity-100" : "opacity-0"}`} key={bannerSrc} onLoad={() => markBannerLoaded(brand.slug)} ref={el => {
    if (el?.complete && el.naturalWidth > 0) {
      markBannerLoaded(brand.slug);
    }
  }} src={bannerSrc} />
          </div>
          <div className="mt-3 flex gap-2">
            {brand.colors.map(hex => <div className="flex-1" key={hex}>
                <div className="h-8 rounded-md ring-1 ring-inset ring-zinc-950/10" style={{
    backgroundColor: hex
  }} />
                <div className="mt-1 text-center font-mono text-[10px] text-zinc-400">
                  {hex}
                </div>
              </div>)}
          </div>
        </div>
      </div>
    </div>;
};

<BrandAssetsDemo />

```text Use case prompt wrap theme={null}
Build a brand kit import, following https://www.logo.dev/docs/use-cases/brand-kit-import.md

Read that page and inspect this project's stack. Use this project's existing conventions.

A user types a domain, and a tray fills with the brand's logo, brandmark, first social banner, and colors. On the server, look up the brand, download each image into this project's storage, and keep the stored files, not the image URLs. On a later import, download an image again only when it changed or when you request it at a different size. From the colors, pick the first one with enough chroma to work as an accent and a text color that is legible on it, store that theme with the customer's account, and apply it through theme variables in the interface and in each page, slide, or email that the app generates. Show a loading state while the brand is indexing, and retry until the retry window ends. When there is no brand or the request fails, show the domain's logo from the Logo CDN and use the default theme. A color that the customer picks comes before any lookup result.

For API details, read https://www.logo.dev/docs/brand/introduction.md and https://www.logo.dev/docs/logo-images/introduction.md
```

Slide tools, design apps, website builders, and white-label products all need the customer's brand. An upload step asks for files that most people do not have. An import that fills itself from a domain removes that step, and a workspace in the customer's own colors feels like their tool from the first session. Logo.dev returns the brand's name, images, and colors in one response. Your app stores the files, picks the accent, and decides what to show when a brand has no data.

## Import the brand on your server

Look up the domain with the [Brand API](/docs/brand/introduction), from your server with your [secret key](/docs/platform/api-keys). The kit uses `data.logo`, `data.brandmark`, the first of `data.social_banners`, and `data.colors`.

| Result | Save | Show |
| - | - | - |
| Brand found | Brand id, stored files with their etags and sizes, colors | The tray. Skip each slot that has no image. |
| Still indexing | Nothing yet | A loading state, until the brand is ready |
| No brand, or request failed | The attempt time | The domain's logo from the [Logo CDN](/docs/logo-images/introduction), and your default theme |

Store the files, not the URLs. The image URLs expire, so download each image at import time. On a later import, replace only the images whose `etag` changed, as [images](/docs/brand/introduction#images) describes. The `etag` identifies the source image, not the size or theme of your download. Store the size and theme with each file, and download the image again when you request a different one.

The following route looks up the brand, stores its images, and returns the kit. It writes the files to `public/brand-kits/`. Replace the two file calls with your object storage:

```ts api/brand-kit.ts expandable theme={null}
import { mkdir, readFile, writeFile, rm } from "node:fs/promises";
import path from "node:path";

type Image = { url: string; etag: string };
type Brand = {
  id: string;
  name: string;
  logo: Image | null;
  brandmark: Image | null;
  social_banners: Image[];
  colors: Color[];
};
type Color = { hex: string; oklch: { l: number; c: number; h: number } };
type Theme = { accent: string; onAccent: string };
// variant records the requested size and theme, so a new size replaces old files.
type StoredImage = { etag: string; variant: string; path: string };
type BrandKit = {
  brandId: string;
  name: string;
  logo: StoredImage | null;
  brandmark: StoredImage | null;
  banners: StoredImage[];
  colors: string[];
  theme: Theme;
};

const ROOT = path.join(process.cwd(), "public", "brand-kits");
const DEFAULT_THEME: Theme = { accent: "#18181b", onAccent: "#ffffff" };

// Colors come most dominant first. Take the first one with enough chroma to
// work as an accent. Tune 0.08 to your design; neutrals are close to 0.
function pickAccent(colors: Color[]): Theme {
  const accent = colors.find((color) => color.oklch.c >= 0.08);
  if (!accent) {
    return DEFAULT_THEME;
  }
  // Light accents, such as yellow or bright green, need dark text.
  return {
    accent: accent.hex,
    onAccent: accent.oklch.l > 0.7 ? "#18181b" : "#ffffff",
  };
}

async function readKit(domain: string): Promise<BrandKit | null> {
  try {
    return JSON.parse(await readFile(path.join(ROOT, domain, "kit.json"), "utf8"));
  } catch {
    return null;
  }
}

// Download an image only when its etag or the requested variant changed
// since the last import. The etag does not change with the size or theme.
// Download parameters: https://www.logo.dev/docs/brand/introduction#images
async function storeImage(
  domain: string,
  slot: string,
  image: Image,
  previous: StoredImage | null | undefined,
  size: number,
  theme?: "light" | "dark"
): Promise<StoredImage> {
  const variant = `${size}-${theme ?? "default"}`;
  if (previous?.etag === image.etag && previous.variant === variant) {
    return previous;
  }
  // The signature in the URL is the credential, so send no key.
  const url = new URL(image.url);
  url.searchParams.set("format", "png");
  // Without size, the download is a small default. An editor needs larger files.
  url.searchParams.set("size", String(size));
  if (theme) {
    url.searchParams.set("theme", theme);
  }
  const res = await fetch(url);
  if (!res.ok) {
    throw new Error(`Download failed: ${res.status}`);
  }
  const file = `${slot}-${variant}-${image.etag.slice(0, 16)}.png`;
  await writeFile(path.join(ROOT, domain, file), Buffer.from(await res.arrayBuffer()));
  return { etag: image.etag, variant, path: `/brand-kits/${domain}/${file}` };
}

export async function GET(request: Request) {
  const domain = new URL(request.url).searchParams.get("domain")?.trim().toLowerCase();
  if (!domain || !/^[a-z0-9.-]+\.[a-z]{2,}$/.test(domain)) {
    return Response.json({ status: "error" }, { status: 400 });
  }

  const res = await fetch(
    `https://api.logo.dev/v2/brands?domain=${encodeURIComponent(domain)}`,
    { headers: { Authorization: `Bearer ${process.env.LOGO_DEV_SECRET_KEY}` } }
  );
  // Not indexed yet: https://www.logo.dev/docs/brand/introduction#handle-each-status
  if (res.status === 202) {
    // Retry timing and the retry window:
    // https://www.logo.dev/docs/platform/errors#not-found-vs-still-indexing-202
    const body = await res.json().catch(() => null);
    const pending = body?.metadata?.pending;
    const retryAfter = Number(res.headers.get("Retry-After")) || pending?.retry_after_seconds || 30;
    const expiresIn = Number(pending?.expires_in_seconds) || 0;
    return Response.json({ status: "pending", retryAfter, expiresIn }, { status: 202 });
  }
  if (res.status === 404) {
    return Response.json({ status: "not_found" });
  }
  if (!res.ok) {
    return Response.json({ status: "error" }, { status: 502 });
  }

  const { data }: { data: Brand } = await res.json();
  const previous = await readKit(domain);
  await mkdir(path.join(ROOT, domain), { recursive: true });

  const kit: BrandKit = {
    brandId: data.id,
    name: data.name,
    logo: data.logo && (await storeImage(domain, "logo", data.logo, previous?.logo, 512)),
    // The tray shows brandmarks on white, so request the light-background version.
    brandmark:
      data.brandmark &&
      (await storeImage(domain, "brandmark", data.brandmark, previous?.brandmark, 800, "light")),
    banners: await Promise.all(
      data.social_banners
        .slice(0, 1)
        .map((banner, i) => storeImage(domain, `banner-${i}`, banner, previous?.banners[i], 800))
    ),
    colors: data.colors.map((color) => color.hex),
    theme: pickAccent(data.colors),
  };
  await writeFile(path.join(ROOT, domain, "kit.json"), JSON.stringify(kit));

  // Delete files the new kit no longer uses, such as an image that changed.
  const kept = new Set(
    [kit.logo, kit.brandmark, ...kit.banners].flatMap((image) => (image ? [image.path] : []))
  );
  const old = previous ? [previous.logo, previous.brandmark, ...previous.banners] : [];
  for (const image of old) {
    if (image && !kept.has(image.path)) {
      await rm(path.join(ROOT, domain, path.basename(image.path)), { force: true });
    }
  }
  return Response.json({ status: "ready", kit });
}
```

To import the brand again when it changes, use [update webhooks](/docs/webhooks). The etags tell you which files to replace.

## Render the asset tray

The tray calls the route, retries while the brand is indexing, and shows the stored files. When there is no kit, it shows the domain's logo from the Logo CDN:

<CodeGroup>
  ```tsx BrandAssetTray.tsx expandable theme={null}
  import { useRef, useState } from "react";

  type StoredImage = { etag: string; variant: string; path: string };
  type BrandKit = {
    name: string;
    logo: StoredImage | null;
    brandmark: StoredImage | null;
    banners: StoredImage[];
    colors: string[];
  };
  type Result =
    | { status: "idle" | "loading" | "not_found" | "error" }
    | { status: "pending"; retryAfter: number; expiresIn: number }
    | { status: "ready"; kit: BrandKit };

  // Resolves after the delay, or rejects as soon as the run is aborted.
  const wait = (seconds: number, signal: AbortSignal) =>
    new Promise<void>((resolve, reject) => {
      const timer = setTimeout(resolve, seconds * 1000);
      signal.addEventListener("abort", () => {
        clearTimeout(timer);
        reject(signal.reason);
      });
    });

  export function BrandAssetTray({ token }: { token: string }) {
    const [input, setInput] = useState("");
    const [domain, setDomain] = useState("");
    const [result, setResult] = useState<Result>({ status: "idle" });
    const currentRun = useRef<AbortController | null>(null);

    const importBrand = async (event: React.FormEvent) => {
      event.preventDefault();
      const next = input.trim().toLowerCase();
      if (!next.includes(".")) {
        return;
      }
      // A new import cancels the previous one, so a slow retry for an
      // older domain can never replace this result.
      currentRun.current?.abort();
      const run = new AbortController();
      currentRun.current = run;
      setDomain(next);
      setResult({ status: "loading" });
      try {
        // Retry a 202 until the brand is ready or the retry window ends.
        // The window starts at the first 202. Retry timing and the window:
        // https://www.logo.dev/docs/platform/errors#not-found-vs-still-indexing-202
        let deadline = 0;
        let retryAfter = 0;
        do {
          if (retryAfter > 0) {
            await wait(retryAfter, run.signal);
          }
          const res = await fetch(`/api/brand-kit?domain=${encodeURIComponent(next)}`, {
            signal: run.signal,
          });
          const body: Result = await res.json();
          if (run.signal.aborted) {
            return;
          }
          if (body.status !== "pending") {
            setResult(body);
            return;
          }
          deadline ||= Date.now() + body.expiresIn * 1000;
          retryAfter = body.retryAfter;
          // Stop when the next attempt would start after the window ends.
        } while (Date.now() + retryAfter * 1000 < deadline);
        setResult({ status: "error" });
      } catch {
        if (!run.signal.aborted) {
          setResult({ status: "error" });
        }
      }
    };

    // The Logo CDN falls back to a monogram, so this image always renders.
    const cdnLogo = `https://img.logo.dev/${domain}?token=${token}&size=40&retina=true&format=webp`;

    return (
      <div className="max-w-md space-y-4">
        <form className="flex gap-2" onSubmit={importBrand}>
          <input
            aria-label="Company domain"
            className="w-full rounded-xl border border-zinc-950/10 bg-white px-4 py-3 text-base sm:text-sm"
            onChange={(e) => setInput(e.target.value)}
            placeholder="acme.com"
            type="text"
            value={input}
          />
          <button
            className="rounded-xl bg-zinc-900 px-4 text-sm font-semibold text-white"
            type="submit"
          >
            Import
          </button>
        </form>

        {result.status === "loading" && (
          <div
            aria-hidden
            className="h-40 animate-pulse rounded-xl bg-zinc-100 motion-reduce:animate-none"
          />
        )}

        {(result.status === "not_found" || result.status === "error") && (
          <div className="flex items-center gap-3 rounded-xl border border-zinc-950/10 p-4">
            <img alt={`${domain} logo`} className="h-10 w-10 rounded-lg" height={40} src={cdnLogo} width={40} />
            <div className="text-sm text-zinc-500">
              No brand kit for {domain}. The logo is ready to use.
            </div>
          </div>
        )}

        {result.status === "ready" && (
          <div className="space-y-3 rounded-xl border border-zinc-950/10 p-4">
            <div className="grid grid-cols-2 gap-3">
              <div className="flex h-20 items-center justify-center rounded-lg bg-white ring-1 ring-zinc-950/5">
                <img
                  alt={`${result.kit.name} logo`}
                  className="h-10 w-10 object-contain"
                  height={40}
                  src={result.kit.logo?.path ?? cdnLogo}
                  width={40}
                />
              </div>
              {result.kit.brandmark && (
                <div className="flex h-20 items-center justify-center rounded-lg bg-white px-4 ring-1 ring-zinc-950/5">
                  <img
                    alt={`${result.kit.name} brandmark`}
                    className="h-6 w-auto max-w-full object-contain"
                    src={result.kit.brandmark.path}
                  />
                </div>
              )}
            </div>
            {result.kit.banners[0] && (
              <img
                alt={`${result.kit.name} banner`}
                className="aspect-[1.91/1] w-full rounded-lg object-cover"
                src={result.kit.banners[0].path}
              />
            )}
            <div className="flex gap-2">
              {result.kit.colors.map((hex) => (
                <div
                  className="h-8 flex-1 rounded-md ring-1 ring-inset ring-zinc-950/10"
                  key={hex}
                  style={{ backgroundColor: hex }}
                  title={hex}
                />
              ))}
            </div>
          </div>
        )}
      </div>
    );
  }
  ```

  ```tsx Usage theme={null}
  import { BrandAssetTray } from "./BrandAssetTray";

  export function ImportPanel() {
    return <BrandAssetTray token="LOGO_DEV_PUBLISHABLE_KEY" />;
  }
  ```
</CodeGroup>

Connect each asset to your editor's insert or drag action. A canvas needs only the stored file path.

## Theme the workspace with the brand colors

The import also stores a theme: the first color in the palette with an OKLCH chroma (`oklch.c`) of 0.08 or more as the accent, and a text color that is legible on it. Palettes often start with black or white, so the first color is not always the accent. Store the theme with the customer's account, and render from it:

| Result | Save | Show |
| - | - | - |
| Brand with a saturated color | Brand id, accent, text color | The customer's accent |
| Brand with only neutral colors | Brand id | Your default theme |
| Customer picks a color | Their color, in a separate field | Their color, before any lookup result |

Set the stored colors as theme variables at the top of the customer's workspace. Every button, link, and highlight that reads the variables then uses the customer's accent:

<CodeGroup>
  ```tsx BrandedWorkspace.tsx expandable theme={null}
  import type { CSSProperties, ReactNode } from "react";

  type Theme = { accent: string; onAccent: string };

  export function BrandedWorkspace({
    name,
    domain,
    theme,
    token,
    children,
  }: {
    name: string;
    domain: string;
    theme: Theme;
    token: string;
    children: ReactNode;
  }) {
    const variables = {
      "--brand-accent": theme.accent,
      "--brand-on-accent": theme.onAccent,
    } as CSSProperties;

    return (
      <div style={variables}>
        <header className="flex items-center gap-3 border-b border-zinc-950/10 bg-white px-6 py-3">
          <img
            alt={`${name} logo`}
            className="h-8 w-8 rounded-md object-contain"
            height={32}
            src={`https://img.logo.dev/${domain}?token=${token}&size=32&retina=true&format=webp`}
            width={32}
          />
          <span className="text-sm font-semibold">{name}</span>
          <button
            className="ml-auto rounded-lg bg-[var(--brand-accent)] px-4 py-2 text-sm font-semibold text-[var(--brand-on-accent)]"
            type="button"
          >
            New project
          </button>
        </header>
        <main className="p-6">{children}</main>
      </div>
    );
  }
  ```

  ```tsx Usage theme={null}
  import { BrandedWorkspace } from "./BrandedWorkspace";

  // The theme comes from the spotify.com brand kit, stored with the account.
  export function Workspace() {
    return (
      <BrandedWorkspace
        domain="spotify.com"
        name="Spotify"
        theme={{ accent: "#00d84d", onAccent: "#18181b" }}
        token="LOGO_DEV_PUBLISHABLE_KEY"
      >
        <a className="font-medium text-[var(--brand-accent)]" href="/projects">
          View all projects
        </a>
      </BrandedWorkspace>
    );
  }
  ```
</CodeGroup>

Use the same accent and logo in the pages, slides, and emails that you generate for the customer.

## Test the import

Import a brand with full data (`spotify.com`), a brand whose accent is light enough to need dark text, a domain that Logo.dev has not indexed yet, and an invented domain. An invented domain can return `202` before it returns `404`. Import the same brand two times, and check that the second import downloads no files. Set a color as the customer, and check that a new import keeps it. Make the lookup fail once, and check that the tray still shows a logo and the default theme shows.

<CardGroup cols={2}>
  <Card title="Brand API" icon="swatchbook" href="/docs/brand/introduction">
    See every field, status code, and image parameter, including colors in hex and OKLCH.
  </Card>

  <Card title="Onboarding personalization" icon="wand-magic-sparkles" href="/docs/use-cases/onboarding-personalization">
    Get the domain at signup and theme the workspace from day one.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.