Skip to content

The Koala AI MCP is out of beta. Every paid plan can connect an AI assistant to 62 SEO tools and KoalaWriter.

Set it up →

Brands API

Create, read, update and delete Brand DNA brands from your own system, then write articles in each brand's voice through the KoalaWriter API. A brand holds what the writer knows about a business: its name and type, its website, its target market and a knowledge base of documents about what it sells.

It's the same brand you see in the app: brands made in Brand DNA show up in the API, and brands made through the API appear in Brand DNA, where their writer settings, content calendar and Google connections are managed.

Authentication

Send your API key (how to get one) as a Bearer token: Authorization: Bearer YOUR_API_KEY. The Brands API is included with every paid plan, at no extra cost. Requests with a body need Content-Type: application/json.

Endpoints

MethodPathWhat it does
GET/api/brands/List your brands
POST/api/brands/Create a brand, from its website or your own details
GET/api/brands/builds/{buildId}Check a website build, and get its brand once it succeeds
GET/api/brands/{brandId}Get one brand
PATCH/api/brands/{brandId}Update a brand
DELETE/api/brands/{brandId}Delete a brand

The brand object

{
  "brandId": "3f2c9a4e-8b1d-4c6a-9e2f-7a5b1c0d4e8f",
  "name": "Blue Spruce",
  "businessName": "Blue Spruce Heating & Air",
  "businessType": "local_business",
  "businessSubtype": "HVAC contractor",
  "domain": "bluesprucehvac.com",
  "country": "us",
  "goals": ["Get more leads", "Rank for service-area pages"],
  "knowledgeBase": {
    "documents": [
      { "name": "Business Overview", "content": "Blue Spruce installs and repairs furnaces..." }
    ],
    "totalCharacters": 18342
  },
  "internalLinking": { "domainId": "45be96c5-afcc-42ba-9a6e-e90c8ccbcf3e" },
  "autopilot": { "enabled": true },
  "connections": {
    "searchConsole": { "connected": true, "property": "sc-domain:bluesprucehvac.com" },
    "analytics": { "connected": false, "property": null }
  },
  "createdAt": "2026-10-01T15:04:12.000Z"
}
FieldTypeDescription
brandIdstringThe brand's id. Pass it as brandId to the KoalaWriter API.
namestringLabel for the brand in the app. Falls back to businessName, then the domain, when no separate label is set. Articles never use it.
businessNamestringThe name articles use for the business. Up to 120 characters.
businessTypestringOne of local_business, saas, professional_services, online_service, ecommerce, content_site, affiliate. It picks the brand's starting writer defaults.
businessSubtypestring | nullFree-text detail, like "HVAC contractor". Up to 120 characters.
domainstringThe website, stored as a bare hostname (https://www.Example.com/about becomes example.com).
countrystring | nullTarget market as a two-letter code such as us, uk or de. Full country names are accepted on input. The United Kingdom is uk (gb is accepted and stored as uk). Null means the United States.
goalsstring[]Growth goals that steer the Growth Agent. Up to 20, each up to 200 characters.
knowledgeBaseobjectdocuments is a list of { name, content } documents the writer reads about the business. totalCharacters is read only.
internalLinkingobjectRead only. The KoalaLinks index of the brand's site that articles draw internal links from. Created automatically.
autopilotobjectRead only. Whether the brand's content calendar is switched on in the app. New brands start switched on, but nothing is written until someone creates a content plan for the brand in the app, so true on its own costs nothing. A locked brand's autopilot doesn't run.
connectionsobjectRead only. Google Search Console and Google Analytics connections, made in the app.
lockedbooleanRead only. True when the brand is past the account's brand limit (see locked brands). A locked brand also has unlockUrl, the page where the account owner can add a brand slot.
createdAtstringISO 8601 timestamp.

Writer defaults, presets, the content calendar, publishing integrations and Google connections are managed in the app. The API reads the brand's writer defaults when you create an article with useBrandDefaults.

List brands

GET https://koala.sh/api/brands/

Returns every brand on the account, newest first, with the account's brand limit. Each entry is a summary: knowledge base documents are counted, and businessSubtype, goals and the connection fields are left out. Get one brand for everything.

curl "https://koala.sh/api/brands/" \
     -H "Authorization: Bearer YOUR_API_KEY"
{
  "brands": [
    {
      "brandId": "3f2c9a4e-8b1d-4c6a-9e2f-7a5b1c0d4e8f",
      "name": "Blue Spruce",
      "businessName": "Blue Spruce Heating & Air",
      "businessType": "local_business",
      "domain": "bluesprucehvac.com",
      "country": "us",
      "knowledgeBase": { "documentCount": 1, "totalCharacters": 18342 },
      "locked": false,
      "createdAt": "2026-10-01T15:04:12.000Z"
    }
  ],
  "brandCount": 1,
  "brandLimit": 20,
  "lockedCount": 0
}

Create a brand

POST https://koala.sh/api/brands/

The quickest way is to send the website and let Koala AI fill in the rest:

curl -X POST "https://koala.sh/api/brands/" \
     -H "Authorization: Bearer YOUR_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{ "domain": "bluesprucehvac.com", "buildFromWebsite": true }'

With buildFromWebsite, the domain is the only required field. Koala AI reads the site, then creates the brand with the business name, type, market, writer defaults and knowledge base filled in, the way the app does when you start from a website. You can also send the details yourself and skip the website read.

From its website

Koala AI reads up to 30 pages of the site and looks up the business's Google Business Profile. Anything you send yourself is kept: pass businessName or businessType when you know them and the website fills in the rest.

Nothing is created until the site has been read. The response is 202 with a build: its buildId and status building. Reading the site takes two to five minutes; poll GET /api/brands/builds/{buildId} every 10 to 15 seconds. When status is succeeded, the response includes the new brand (and build.brandId). When it's failed, no brand was created and build.error says why.

const headers = { Authorization: "Bearer YOUR_API_KEY", "Content-Type": "application/json" };

const res = await fetch("https://koala.sh/api/brands/", {
  method: "POST",
  headers,
  body: JSON.stringify({ domain: "bluesprucehvac.com", buildFromWebsite: true }),
});
if (res.status !== 202) throw new Error((await res.json()).message);
let { build } = await res.json();
let brand = null;

while (build.status === "building") {
  await new Promise((r) => setTimeout(r, 15000));
  const poll = await fetch(`https://koala.sh/api/brands/builds/${build.buildId}`, { headers });
  if (poll.ok) ({ build, brand } = await poll.json()); // retry on 429 or 5xx
}

if (build.status === "failed") throw new Error(build.error);
console.log(brand.brandId, brand.businessName); // "3f2c9a4e-..." "Blue Spruce Heating & Air"
{
  "build": {
    "buildId": "9c1e4b7a-2f3d-4e8a-b6c5-0d1f2e3a4b5c",
    "status": "succeeded",
    "brandId": "3f2c9a4e-8b1d-4c6a-9e2f-7a5b1c0d4e8f",
    "domain": "bluesprucehvac.com",
    "startedAt": "2026-10-19T06:44:15.756Z",
    "finishedAt": "2026-10-19T06:46:06.345Z",
    "error": null,
    "pagesRead": 30
  },
  "brand": { "brandId": "3f2c9a4e-8b1d-4c6a-9e2f-7a5b1c0d4e8f", "...": "the full brand object" }
}
  • Anything you send with the request wins over what the site says. If neither you nor the site gives a business name, the build fails and asks you to send businessName.
  • The brand limit is checked when you start the build and again when the brand is created. If the account fills up in between, the build fails with no brand created.
  • If the build fails (for example, a site that blocks crawlers), try again or create the brand with your own details.
  • Website builds are free and count toward the daily site import limit. When it's used up the create request returns 429 with CRAWL_LIMIT_REACHED and nothing is started.
  • A build can only be read with an API key from the account that started it; other accounts get 404.

With your own details

Leave out buildFromWebsite and the brand is created from what you send, with no website read. businessName is required.

curl -X POST "https://koala.sh/api/brands/" \
     -H "Authorization: Bearer YOUR_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
           "domain": "bluesprucehvac.com",
           "businessName": "Blue Spruce Heating & Air",
           "businessType": "local_business",
           "country": "us",
           "knowledgeBase": {
             "documents": [
               { "name": "Services", "content": "Furnace repair, AC installation and heat pumps across Denver." }
             ]
           }
         }'

Responds 201 with { "brand": { ... } }.

Fields

FieldTypeDescription
domainstringRequired. The brand's website.
buildFromWebsitebooleanRead the website first and create the brand when that succeeds, filling in everything you leave out. Returns a build to poll. See from its website.
businessNamestringRequired, unless buildFromWebsite is true. The business's name, as articles should say it.
namestringOptional display label, when it should differ from the business name.
businessTypestringDefaults to local_business. Set it: the type picks the brand's starting writer defaults.
businessSubtypestringFree-text detail.
countrystringTarget market.
goalsstring[]Growth goals.
knowledgeBaseobject{ "documents": [{ "name", "content" }] }. Up to 20 documents and 100,000 characters in total.

Either way, creating a brand uses one of your plan's brand slots; when they are all in use the response is 403 with BRAND_LIMIT_REACHED. The brand's KoalaLinks index is set up in the background, so internalLinking.domainId can be null for the first few seconds.

Creating is not idempotent. If a create request times out, list your brands before retrying so you don't make a second copy.

Starting writer defaults

Every new brand gets writer defaults for its business type. A brand built from its website uses the detected type and the site's tone of voice. The defaults only apply to articles created with useBrandDefaults, and they can be changed in the brand's settings in the app (the API doesn't read or change them):

  • All types: GPT-6.1 Sol, real-time data and deep research, an FAQ section, automatic SEO optimization, and no source citations.
  • local_business, professional_services, saas, online_service, ecommerce: one AI image, and the brand is mentioned prominently. Local businesses and professional services write as "we"; the others address the reader as "you".
  • content_site, affiliate: three AI images, and the brand stays out of the article. That brand mention level ("none") also leaves the knowledge base unused; send "brandMentionLevel": "subtle" with an article to have it read.

Changing businessType later doesn't change the writer defaults. Images and deep research use credits, so send "multimediaOption": "none" or "enableDeepResearch": false with an article to turn them off for that article.

Get a brand

GET https://koala.sh/api/brands/{brandId}

Responds with { "brand": { ... } }, including the full content of every knowledge base document.

Update a brand

PATCH https://koala.sh/api/brands/{brandId}

Send only the fields you want to change. Read-only fields (brandId, createdAt, internalLinking, autopilot, connections, locked, unlockUrl) are ignored, and a field sent back exactly as GET returned it counts as unchanged, so a fetched brand can be edited and sent back whole. Any other unknown field is an error.

FieldOn update
name, businessName, businessType, businessSubtypeReplaced. null clears name and businessSubtype; businessName can't be empty. Changing businessType leaves the writer defaults as they are.
domainReplaced. The brand switches to that site's KoalaLinks index, reusing one already on your account; a new one counts toward the daily site import limit. The knowledge base stays as it is.
countryReplaced. null clears it (the United States).
goalsThe whole list is replaced.
knowledgeBaseThe whole document list is replaced. The previous version is saved to the brand's history, where it can be restored from the Brand DNA page.
curl -X PATCH "https://koala.sh/api/brands/BRAND_ID" \
     -H "Authorization: Bearer YOUR_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{ "goals": ["Get more leads"], "country": "ca" }'

Responds with the updated { "brand": { ... } }. When two updates to the same brand overlap, the later one wins.

Delete a brand

DELETE https://koala.sh/api/brands/{brandId}

Deletes the brand permanently and responds with { "deleted": true, "brandId": "..." }. This removes the brand's writer defaults, presets, Growth Agent chats, history, Google Search Console and Analytics connections, and its content calendar (a task that is writing its article finishes first). Library folders tied to the brand and apps connected through the MCP lose their link to it. Articles already written stay in your library, and the KoalaLinks index and publishing integrations stay on your account. The brand slot is free again right away.

Writing articles with a brand

Pass brandId to POST /api/articles/v2/ and the article is written with the brand's business name, type, website and knowledge base. It works for blog_post, listicle and service_page articles (service pages require it). The brand must belong to the account that owns the API key.

Brands need the v2 endpoint. On the legacy /api/articles/ endpoint a request with brandId is rejected with a 400. Moving to v2 means sending the same request to /api/articles/v2/; most parameters are unchanged (see the migration guide).

Add "useBrandDefaults": true to also apply the brand's writer defaults from the app (tone, point of view, model, images, brand-wide instructions and the rest), its KoalaLinks index for internal links, and its publishing integration. Any field you send in the request takes priority over the brand's setting for it, and your articleInstructions are combined with the brand's own instructions. To start from one of the brand's presets (named in its settings in the app), pass the name as presetName.

To receive finished articles, poll the article status endpoint or create a Custom Webhook integration. Link it to the brand in the app and useBrandDefaults articles go to it; an integrationId in the request replaces the brand's. Brands created through the API start with no integration.

The article endpoint answers errors in plain text, unlike the JSON errors below: a brandId from another account returns 400 Brand not found. Pass the brandId of one of your own brands., and a brand with no business name returns 400.

curl -X POST "https://koala.sh/api/articles/v2/" \
     -H "Authorization: Bearer YOUR_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
           "targetKeyword": "furnace repair denver",
           "brandId": "BRAND_ID",
           "useBrandDefaults": true
         }'

Limits

  • Brands: each plan includes a number of brand slots, and extra slots are $29/month each, up to 10, from the account page. brandLimit in the list response shows yours, slots included.
  • Daily site imports: brands created from their website, domain changes, and site imports in the app share a daily allowance of twice your brand limit, with a minimum of 25. Past it, those requests return 429 with CRAWL_LIMIT_REACHED; the allowance frees up over the following 24 hours.
  • Knowledge base: up to 20 documents and 100,000 characters per brand.

Locked brands

When an account has more brands than its limit allows (after a downgrade, a lapsed plan or a removed slot), the oldest brands up to the limit keep working and the rest are locked, newest first. Brands created before October 22, 2026 don't lock until November 21, 2026, though they count toward the limit before then. A locked brand can still be read, edited, deleted and written with through the KoalaWriter API, but the Growth Agent won't reply on it and its autopilot doesn't run.

The list response gives lockedCount, and each locked brand has "locked": true and an unlockUrl. Adding a brand slot there for each locked brand, or deleting brands, unlocks them as soon as the change goes through; paused autopilot tasks pick up again within a day.

Rate limits

Per account, per minute:

  • Reads (list and get): 120
  • Updates and deletes: 30
  • Creates: 10

Past a limit the API returns 429 with RATE_LIMITED.

Errors

Errors come back as JSON with a code and a readable message:

{
  "error": "INVALID_REQUEST",
  "message": "Unknown field: writerDefaults. domain must be a website address like example.com.",
  "errors": [
    "Unknown field: writerDefaults.",
    "domain must be a website address like example.com."
  ]
}
StatusCodeMeaning
400INVALID_REQUESTThe body failed validation. errors lists every problem.
400INVALID_JSONThe body isn't valid JSON.
401UNAUTHORIZEDMissing or invalid API key.
403PAID_PLAN_REQUIREDThe account needs a paid plan.
403BRAND_LIMIT_REACHEDEvery brand slot is in use. Includes brandLimit, brandCount and addSlotUrl, where the account owner can add a brand slot.
404BRAND_NOT_FOUNDNo brand with that id on this account.
404NOT_FOUNDNo such path under /api/brands/.
404BUILD_NOT_FOUNDNo website build with that id on this account.
405METHOD_NOT_ALLOWEDThat method isn't supported on this path.
429RATE_LIMITEDToo many requests this minute. See rate limits.
429CRAWL_LIMIT_REACHEDThe daily site import allowance is used up. Includes dailyLimit.
500BUILD_START_FAILEDA buildFromWebsite create couldn't start its build. Nothing was created; retry.
500INTERNAL_ERROR and othersSomething failed on our side. Retry after a few seconds.

Need help?

For questions about the Brands API, contact our support team.