BrandAttach docs

One verified token and done. Three ways to attach a brand: JavaScript, CSS, or REST.

Try the integration without writing any code. Open the test sites — three standalone HTML pages (customer portal, analytics dashboard, email preview), one per integration method. Brand them manually first, then paste a BrandAttach token to watch the same surface repaint.

Quickstart

  1. Create an account.
  2. Scan a domain to create a brand profile. Verify the domain to enable live tokens.
  3. Register your SaaS app in Apps and create an app token.
  4. In your app, accept a customer's BrandAttach brand token. Pass both tokens to BrandAttach.

Integrate with your AI assistant

Using Claude, Copilot, Cursor or similar? Copy the prompt for your side of the integration, paste it into your assistant with your codebase open, and fill in the placeholders. Each prompt carries everything the assistant needs to know about BrandAttach, so it can focus on fitting it into your code.

SaaS app developers — show your customers' brands

Adds a brand-token setting, applies the brand across your UI, and falls back to your own theme on any error. Register your app and create an app token in Apps first.

I'm integrating BrandAttach (https://brandattach.com/) into this app so each customer sees their own brand (logo, colours, fonts) inside our product. Read this codebase first, then implement it the way this project already does things.

HOW BRANDATTACH WORKS
- Each customer gives us a brand token (ba_live_… or ba_test_…). It's per customer and designed to be used in the browser.
- We have one app token for our product (ba_app_live_… or ba_app_test_…). It identifies our app to BrandAttach.
- Brand JSON: GET https://brandattach.com/api/brand/<brandToken> with header "Authorization: Bearer <appToken>".
  Returns: brand {id, name, domain, description, status, version}; logos {primary, horizontal, square, icon, favicon, light, dark, bimi} each {url, type, format, aspectRatio, source} (bimi may be null); logoPlacements {navbar, footer, login, avatar, favicon, email, app_icon, card} each {width, height, fit} — recommended display size only, get the image from the logo endpoint below; colors {primary, accent, background, surface, text, muted, border, success, warning, error, info}; fonts {primary, fallback, source}; radius {sm, md, lg}; initials; cache {ttl, staleWhileRevalidate, version, compiledAt}. Missing values are already filled with safe fallbacks.
- Theme stylesheet: https://brandattach.com/api/brand/<brandToken>/theme.css — CSS variables --ba-color-primary, --ba-color-accent, --ba-color-background, --ba-color-surface, --ba-color-text, --ba-color-muted, --ba-color-border, --ba-font-primary, --ba-radius-sm/md/lg, plus helper classes .ba-button, .ba-text-primary, .ba-bg-primary.
- Logo: https://brandattach.com/api/brand/<brandToken>/logo?slot=navbar — redirects to the right image for that slot; use it directly as an <img src>. Optional width, height (max 2048), fit (contain|cover|pad) and format (svg|png|webp|auto). Never stretch logos (object-fit: contain).
- Browser drop-in alternative: <script src="https://brandattach.com/attach.js" data-brand="<brandToken>" data-app="<appToken>"></script>, then <img data-ba-logo="navbar">, <span data-ba-name></span>. It dispatches a "brandattach:ready" event on document and exposes window.BrandAttach.brand and window.BrandAttach.refresh().
- Optional ?use_case=… on any endpoint (e.g. customer_portal, login, email). login, payment, email and advertising need the brand owner's approval by default.
- Errors are JSON {error, message}: invalid_token (404 — unknown, revoked or expired brand token); origin_not_allowed, missing_app_token, invalid_app_token, app_not_active, brand_access_denied, brand_approval_required (403 — may include approvalRequestId); unverified_quota_exceeded (429).
- The app token identifies our app. Always send it: brands set to "Require approval" or "Private" refuse requests without one (missing_app_token). Send it as "Authorization: Bearer <appToken>" on the JSON endpoint, and as ?app=<appToken> on theme.css and logo URLs, because <link> and <img> can't send headers (attach.js does this automatically).
- In the browser the app token is visible in page source; it's protected by the origins we register for our app, and a live app token is refused in browsers until at least one origin is registered. Server-to-server requests (no Origin header) are allowed, so prefer calling from our backend where the page is server-rendered, and keep the app token in server config there.
- High-risk use cases (login, login_page, payment, email, advertising) need the brand owner's explicit approval even after our app is auto-approved for other use cases.

WHAT I'D LIKE
1. Add a "BrandAttach brand token" field to customer/workspace settings, stored with the customer. Validate the prefix (ba_live_ or ba_test_).
2. Read our app token from configuration (e.g. BRANDATTACH_APP_TOKEN) — never hard-code it.
3. Apply the brand wherever customers see our product. Server-rendered: fetch the JSON on the server, cache it for cache.ttl seconds (respect ETag), and link theme.css. Single-page app: use attach.js or a small hook/provider.
4. If there's no token or any request fails, fall back to our default theme — BrandAttach must never break the page or block rendering.
5. On brand_approval_required, show the customer a clear message that their brand owner needs to approve our app in BrandAttach.
6. Add tests for the fallback and error handling, and list the origins I need to register for our app in the BrandAttach dashboard.

Brand owners — verify your domain

Adds the verification tag or file to your website so you can issue live tokens. Get your code from your brand's Verify domain page in the dashboard.

We use BrandAttach (https://brandattach.com/) to publish our official brand so the SaaS tools we use display it correctly. I need to prove we own <OUR_DOMAIN>. The verification code from our BrandAttach dashboard is: <PASTE_CODE — starts with ba-verify->

Read this codebase and add ONE of the following, whichever fits how this site is built and deployed:
- A meta tag in the <head> of the homepage served at https://<OUR_DOMAIN>/:
  <meta name="brandattach-verify" content="<CODE>">
- Or a plain-text file at https://<OUR_DOMAIN>/.well-known/brandattach-verify whose body is just the code. Make sure the framework/router/web server serves it as-is: no HTML wrapper, no auth, and not blocked by .well-known handling.

Requirements:
- It must be live on the production domain over HTTPS (redirects such as apex → www are followed, up to 3 hops).
- Put it in source, not in generated build output, so it survives the next deploy.
- Leave it in place after verification succeeds.
- Tell me how to deploy it and give me a curl command to confirm it's live before I click "Check" in BrandAttach.

If this repo doesn't serve <OUR_DOMAIN>, say so. The alternative is a DNS TXT record at _brandattach.<OUR_DOMAIN> with the value "ba-verify=<CODE>".

Always review what your assistant changes before deploying. Neither prompt asks it to handle any secret beyond your app token.

For SaaS apps

Register your app once. Support every customer's brand token.

  1. Register your app in Apps.
  2. Add the origins where your app runs (e.g. https://*.vendor.com).
  3. Create a ba_app_live_… token. It identifies your app rather than acting as a password: in browser integrations it's visible in page source and protected by your app's registered origins. Browsers can't use a live app token until you've registered at least one origin. For server-rendered calls, keep it in server config.
  4. Add a "BrandAttach token" field to your customer settings.
  5. When rendering, combine the customer's brand token with your app token. If the response is 403 brand_approval_required, prompt the customer to ask their brand owner to approve your app.

<link> and <img> tags can't send headers, so pass the app token on theme.css and logo URLs as ?app=ba_app_…. attach.js does this for you.

<script
  src="https://brandattach.com/attach.js"
  data-brand="ba_live_<customer_brand_token>"
  data-app="ba_app_live_<your_app_token>">
</script>

<img data-ba-logo="navbar" alt="Company logo">
<img data-ba-logo="avatar" alt="">
<span data-ba-name></span>
<button class="ba-button">Continue</button>

Backend (server-rendered) example:

GET https://brandattach.com/api/brand/{{ customer.brandAttachToken }}
Authorization: Bearer {{ appToken }}

For brand owners

  • Verify your domain in the dashboard so you can issue live tokens.
  • Choose an access policy: Allow registered apps (recommended for most brands), Require approval, or Private.
  • Review which apps are using your brand on its Apps page, and approve, revoke or block any of them at any time.
  • Changed your logo or colours? Rescan the brand to pick up the new ones.
  • Use /report to flag misuse.

JavaScript snippet

One line. Attaches CSS variables, replaces logos and brand names, exposes window.BrandAttach.

<script
  src="https://brandattach.com/attach.js"
  data-brand="ba_live_<customer_brand_token>"
  data-app="ba_app_live_<your_app_token>">
</script>

<img data-ba-logo="navbar" alt="Company logo">
<img data-ba-logo="avatar" alt="">
<span data-ba-name></span>
<button class="ba-button">Continue</button>
  • data-ba-logo="<slot>" — sets an <img>'s src, or the background image of any other element.
  • data-ba-name — filled with the brand name.
  • data-ba-color="<role>" — sets the element's text colour to that brand colour.
  • data-use-case on the script tag — declares your use case (see Security model).
  • window.BrandAttach.brand holds the brand JSON; call window.BrandAttach.refresh() to re-fetch. A brandattach:ready event fires on document each time it loads.

The script never stretches images: it sets object-fit: contain; max-width: 100%; height: auto;. An alt you set is kept; if there's none, it becomes "<brand name> logo".

CSS-only integration

For server-rendered apps, link the theme stylesheet and use the CSS variables / utility classes.

<link rel="stylesheet" href="https://brandattach.com/api/brand/ba_live_xxx/theme.css?app=ba_app_live_yyy">

<button class="ba-button">Continue</button>
<p style="color: var(--ba-color-primary)">Brand colour</p>

Available variables:

  • --ba-brand-name
  • --ba-color-primary, --ba-color-accent, --ba-color-background, --ba-color-surface, --ba-color-text, --ba-color-muted, --ba-color-border
  • --ba-color-success, --ba-color-warning, --ba-color-error, --ba-color-info
  • --ba-font-primary
  • --ba-radius-sm, --ba-radius-md, --ba-radius-lg
  • --ba-logo-primary, --ba-logo-login, --ba-logo-avatar, --ba-logo-footer

Helper classes:

  • .ba-button, .ba-text-primary, .ba-bg-primary, .ba-text-accent, .ba-bg-accent, .ba-font
  • .ba-logo-navbar, .ba-logo-footer, .ba-logo-login, .ba-logo-avatar

REST API

GET https://brandattach.com/api/brand/ba_live_xxx
Authorization: Bearer ba_app_live_yyy

200 OK
{
  "brand": { "id": "clx…", "name": "Acme", "domain": "acme.com", "description": "…", "status": "live", "version": 7 },
  "logos": {
    "primary": { "url": "…", "type": "logo_horizontal", "format": "svg", "aspectRatio": 4, "source": "scan" },
    "horizontal": { … }, "square": { … }, "icon": { … }, "favicon": { … },
    "light": { … }, "dark": { … }, "bimi": null
  },
  "logoPlacements": {
    "navbar": { "width": 160, "height": 40, "fit": "contain" },
    "avatar": { "width": 96,  "height": 96, "fit": "contain" },
    …footer, login, favicon, email, app_icon, card
  },
  "colors": {
    "primary": "#0057FF", "accent": "#FFB000", "background": "#FFFFFF",
    "surface": "#F8FAFC", "text": "#101828", "muted": "#667085",
    "border": "#EAECF0", "success": "#12B76A", "warning": "#F79009",
    "error": "#F04438", "info": "#0BA5EC"
  },
  "fonts": { "primary": "Inter", "fallback": "system-ui, …", "source": "google" },
  "radius": { "sm": "4px", "md": "8px", "lg": "16px" },
  "rules": { "doNotStretchLogo": true, "minimumLogoWidth": 120, "preferLogoOnWhite": true, "allowDarkMode": true },
  "trust": { "verified": true, "verifiedDomain": "acme.com" },
  "quality": { "completenessScore": 92, "status": "live_ready", "missing": [], "warnings": [], "blocking": [] },
  "permissions": {
    "environment": "live", "brandVerified": true, "appRegistered": true, "appName": "Vendor",
    "approvalStatus": "auto_approved",
    "allowedUseCases": [], "originStatus": "allowed"
  },
  "cache": { "ttl": 300, "staleWhileRevalidate": 600, "version": "brand_clx…-v7", "compiledAt": "…" },
  "assets": { "css": "…/theme.css", "json": "…" },
  "initials": "AC", "sources": { … }, "voice": { … }
}

The brand endpoints take the brand token in the path; /attach.js reads it from the script tag's data-brand. All of them can be used directly from browsers — via <script>, <link>, <img>, and fetch for the JSON endpoint.

MethodPathWhat it does
GET/api/brand/<token>Complete brand JSON, with fallbacks, version, and cache headers
GET/api/brand/<token>/theme.cssCSS variables + helpers
GET/api/brand/<token>/logo?slot=…Placement-aware logo (302 to the processed asset, or a generated initials SVG if the brand has no logos)
GET/attach.jsDrop-in JS snippet

Logo placements

BrandAttach classifies every logo by shape and serves the right one for each UI slot. Logos are never stretched; fit=cover crops to fill instead.

SlotIdeal shapeUsed in
navbarhorizontalSite header, app top bar
footerhorizontalPage footer
loginhorizontal or squareLogin / signup cards
avataricon / squareProfile chip, badge
faviconiconBrowser tab
emailhorizontalTransactional email header
app_iconicon / squarePWA, mobile shortcut
cardhorizontal or squareMarketing cards, hero
<img data-ba-logo="navbar" alt="Company logo">
<img data-ba-logo="avatar" alt="Company icon">
GET https://brandattach.com/api/brand/ba_live_xxx/logo?slot=navbar
→ 302 to the processed PNG/WebP (or the original SVG) for that slot

Optional: format=svg|png|webp|auto  width=…  height=…  (max 2048)
          fit=contain|cover|pad  use_case=…
If the brand has no logos at all: 200 image/svg+xml initials mark,
with header X-BA-Fallback: generated_initials

If a slot has no assigned logo, you get the best match from the brand's other logos (header X-BA-Fallback: true). If the brand has no logos at all, you get a generated initials SVG, so your UI never breaks.

BIMI Connect

BrandAttach uses BIMI as an additional trust signal and as a source of square logo assets. We do not issue VMC/CMC certificates, and BIMI alone does not guarantee inbox display in any mail client.

  • Check your DNS for a BIMI record at default._bimi.<domain>.
  • Validate DMARC and SPF readiness.
  • Import your BIMI logo as a logo_bimi BrandAttach asset for avatar/email slots (your primary logo is never overwritten without explicit confirmation).
  • Verified-mark display in Gmail/Yahoo needs a VMC (Verified Mark Certificate) or CMC (Common Mark Certificate). BrandAttach shows the certificate URL from your BIMI record (a=) but does not issue or validate certificates.

React example

// React example. The app token is visible to the browser here — that's
// expected for client-side use; it's protected by your app's registered origins.
import { useEffect, useState } from "react";

export function useBrand(brandToken: string, appToken: string) {
  const [brand, setBrand] = useState<any>(null);
  useEffect(() => {
    const ctrl = new AbortController();
    fetch(`https://brandattach.com/api/brand/${brandToken}`, {
      headers: { Authorization: `Bearer ${appToken}` },
      signal: ctrl.signal,
    })
      .then((r) => (r.ok ? r.json() : Promise.reject(r)))
      .then(setBrand)
      .catch(() => setBrand(null)); // fall back to your default theme
    return () => ctrl.abort();
  }, [brandToken, appToken]);
  return brand;
}

Security model

BrandAttach's rule is one verified token and done. The safety system sits behind the scenes so the integration stays a single script tag, but every guardrail below applies to the public APIs.

Test vs live tokens

  • ba_test_… — for development. Scoped to one brand, with no origin restriction.
  • ba_live_… — for production. Requires a verified brand domain and at least one allowed origin (or an explicit "unrestricted" setting).
  • ba_app_test_… / ba_app_live_… — identify the SaaS app making the request. Send it with the brand token so the brand's access policy and approvals apply to your app.
  • Unverified brands are limited to 50 distinct viewers per 24 hours; beyond that, requests return 429 unverified_quota_exceeded. Verify the domain to lift the cap.

Brand access policy

Brand owners choose how registered apps can use their brand:

  • Allow registered apps — registered apps can use the brand by default; brand owners can revoke at any time. Requests without an app token are also served, limited by the brand token's allowed origins.
  • Require approval — an app's first request creates a pending approval (403 brand_approval_required) until the owner approves it.
  • Private — new apps are blocked; only apps the owner manually approves on the brand's Apps page can use it.

Under Require approval and Private, every request must identify an app with an app token, or it gets 403 missing_app_token. The exceptions are the brand's own domain and its subdomains, so the owner's own sites keep working, and localhost for test tokens.

Apps declare a use case with ?use_case= or data-use-case. High-risk use cases (login, login_page, payment, email, advertising) need the owner's explicit approval on every policy, even for an app that was auto-approved for ordinary use. A token with an allowed-use-case list rejects requests that don't declare one of them. Owners grant approval with Approve all use cases on the Apps page.

Domain verification

Verify ownership of the brand domain in the dashboard. Three methods:

  • DNS TXT: _brandattach.<domain> with value ba-verify=<code>.
  • Meta tag: <meta name="brandattach-verify" content="ba-verify-…">.
  • Well-known file: the code as plain text at https://<domain>/.well-known/brandattach-verify.

Origin restrictions

Live tokens carry an allowlist of origins. Registered apps also carry their own list of origins. The public APIs read the request's Origin header (falling back to Referer) and reject anything outside both lists. Wildcard patterns are supported:

https://app.acme.com    // exact origin
https://*.acme.com      // any subdomain (not acme.com itself)
*.acme.com              // protocol-agnostic wildcard
acme.com                // bare host, https implied

Requests with neither Origin nor Referer are treated as server-to-server and allowed — the threat model is browser-based misuse.

Caching + TTL

  • Access is decided per caller, so gated responses are private: browsers cache them, shared caches and CDNs don't.
  • Brand JSON: private, max-age=300, stale-while-revalidate=600, with ETag and Vary: Authorization, Origin.
  • theme.css: private, max-age=900, stale-while-revalidate=2400, with ETag.
  • Logo endpoint (redirect or generated SVG): private, max-age=300. The image files it redirects to are content-addressed: public, max-age=31536000, immutable.
  • Denied responses use private, no-store.
  • The brand snapshot recompiles automatically on every edit. Owners can also rescan the domain from the brand page.

Revocation

  • Revoked brand tokens return 404 invalid_token; revoked app tokens return 403 invalid_app_token. The brand owner sees both attempts in the brand's Usage log.
  • Access is checked on every request, so revocations apply immediately at BrandAttach. A browser may keep using an earlier allowed response until its cache lifetime ends (max-age plus stale-while-revalidate).

Usage logging

Every brand JSON, theme.css and logo request is logged with status (allowed / denied), endpoint, token, origin, app and deny reason. Brand owners see requests on each brand's Usage page, and per-app approvals and origins on its Apps page.

Misuse reports

Anyone can report a token being used to impersonate a brand at /report.

Domain scanning

  • Scans are SSRF-safe: private, loopback, link-local and cloud-metadata addresses and non-HTTP(S) URLs are blocked, and every redirect is re-checked.
  • Requests are bounded by timeout (≤9s), response size (≤2MB), and at most 3 redirects.
  • If a scan fails or detection is poor, fill in the brand manually — every field is editable.
  • Changed your logo or colours? Use Rescan on the brand page to pick up new logos. Existing placements are kept, and colours and fonts are only replaced if you choose to.