---
title: "Brand API skill"
description: "A skill file that teaches an AI agent to manage Tarkle brand pages with the API, and when to use MCP instead."
---

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

# Brand API skill

This page is the text of the `tarkle-brand-api` agent skill. Give it to an assistant that manages Tarkle brand pages. Ask for this page with `Accept: text/markdown` to get it as Markdown. The skill file itself is `SKILL.md` with this front matter:

```yaml
name: tarkle-brand-api
description: Create, update, publish, delete and restore Tarkle brand pages through the Tarkle API, including brand files, brand domains and brand webhooks. Use when a task manages a workspace's brand pages from code or from an agent, or when you must choose between the Tarkle MCP server and the API.
```

Use this skill to manage Tarkle brand pages (logo, color, font and guideline pages that a workspace shares with clients) from code or an agent. Check facts in the live OpenAPI file before relying on this text: `GET https://tarkle.com/api/v1/openapi.json` needs no token and lists every path, scope and error code.

## API or MCP?

| You hold | You can do |
| --- | --- |
| An **API token** (`tk_...`, made in Settings > API) | Everything in this skill. |
| An **MCP token** or OAuth login | Only `tarkle_list_brands` (read-only, takes a `workspaceId` uuid). It lists brands and their domains. No MCP tool creates, edits, uploads, deletes or reads a brand's details. |

An MCP token does not work on `/api/v1`. If a task needs more than a list and you only have MCP, say so and ask for an API token. Never put a token in a prompt, a file in the repository or a log.

## Every call

- Base URL `https://tarkle.com/api/v1`. Header `Authorization: Bearer tk_...`.
- Header `X-Workspace-Key: <8 character key>` on every call inside a workspace. `GET /workspaces` lists the keys the token can use. A workspace id is refused.
- The token acts as the person who created it. For brand calls that person must be an **owner or admin** of the workspace, otherwise `403 forbidden`.
- A brand in `{brand}` is its id (uuid) or its slug. A brand of another workspace is `404 not_found`.
- Add `Idempotency-Key: <uuid>` to every `POST`, `PATCH`, `PUT` and `DELETE`. A retry with the same key and body returns the first answer (`Idempotent-Replayed: true`). A reused key with a different body is `422 idempotency_key_reused`.
- Errors are `application/problem+json`. Branch on `code`, not on the message. On `429`, wait `Retry-After` seconds.
- Limits: 120 requests a minute per token on brand calls. Read `X-RateLimit-*` instead of hard-coding it.

## Scopes

A token with no scopes can do nothing. Ask for the smallest set.

| Scope | Calls |
| --- | --- |
| `brands:read` | `GET /brands`, `/brands/{brand}`, `/assets`, `/markdown`, `/analytics` |
| `brands:write` | `PATCH /brands/{brand}` |
| `brands:create` | `POST /brands`, `DELETE /brands/{brand}`, `GET /deleted-brands`, `POST /deleted-brands/{brand}/restore` |
| `brands:files` | `POST /brands/{brand}/files`, `POST /brands/{brand}/files/{file}/complete` |
| `brands:domains` | `GET`, `PUT`, `DELETE /brands/{brand}/domain` |
| `brands:requests` | `GET /brands/{brand}/requests`, `POST /brands/{brand}/requests/{request}` |
| `webhooks:manage` | `/webhooks/**` |

A `403 forbidden` that names a scope means the token lacks it. Ask the user for a token that has it.

## Recipes

**Create a brand and fill it in**
1. `POST /brands {"name": "Acme Bank", "slug": "acme-bank"}`. Limits are the app's: name 48 characters, summary 200, story 2,000. Do not send `access`, `is_draft` or `confirm_public`; a new brand is a private draft.
2. `PATCH /brands/{brand}` with text, links, `primary_color` (`#RRGGBB`), `download_approval`, `tracking`, SEO fields.
3. Upload files (below).
4. To publish, `PATCH {"is_draft": false}`. The API does **not** check what the editor requires before publishing (summary, story, website, icon, main logo, primary color, fonts, banner), so check the brand is complete first.
5. To open it to everyone, `PATCH {"access": "public", "confirm_public": true}`. Without the flag the call is `400 confirmation_required`. This removes any password. Ask the user before doing it. `access: "restricted"` needs a contact or organization assigned in the app first.

**Upload a file (two steps)**
1. `POST /brands/{brand}/files {"slot": "icon", "size": <bytes>, "filename": "icon.png"}`. Slots: `icon`, `main_logo`, `banner`, `social_image`, `favicon` (replace), `logo` (extra logo), `asset` (collection file).
2. `PUT` the bytes to `upload.url`. No Authorization header on this request.
3. `POST /brands/{brand}/files/{file_id}/complete`. For `logo` you may send `name`, `type`, `usage`. For `asset` send `collection_id` or `collection_name`.
- Limits: logos and icons PNG, JPG, WebP or SVG up to 10 MB; banner and social image PNG, JPG, WebP up to 10 MB; favicon also ICO up to 2 MB; collection files any type up to 100 MB. The type is read from the file's bytes, not its name: a renamed program is `422 invalid_file` and is removed.
- `409 file_not_uploaded`: the PUT did not happen. `409 conflict` on `asset`: the brand still has images from the older editor; ask the user to open it in the app once.
- Completing twice is safe (`already_attached: true`).

**Connect a domain**
`PUT /brands/{brand}/domain {"domain": "brand.example.com"}` (plain hostname). Show the user the returned `dns_records` to add at their DNS host, then poll `GET /brands/{brand}/domain` until `status` is `active`; do not poll faster than every 30 seconds. Free plans have no brand domain: `403 forbidden` with `details.entitlement: "BRAND_PAGE_DOMAIN_LIMIT"`. Tell the user it needs Pro; do not retry. `409 domain_taken` means someone else uses it.

**Delete and restore**
`DELETE /brands/{brand}` with `{"confirm_delete": true}` moves the brand to the bin (30 days on Free, 90 on Pro and Agency). Its domains are detached and are not restored; requests, analytics and the kit link are not kept. Ask the user before deleting. `GET /deleted-brands` lists what can come back; `POST /deleted-brands/{id}/restore` restores it (`409 name_taken`: send another `slug`; `410 expired`: gone; `403`: the plan's brand page limit).

**Listen for changes**
`POST /webhooks {"url": "https://...", "events": ["brand.updated"]}` returns the `secret` once. Verify `Tarkle-Signature` (`t=<unix>,v1=<hex>`, HMAC-SHA256 of `<t>.<raw body>`, reject `t` older than 5 minutes, accept any `v1`). Events: `brand.updated`, `brand.downloaded`, `brand.request.created|approved|rejected`, and `brand.viewed` only when named. Events never carry the visitor of a download.

## Rules for agents

- Read before you write: `GET /brands/{brand}` and show the user what will change. Send only the fields that change.
- Confirm with the user before: making a brand public, deleting a brand, removing a domain, deleting a webhook.
- Plan limits answer with the app's own errors. Report them; never try to work around a plan limit.
- Treat test data as real: use a clearly named test brand and delete it (to the bin) when done.
- Do not call the API from browser code. It has no CORS support and a token in a page is a leaked token.
- Not available in the API: setting a password, changing fonts, team or press, removing a file, uploading team photos, guideline images or fonts.

## Where to read more

`https://tarkle.com/docs/api/brands` (guides), `https://tarkle.com/docs/api/errors` (one page per error code), `https://tarkle.com/help/branding/what-is-a-brand-page` (what a brand page is for customers).

Source: https://tarkle.com/docs/ai-agents/brand-skill/index.mdx
