Documentation
OnBrand by Canopy Labs is the brand layer for AI agents: extract any brand system, search for style inspiration, and verify that what your agents make stays on brand — over REST, MCP or agent skills.
Quickstart
- Create a key on the API Keys page.
POST /extracta URL, poll/extract/{id}/resultuntil it's completed, and hand the brand (or/brief,/tokens) to your agent.- After your agent ships a page,
POST /adherencewith the brand and the page, apply the returnedfixesandrecommendations, and re-verify.
# 1. start ID=$(curl -s https://brand.trycanopy.space/api/v1/extract -H 'X-API-Key: YOUR_API_KEY' -H 'Content-Type: application/json' \ -d '{ "url": "https://stripe.com" }' | jq -r .id) # 2. poll until completed (409 means nothing has landed yet) curl -s https://brand.trycanopy.space/api/v1/extract/$ID/result -H 'X-API-Key: YOUR_API_KEY' | jq .status # 3. read only what you need curl -s "https://brand.trycanopy.space/api/v1/extract/$ID/result?sections=colors,typography" -H 'X-API-Key: YOUR_API_KEY'
How it works
Renders the page in a real browser, measures computed styles, CSS, DOM structure and screenshots, then turns them into a brand system. Async job.
Ranks brands in the curated OnBrand index by your description or by similarity to one of your extractions. Synchronous.
Extracts a reference and a candidate, measures colour distance, type, surfaces, layout and elevation, then critiques both screenshots. Async job.
Authentication
Send your key in the X-API-Key header (or as Authorization: Bearer ob_live_…). Base URL: https://brand.trycanopy.space/api/v1. Keys are hashed at rest and shown once at creation. /result and /download on public extractions, and /result on public verifications, also work without a key. Errors share one envelope: { "error": { "code", "message" } }.
Brand Extraction
Turn any URL into an agent-ready brand system plus the artifacts it was measured from. Extraction is a job: start it, poll it, read it. Sections stream in as they finish, so you can read partial results while it runs.
/extract2 credits · refunded on failureStarts an extraction and returns 202 with its id (200 when a fresh cached result is reused; cache_hit tells you which). Sections: identity, colors, surfaces, typography, layout, elevation, structure, interactions, navigation, icons, motion, data_display, sections, media, tokens. Selective extractions are never used as a cache source, and can't be combined with map or deep.
{
"url": "https://linear.app", // required
"force": false, // true = skip the cache and re-crawl
"depth": "light" | "deep", // deep adds an AI review pass (full extractions only)
"sections": ["colors", "typography"], // optional: extract only these sections
"map": false, // true = also extract same-domain pages
"max_pages": 20 // map mode cap (max 50)
}curl https://brand.trycanopy.space/api/v1/extract \ --header 'X-API-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "url": "https://linear.app" }'
/extract/{id}/result?sections=colors,typographyfreeThe brand system and artifact links. While the job runs you get 200 with whatever has landed, or 409 not_ready before the first section. Keep polling until status is completed or failed. ?sections= narrows the payload, which matters for agents because a full system is large. Public extractions are readable without a key.
curl 'https://brand.trycanopy.space/api/v1/extract/ext_8h2k.../result?sections=colors,typography' -H 'X-API-Key: YOUR_API_KEY'
{
"id": "ext_8h2k...", "object": "extraction", "status": "completed",
"url": "https://linear.app/", "sections": null, "cache_hit": false, "is_public": false,
"palette": ["#08090A", "#F7F8F8", "#5E6AD2"],
"artifacts": { "screenshot": "...", "hero": "...", "html": "...", "css": "..." },
"brand": {
"colors": { "baseline": [{ "name": "Void", "hex": "#08090A", "shades": [...] }], "secondary": [...] },
"typography": { "families": [...], "titles": [...], "body": [...], "labels": [...] }
}
}/extract/{id}freeStatus, per-stage progress and partial results. Add ?fields=status for the lightest poll.
curl https://brand.trycanopy.space/api/v1/extract/ext_8h2k...?fields=status -H 'X-API-Key: YOUR_API_KEY'
/extract?status=&q=&api_key_id=&limit=&offset=freeYour extractions, newest first, with total and totals (completed / failed / in_progress) for the whole filter.
curl 'https://brand.trycanopy.space/api/v1/extract?status=completed&limit=20' -H 'X-API-Key: YOUR_API_KEY'
/extract/{id}/downloadfreeA manifest of every file in a completed extraction: brand.json and tokens.json inline, plus URLs for the captured HTML, CSS, screenshots and media. Files are stored on our side, so links survive changes to the origin site. Prefer a zip? Use /extract/{id}/bundle.
curl https://brand.trycanopy.space/api/v1/extract/ext_8h2k.../download -H 'X-API-Key: YOUR_API_KEY'
/extract/{id}/visibilityfreeMake an extraction readable from /result and /download without a key, e.g. to share it. Only the owner can change this.
{ "is_public": true }curl -X PATCH https://brand.trycanopy.space/api/v1/extract/ext_8h2k.../visibility -H 'X-API-Key: YOUR_API_KEY' -H 'Content-Type: application/json' -d '{ "is_public": true }'
/extract/{id}/tokens?format=json|css|tailwindfreeDesign tokens as JSON, CSS custom properties or a Tailwind v4 @theme block.
curl 'https://brand.trycanopy.space/api/v1/extract/ext_8h2k.../tokens?format=tailwind' -H 'X-API-Key: YOUR_API_KEY'
/extract/{id}/brieffreeA compact markdown brief of the brand, ready to drop into an agent's context.
curl https://brand.trycanopy.space/api/v1/extract/ext_8h2k.../brief -H 'X-API-Key: YOUR_API_KEY'
/enhancefreeRewrites a prompt so it's grounded in the brand's exact colours, type, layout and components. Synchronous. Needs a completed full extraction.
{ "extraction_id": "ext_8h2k...", "prompt": "Design a pricing page with three tiers" }curl https://brand.trycanopy.space/api/v1/enhance -H 'X-API-Key: YOUR_API_KEY' -H 'Content-Type: application/json' \ -d '{ "extraction_id": "ext_8h2k...", "prompt": "Design a pricing page" }'
Style Search
No brand yet? Describe a look in plain language and get ranked, real brands from the OnBrand index, each with a screenshot, palette, type pairing and taxonomy tags. Search is synchronous: nothing to poll. A card points into the index; extract its url to get the full system.
/searchfast 1 credit · deep 2 creditsReturns ranked cards with match (strong, good, related) and, on unfiltered searches, one rotating badge: "discovery" exemplar so repeat searches widen your set. Filters are hard constraints from a closed vocabulary (below); unknown values return 422.
{
"query": "dark bold creative studio with expressive typography",
"depth": "fast" | "deep", // deep re-ranks with AI and explains each match
"top_k": 6, // 1-30
"filters": { // optional hard constraints
"page_type": "homepage",
"industry": "creative_agency",
"hue": "red",
"layout": "asymmetric_broken_grid"
}
}curl https://brand.trycanopy.space/api/v1/search \ -H 'X-API-Key: YOUR_API_KEY' -H 'Content-Type: application/json' \ -d '{ "query": "warm pastel skincare landing page", "top_k": 6 }'
{
"object": "search", "id": "srch_...", "depth": "fast", "query_tags": ["Warm", "Skincare"],
"results": [{
"url": "https://aesop.com", "brand_name": "aesop", "match": "strong", "badge": null,
"identity_paragraph": "...", "reason": null,
"tags": ["Earthy", "Editorial", "Luxury"], "traits": ["Luxury", "Editorial", "Minimal"],
"palette": { "primary": ["#FFFEF2", "#333333", "#B5A48B"], "secondary": [], "mode": "light" },
"typography": "Suisse Intl + Zapf Humanist", "screenshot_url": "..."
}]
}/search/similar?extraction_id=&top_k=1 creditBrands in the index that look like one of your completed full extractions: palette distance, mode, type and keywords. Good for competitor sets and moodboards.
curl 'https://brand.trycanopy.space/api/v1/search/similar?extraction_id=ext_8h2k...&top_k=12' -H 'X-API-Key: YOUR_API_KEY'
/search?kind=search|similar&depth=&q=&limit=&offset=freeYour search history, newest first, with total and total_by_status.
curl 'https://brand.trycanopy.space/api/v1/search?limit=20' -H 'X-API-Key: YOUR_API_KEY'
Verify Adherence
Give two URLs: the reference brand (the standard) and a candidate page (a rebuild, a generated page, a redesign). Both sides are extracted automatically and the candidate is judged against the reference. The verdict is one score from 0 to 1, recommendations in prose, and structured fixes with exact target values. Both lists are worst-first, so they double as a work queue for an agent loop.
/adherence2 credits · both extractions included · refunded on failureStarts a verification and returns 202 with its id. The candidate must be a public URL.
{ "reference_url": "https://linear.app", "candidate_url": "https://my-redesign.vercel.app" }curl https://brand.trycanopy.space/api/v1/adherence \ -H 'X-API-Key: YOUR_API_KEY' -H 'Content-Type: application/json' \ -d '{ "reference_url": "https://linear.app", "candidate_url": "https://my-redesign.vercel.app" }'
/adherence/{id}/resultfreeThe verdict. 409 not_ready while both pages are extracting; 424 adherence_failed if the run failed (stop polling; credits are refunded). Public runs are readable without a key.
curl https://brand.trycanopy.space/api/v1/adherence/adh_.../result -H 'X-API-Key: YOUR_API_KEY'
{
"object": "adherence_verdict", "status": "completed", "score": 0.78, "grade": "C",
"recommendations": [
"Set headline type in Inter Display, sans-serif instead of Arial.",
"Change the background-color from #FFFFFF to #08090A (--void)."
],
"fixes": [
{ "action": "replace_font_family", "category": "typography", "property": "font-family", "role": "headline", "from": "Arial", "to_value": "Inter Display, sans-serif" },
{ "action": "snap_to_token", "category": "colors", "property": "background-color", "from": "#FFFFFF", "to_value": "#08090A", "token": "--void" },
{ "action": "add_color_token", "category": "colors", "token": "--accent", "value": "#5E6AD2", "role": "Accent", "usage": ["Buttons"] }
],
"categories": [{ "key": "colors", "label": "Colours", "score": 0.84, "deviations": [...] }, ...]
}/adherence/{id}freeStatus and stages. Add ?include=sides for both brand systems.
curl https://brand.trycanopy.space/api/v1/adherence/adh_... -H 'X-API-Key: YOUR_API_KEY'
/adherence?status=&q=&limit=&offset=freeYour verifications, newest first, with total.
curl 'https://brand.trycanopy.space/api/v1/adherence?limit=20' -H 'X-API-Key: YOUR_API_KEY'
/adherence/{id}/visibilityfreeShare a verdict: public runs are readable from /result without a key.
{ "is_public": true }curl -X PATCH https://brand.trycanopy.space/api/v1/adherence/adh_.../visibility -H 'X-API-Key: YOUR_API_KEY' -H 'Content-Type: application/json' -d '{ "is_public": true }'
Account
Balance and usage for the key's owner.
/mefreeProfile, plan and credit balance.
curl https://brand.trycanopy.space/api/v1/me -H 'X-API-Key: YOUR_API_KEY'
/usage?feature=extraction&days=30freeDaily usage series, totals and latency.
curl 'https://brand.trycanopy.space/api/v1/usage?days=7' -H 'X-API-Key: YOUR_API_KEY'
MCP server
Streamable-HTTP MCP endpoint at https://brand.trycanopy.space/api/mcp, authenticated with your API key as a bearer token. The tools mirror the API one to one, so an agent can run the whole extract → build → verify loop in one conversation.
claude mcp add --transport http onbrand https://brand.trycanopy.space/api/mcp \ --header "Authorization: Bearer YOUR_API_KEY"
extract_brand(url, force?, sections?, map?)Start an extraction; returns extraction_id.poll_brand_extraction(extraction_id)Lightweight status + which sections have landed.get_brand_extraction_result(extraction_id, sections?)Read the brand system; narrow with sections.list_brand_extractions(status?, search?)Your past extractions.get_brand_brief / get_brand_tokens / enhance_promptMarkdown brief, CSS · Tailwind · JSON tokens, grounded prompts.search_brands(query, depth?, top_k?, filters?)Find brands by a description of a look.search_similar_brands(extraction_id, top_k?)Visual neighbours of a brand you extracted.verify_brand_adherence(reference_url, candidate_url)Start a verification; returns adherence_id.poll_brand_adherence / get_brand_adherence_resultStatus, then score + recommendations + fixes.list_brand_adherence_jobs / get_creditsHistory and balance.Agent skills
Skills teach skill-aware agents (Claude Code, Codex, Cursor and others) how to use the tools well. onbrand-search finds real references before the agent commits to a look and inspects every result. onbrand-adherence builds a page from a brand's exact values, then verifies it and applies the fixes. Install the skills, and connect the MCP server so they have tools to call.
npx skills add PrateekDevashetti/OnBrand-API
TypeScript SDK
import { OnBrand } from "@canopylabs/onbrand";
const onbrand = new OnBrand({ apiKey: process.env.ONBRAND_API_KEY });
const ext = await onbrand.extract({ url: "https://linear.app", wait: true });
const brand = await onbrand.result(ext.id, { sections: ["colors", "typography"] });
const tokens = await onbrand.tokens(ext.id, "tailwind");
const { results } = await onbrand.search({ query: "warm editorial fintech", filters: { industry: "fintech" } });
const verdict = await onbrand.verify({ reference_url: "https://linear.app", candidate_url: "https://my-redesign.vercel.app", wait: true });
console.log(verdict.score, verdict.fixes, verdict.recommendations);Filter vocabulary
Search filters are hard constraints matched against this closed vocabulary.
page_typehomepage · about · pricing · careers · portfolio · product · blog · case_study · landing_page · servicesindustrysaas · ai · developer_tools · creative_agency · branding · design · fintech · crypto · e_commerce · fashion · beauty · food_beverage · hardware · travel · education · media · productivity · architecturehuered · orange · yellow · green · teal · blue · purple · pink · brown · neutrallayoutgenerous_whitespace · dense_packed · asymmetric_broken_grid · grid_based_strict · centered_symmetric · card_basedErrors & limits
400 invalid_request · invalid_jsonBody failed validation; the message names the field.401 unauthorizedMissing, invalid or revoked API key.402 insufficient_creditsNot enough credits. Includes balance and needed. Failed jobs are refunded automatically.404 not_foundUnknown id, or it belongs to another account and isn't public.409 not_readyThe job hasn't produced anything yet. Keep polling (retry_after_ms is a hint).422 invalid_sections · invalid_filterA section name or filter value isn't in the vocabulary.422 selective_map_unsupportedsections can't be combined with map mode or deep depth.422 full_extraction_requiredSimilar search and enhance need a full extraction.422 same_urlReference and candidate are the same page.424 adherence_failedThe verification failed for good. Stop polling; credits are refunded.500 internal_errorSomething broke on our side. Retries are safe.
