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

# Branding

> Make the notetaker look like your product in your customers

A brand kit is how the notetaker presents itself in your customer's calls. It also holds the
look of a summary email, which only the preview shows for now. You build one in five steps:

1. Upload your images with `POST /files`.
2. Create the kit with `POST /brand_kits`. It answers a `preview_url`.
3. Show `preview_url` to a human.
4. Change the kit with `PATCH /brand_kits/{id}` until they approve it. Nothing is live yet.
5. Apply it: set `branding.brand_kit_id` in a workspace's settings.
6. Optionally, send its summary emails from your own domain.

Every field is on [the brand kit object](https://docs.aefer.ai/api-reference/brand-kits/object.md).

## 1. Upload your images

Upload each image once, as `multipart/form-data` with `purpose=brand_kit`: a logo, and a 16:9
picture if you want your own video tile.

```bash theme={null}
curl -X POST "https://api.aefer.ai/api/v1/files" \
  -H "Authorization: $AEFER_API_KEY" \
  -H "Aefer-Version: 2026-11-13" \
  -F file=@logo.png \
  -F purpose=brand_kit \
  -F workspace_id=wks_01k6a2r9s8x7c2dvq3m5n6p4ab
```

```json theme={null}
{
  "id": "file_01k6d8q4z7m2n5p8r1s3t6v9wx",
  "object": "file",
  "purpose": "brand_kit",
  "workspace_id": "wks_01k6a2r9s8x7c2dvq3m5n6p4ab",
  "filename": "logo.png",
  "content_type": "image/png",
  "size": 48213,
  "url": "https://d3sr83esc6q22p.cloudfront.net/kh8wgfikrt8zf30ghdu2j3itx8rd?Expires=1822382401&Signature=…",
  "created_at": "2026-10-01T09:20:00Z"
}
```

A file is a PNG, JPEG, GIF, WebP or SVG of at most 10 MB. **Use a PNG for the logo the email
shows**: most mail clients don't render SVG, and an SVG's `url` downloads instead of
displaying. A workspace takes 60 uploads a minute; past that, `429 rate_limited`.

## 2. Create the kit

Send what you know; every group is optional and takes its default when left out.

```bash theme={null}
curl -X POST "https://api.aefer.ai/api/v1/brand_kits" \
  -H "Authorization: $AEFER_API_KEY" \
  -H "Aefer-Version: 2026-11-13" \
  -H "Content-Type: application/json" \
  -d '{
    "workspace_id": "wks_01k6a2r9s8x7c2dvq3m5n6p4ab",
    "bot": {
      "name": "Acme Notetaker",
      "tile": {
        "type": "logo",
        "logo": { "file": "file_01k6d8q4z7m2n5p8r1s3t6v9wx", "background_color": "#0B1F3A", "scale": 70 }
      },
      "chat": { "recording_message": "Acme Notetaker is taking notes for {{user}}." }
    },
    "email": {
      "type": "branded",
      "branded": {
        "color": "#0052FF",
        "website": "https://acme.com",
        "description": "Acme, meeting notes for sales teams",
        "cta": { "type": "link", "link": { "label": "Open in Acme", "url": "https://acme.com/meetings/{{meeting_id}}" } }
      }
    }
  }'
```

```json theme={null}
{
  "id": "brk_01k6c3v7w2q9d8m4n5p6r7s8tx",
  "object": "brand_kit",
  "workspace_id": "wks_01k6a2r9s8x7c2dvq3m5n6p4ab",
  "workspace_ids": [],
  "bot": { "name": "Acme Notetaker", "tile": { "type": "logo", "logo": { … } }, "chat": { … } },
  "email": { "type": "branded", "branded": { … } },
  "preview_url": "https://api.aefer.ai/app/shared/brand-kits/brk_01k6c3v7w2q9d8m4n5p6r7s8tx/preview?token=WyJicmtf…",
  "preview_expires_at": "2026-10-01T21:20:16Z",
  "metadata": {},
  "created_at": "2026-10-01T09:20:16Z"
}
```

The kit changes nothing yet: `workspace_ids` is empty until a workspace applies it in step 5.
`workspace_ids` is read-only; settings decide which workspaces use a kit.

### Which workspace owns it

`workspace_id` names the workspace that owns the files and the kit; send the same one on
`POST /files` (as a form field) and on `POST /brand_kits`. Without it, both answer `400 missing`
on `workspace_id`, like every endpoint that acts in a workspace.
`workspace_id` reads `null` when the key does not belong to the workspace that owns the kit.

## 3. Show the preview

Give `preview_url` to a human: it shows the notetaker's tile in a call, its chat messages and
the summary email, exactly as the kit is saved. It needs no sign-in and lasts 12 hours
(`preview_expires_at`). Every `GET` or `PATCH` of the kit answers a fresh one:

```bash theme={null}
curl "https://api.aefer.ai/api/v1/brand_kits/brk_01k6c3v7w2q9d8m4n5p6r7s8tx" \
  -H "Authorization: $AEFER_API_KEY" \
  -H "Aefer-Version: 2026-11-13"
```

An expired link shows that it expired; get a new one rather than retrying it.

## 4. Iterate

Send only what changes. Here the human wants their own picture on the tile:

```bash theme={null}
curl -X PATCH "https://api.aefer.ai/api/v1/brand_kits/brk_01k6c3v7w2q9d8m4n5p6r7s8tx" \
  -H "Authorization: $AEFER_API_KEY" \
  -H "Aefer-Version: 2026-11-13" \
  -H "Content-Type: application/json" \
  -d '{ "bot": { "tile": { "type": "image", "image": { "file": "file_01k6d9b2c4e6g8j0k2m4p6r8tv" } } } }'
```

The name, the chat and the email stay as they were, and the logo tile's options stay stored:
`{ "bot": { "tile": { "type": "logo" } } }` switches back to it. Show the new `preview_url`.

While no workspace uses the kit, nothing a customer sees changes. Once one does, every `PATCH`
reaches its next calls and emails straight away.

## 5. Apply it

A workspace uses a kit through its settings, set on the workspace alone
(`["workspaces:wks_…"]`). A new workspace has none of its own: it inherits your account's. Give
it settings that apply the kit:

```bash theme={null}
curl -X POST "https://api.aefer.ai/api/v1/settings" \
  -H "Authorization: $AEFER_API_KEY" \
  -H "Aefer-Version: 2026-11-13" \
  -H "Content-Type: application/json" \
  -d '{
    "branding": { "brand_kit_id": "brk_01k6c3v7w2q9d8m4n5p6r7s8tx" },
    "assignments": [{ "scope": ["workspaces:wks_01k6a2r9s8x7c2dvq3m5n6p4ab"] }]
  }'
```

If the workspace has its own settings already, that answers `409 scope_taken` naming the
assignment: `PATCH` its settings with the same `branding` instead. A kit is set on a workspace
alone; on an account, a member or a meeting category it answers `422 not_supported`.

The kit's read-only `workspace_ids` now lists the workspace. One kit can brand many workspaces: apply it
to each. `"brand_kit_id": "none"` goes back to the default look. See [Company settings](https://docs.aefer.ai/guides/settings.md).

A single bot can still override the name, avatar and greeting when you
[send it](https://docs.aefer.ai/guides/send-bot.md#how-the-bot-looks-and-records).

## 6. Send from your domain

Summary emails come from `summaries@aefer.ai` until you give the kit an address on a domain you
control. Use a subdomain such as `mail.acme.com`: your root domain's SPF and DMARC stay as they are.

1. Add the domain with your account key. The answer lists four DNS records: three CNAMEs and a TXT.

   ```bash theme={null}
   curl -X POST "https://api.aefer.ai/api/v1/email_domains" \
     -H "Authorization: $AEFER_API_KEY" \
     -H "Aefer-Version: 2026-11-13" \
     -H "Content-Type: application/json" \
     -d '{ "domain": "mail.acme.com" }'
   ```

2. Create the four records at your DNS provider, then call `POST /email_domains/{id}/verify`, or wait
   for the hourly check. The domain turns `verified` once all four resolve. Keep them in place after
   that. [The email domain](https://docs.aefer.ai/api-reference/email-domains/object.md) explains each status.

3. Set the sender on the kit. You can do it right away: until the domain is `verified`, emails go out
   from `summaries@aefer.ai` under your `name`, so none waits on DNS.

   ```json theme={null}
   {
     "email": {
       "sender": {
         "address": "notes@mail.acme.com",
         "name": "Acme Notetaker",
         "reply_to": "support@acme.com"
       }
     }
   }
   ```

The kit's read-only `email.sender.sending_as` shows the From line that goes out right now. A domain
that stops resolving turns `failing`, and emails fall back to `summaries@aefer.ai` until it
resolves again. A domain a kit sends from can't be deleted (`409 domain_in_use`): change the kit's
`email.sender.address` first.

## What a kit holds

Each choice is a `type` plus an object named after it. A read returns only the chosen type's
object; the options of the others stay stored.

### The bot

| Field | What it sets |
| - | - |
| `bot.name` | The notetaker's name in the call, up to 64 characters. |
| `bot.tile` | Its video tile, below. |
| `bot.chat.enabled` | Whether it posts in the call's chat. |
| `bot.chat.recording_message` | Posted when it joins and starts recording, up to 300 characters. |
| `bot.chat.paused_message` | Posted while recording is paused, up to 300 characters. |

| `bot.tile.type` | The tile shows | Needs |
| - | - | - |
| `default` | The default logo on the background color. | Nothing. |
| `color` | A plain background color. | `color.background_color` |
| `logo` | Your logo on the background color, at `scale` (10 to 100) percent. | `logo.file`; `background_color` and `scale` |
| `image` | Your own 16:9 picture, filling the tile. | `image.file` |

`bot.tile.color.background_color` and `bot.tile.logo.background_color` are one value: sending
both with different colors is `422 invalid`.

The tile shows the same picture in every state of the bot (joining, recording, paused), so
don't put a state such as "Recording" in it.

Write both chat messages in the brand's voice, and keep `{{user}}`: it becomes the name of
the person the notetaker records for, as you named them on the member. A message you don't write
reads as the standard one. The standard messages use `bot.name`.

### The email

The `email` group is how the summary email looks. \_æfer sends it after a meeting when
`summary.email.enabled` is on in [settings](https://docs.aefer.ai/guides/settings.md#summary-email), which is off by
default, from `summaries@aefer.ai` or [your own domain](#6-send-from-your-domain): under a
`bot_name` sent with the bot, else `email.sender.name`, else the bot's name with the `branded` or
`custom` email, and as "\_æfer Notetaker" otherwise. The preview shows that same email. To write the
email yourself and still have \_æfer send it, use a [`custom` email](#your-own-html). To send it
yourself, listen for [`bot.summary_ready`](https://docs.aefer.ai/api-reference/bots/bot-summary-ready) and read
the summary with `GET /meetings/{meeting_id}/results` ([A finished meeting](https://docs.aefer.ai/guides/results.md)).

| `email.type` | The summary email |
| - | - |
| `default` | The default email. A new kit starts here. |
| `branded` | Your logo, `color`, `website`, `description` and button, under the bot's name. |
| `custom` | Your own HTML and subject. See [Your own HTML](#your-own-html). |

`email.branded.logo` is the tile's logo until you set one of its own. `email.branded.cta` is
the button under the meeting's name:

| `cta.type` | The button |
| - | - |
| `open_meeting` | Opens the meeting in \_æfer, for a reader who can open it there. |
| `none` | No button. |
| `link` | Opens your `link.url` with the text `link.label`. Both are required. |

If your users have no \_æfer account, set `cta` to `none` or to a `link`. In `link.url`,
`{{meeting_id}}` is replaced by the meeting's `mtg_`id; write it exactly so, with no spaces.
An email with no meeting to name shows the "Open meeting" button instead of a`link`whose url
names`{{meeting_id}}`.

### Your own HTML

With `email.type: "custom"`, the email is your HTML, whole: \_æfer adds no header, footer or
unsubscribe link. Write `email.custom.html` and, optionally, `email.custom.subject` in
[Liquid](https://shopify.github.io/liquid/):

```json theme={null}
{
  "email": {
    "type": "custom",
    "custom": {
      "subject": "Notes: {{ meeting.title }}",
      "html": "<html><body><h1>{{ meeting.title }}</h1><p>{{ meeting.date }}</p>{{ summary.html }}<ul>{% for item in summary.action_items %}<li>{{ item.text }}</li>{% endfor %}</ul></body></html>"
    }
  }
}
```

The variables have the shapes of [a meeting's results](https://docs.aefer.ai/guides/results.md):

| Variable | What it holds |
| - | - |
| `meeting` | `id`, `title`, `starts_at`, `date` (formatted in the organizer's time zone) and `open_url` (your `link` button's URL, or `null`). |
| `organizer` | `name` and `email`, or `null`. |
| `participants` | A list of `name` and `email`. |
| `summary` | `html` (the summary, ready to place), `markdown`, `sections` and `action_items`, each with `text` and `assignee`. An `assignee` can be someone linked to the recording who wasn't invited, so think twice before showing their `email` to guests. |
| `recipient` | The `name` and `email` of the person this copy goes to. |
| `brand` | The kit's `name`, `color`, `logo_url` and `website`. |

* **Strict.** An unknown variable or filter is an error, and so is reading a field of `null`:
  write `{% if item.assignee %}{{ item.assignee.name }}{% endif %}`. `include` and `render` aren't
  available.
* **Escaped.** Values are HTML-escaped where they land; `summary.html` is markup \_æfer wrote.
* **What HTML is allowed.** Inline `style`, `<style>`, tables and images. Every `href` and `src` is
  `https:`, `mailto:` or `tel:`. `<script>`, `<form>`, `<iframe>`, `<object>`, `<embed>`,
  `<noscript>`, SVG animation, `<link rel="stylesheet">`, `@import` and `on*` attributes are
  refused, never removed, so the preview is what's sent.
* **Sizes.** At most 100 KB as written and 102 KB rendered, where Gmail starts clipping. The subject
  is one line of at most 200 characters; `null` sends the standard one.
* **Checked when saved.** A `PATCH` renders the email for a sample meeting, and again with no
  organizer, participants, time, meeting id or names, as a meeting may have. It refuses a template that fails either
  with a `422` saying what failed, with the line for a Liquid error. Check it in the preview.
* **Checked again when sent.** A copy that can't be rendered, such as a URL built from a meeting's
  data that isn't `https`, goes out as your `branded` email instead, and `email.custom.last_error`
  says why until the template changes.

`PATCH email.type: "branded"` goes back to the branded email and keeps your HTML for later.

## Writing rules

* **Partial at every depth.** On `POST` and `PATCH`, a group, an object or a field you leave
  out keeps its value. `type` can be left out: the current one stays.
* **`null` resets a text** (`bot.name`, the chat messages, `color`, `website`, `description`)
  to its default.
* **A read's `bot` and `email` can be sent back unchanged.** Send those groups only, not the
  whole object: `id`, `object`, `preview_url` and the other read-only fields are
  `422 unknown_field`. A name or message equal to the default the read showed stays unset, so it
  keeps following the default.
* **Files** (`bot.tile.logo.file`, `bot.tile.image.file`, `email.branded.logo`) take a `file_`
  id, `null`, or the `{ id, url }` a read returned. From that object only `id` counts: the
  same id keeps the image, another id copies that file. The kit keeps its own copy, so it
  doesn't depend on the file afterwards.
* **A file reads as `{ id, url }`.** `url` is the kit's copy, short-lived (at most seven days, often
  less) and fresh on every read. `id` is `null` for an image uploaded in the dashboard.
* **`null` removes an image.** `email.branded.logo: null` goes back to the tile's logo. A
  `logo` or `image` tile can't lose its file: choose another tile first.

## Errors

Every error is `{ "errors": [{ "code", "field", "message" }] }`. See
[Errors](https://docs.aefer.ai/api-reference/errors.md).

| Status | `code` | When |
| - | - | - |
| `400` | `missing` | `workspace_id` was not sent. |
| `403` | `workspace_update_not_allowed` | `DELETE`: a workspace outside your account still uses the kit. |
| `404` | `not_found` | A `file_` or `brk_` id the key can't read. |
| `422` | `unknown_field` | A field the kit doesn't have: a read-only one (`id`, `object`, `created_at`, `preview_url`), a flat name such as `bot_name`, or a typo. |
| `422` | `inclusion` | A `type` outside its list. |
| `422` | `invalid` | A `custom` email that does not render for a sample meeting or breaks its rules; two different tile background colors; a file that is not an id, `null` or `{ id, url }`; a URL that is not http(s); an `email.sender.address` on a domain your account didn't add, or a sender address or `reply_to` that is not an email. |
| `422` | `invalid_format`, `too_long`, `too_low`, `too_high` | A color that is not HEX, such as `#0B1F3A`; a text over its limit; a `scale` outside 10 to 100. |
| `422` | `missing` | A `logo` or `image` tile with no file, on `bot.tile.logo.file` or `bot.tile.image.file`; `email.type: custom` with no `email.custom.html`. |
| `422` | `blank` | A `link` button with no `label` or `url`. |
| `429` | `rate_limited` | More than 60 uploads a minute in one workspace. Wait `retry_in_seconds`. |

`field` is the dotted path into your request, such as `bot.tile.logo.background_color`.


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