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

# Assistants overview

> Create assistants, write the blocks that make up their prompt, and give them webhook functions to call — the same configuration the Keebai portal edits.

An assistant is what answers your customers. Its behaviour is not one prompt string: it is an ordered list of **blocks**, each one a named piece of the system prompt, plus the **functions** it is allowed to call while it answers.

**This API edits that configuration.** Everything the portal's assistant editor does to prompts, block ordering and webhook functions, you can do here — which is what makes it possible to version your assistants in git, provision them per client, or let an agent tune them.

## The data model

<CardGroup cols={2}>
  <Card title="Assistant" icon="robot">
    Name, model, knowledge nodes, and an ordered list of references to blocks.
  </Card>

  <Card title="Block" icon="cube">
    One named piece of the prompt, with a type that decides where it lands.
  </Card>

  <Card title="Version" icon="clock-rotate-left">
    An immutable snapshot of a block's content. Saving never overwrites.
  </Card>

  <Card title="Function" icon="bolt">
    An HTTPS webhook the assistant can call mid-conversation.
  </Card>
</CardGroup>

A block is **not owned by an assistant**. It lives in the project's catalogue and any number of assistants can reference it, each pinning a different version if they want to. Attaching and detaching is wiring, not creation — [detaching a block](/dev/endpoints/assistants-block-detach) leaves it in the catalogue, and [deleting an assistant](/dev/endpoints/assistants-delete) leaves all of its blocks behind.

### The three-step drill-down

The read path is deliberately three calls, because the second one is the cheap one you will make most:

```bash theme={null}
TOKEN="kbai_pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

# 1. Which assistants exist
curl -s https://api.keebai.com/v1/assistants -H "Authorization: Bearer $TOKEN"

# 2. Which blocks make up this one, in prompt order — metadata only, no content
curl -s https://api.keebai.com/v1/assistants/6650a1b2c3d4e5f6a7b8c9d0/blocks \
  -H "Authorization: Bearer $TOKEN"

# 3. The content of one block
curl -s https://api.keebai.com/v1/blocks/6650bbbb2222cccc3333dddd/content \
  -H "Authorization: Bearer $TOKEN"
```

Step 2 never returns content, and that is structural rather than a filter we apply: a block document genuinely holds no content. The text lives in its versions, which is why reading it is a separate call and why saving it creates history for free.

## Block types

There is one creation endpoint per type, because the type is what decides where the block lands in the prompt and what it is expected to say.

| Type                                                              | Family       | What it holds                                              |
| ----------------------------------------------------------------- | ------------ | ---------------------------------------------------------- |
| [`personification`](/dev/endpoints/blocks-create-personification) | identity     | Who the assistant is: role, tone, how it addresses people. |
| [`objective`](/dev/endpoints/blocks-create-objective)             | identity     | What it must achieve, and how you know it did.             |
| [`response_format`](/dev/endpoints/blocks-create-response-format) | identity     | Message length, emoji usage, formatting rules.             |
| [`steps`](/dev/endpoints/blocks-create-steps)                     | instructions | The procedure it follows, in order.                        |
| [`cases_possible`](/dev/endpoints/blocks-create-cases-possible)   | instructions | Situations that can come up and how to answer each.        |
| [`do_not`](/dev/endpoints/blocks-create-do-not)                   | instructions | What it must never do.                                     |
| [`response_policy`](/dev/endpoints/blocks-create-response-policy) | instructions | When to stay silent, when to react instead of reply.       |

`family` is **derived from the type** and is read-only. It decides which section of the prompt the block is composed into, so it is not something a caller gets to choose.

<Note>
  **One identity block of each type per assistant.** Attaching a second `personification`, `objective` or `response_format` to the same assistant fails with `ASSISTANT_DUPLICATE_IDENTITY_BLOCK`. Instruction blocks have no such limit — an assistant can hold several `steps` blocks.
</Note>

### Catalogue blocks are read-only here

Five more types exist and you will see them in [`GET /v1/blocks`](/dev/endpoints/blocks-list) and in an assistant's block list: `product_info`, `store_info`, `reservation_info`, `tables_info` and `tags`. You can read their content, rename them and archive them, but [saving their content](/dev/endpoints/blocks-content-save) returns `409 BLOCK_TYPE_NOT_WRITABLE`.

<Warning>
  **This is not an arbitrary restriction.** Saving a `product_info` block has to rebuild up to eleven ecommerce functions on the assistant, keyed to the store and product ids inside the payload; `store_info` rebuilds the branch lookup function. That rebuild lives in the portal. If the API accepted the write without it, the block would save cleanly and the assistant would silently lose its ecommerce toolset — which is worse than a refusal. Configure those five in the portal.

  Every block field also tells you directly: `content_writable` is `true` for the seven types above and `false` for these five.
</Warning>

## Content is text

Blocks written through this API are **free blocks**: their content is plain text, not a structured object.

Three markdown conventions are recognised, because the portal editor renders them as real list and heading elements rather than literal characters:

| You write             | It becomes           |
| --------------------- | -------------------- |
| `- item` or `* item`  | A bullet             |
| `1. item`             | A numbered list item |
| `# Title`, `## Title` | A heading            |

Everything else is a paragraph. [Reading the content back](/dev/endpoints/blocks-content-get) returns the same text.

<Note>
  **Rich formatting authored in the portal flattens.** Bold, italics, colours and inline function chips do not survive a round trip through this API: reading gives you the text, and writing replaces the document with a plain-text rendering. If an assistant was carefully styled in the portal, prefer editing it there.
</Note>

## Saving content never overwrites

[`PUT /v1/blocks/{id}/content`](/dev/endpoints/blocks-content-save) appends an immutable version and points the block at it. The previous one stays in [the history](/dev/endpoints/blocks-versions-list) and can be [restored](/dev/endpoints/blocks-version-restore), which also appends rather than overwrites. Nothing in this API destroys a version.

The same is true of deletes at the block level: [`DELETE /v1/blocks/{id}`](/dev/endpoints/blocks-archive) **archives**. Assistants that still reference an archived block keep using it — archiving hides it from the catalogue, it does not switch it off. To take a block out of an assistant, [detach it](/dev/endpoints/assistants-block-detach).

## Functions

A function is an HTTPS webhook the assistant can call while it answers. You describe what it does, the model decides when to call it, and Keebai makes the request.

Placeholders written `{{name}}` in the path, the query string or the body are filled at call time from the arguments the model chose, so `https://erp.example.com/stock/{{sku}}` with a `sku` parameter is the normal shape.

<Warning>
  **Functions are company-wide, not project-scoped.** Unlike assistants and blocks, a function is visible to every project in the company. A name collision is a company-level collision.
</Warning>

Two things about them are worth reading before your first write: [how secret values are masked](/dev/assistants/webhook-security#secrets-are-write-only) and [which URLs are rejected](/dev/assistants/webhook-security#which-urls-are-rejected).

## Scoping

|                              | Scoped by                            |
| ---------------------------- | ------------------------------------ |
| Assistants, blocks, versions | Company **and** project of the token |
| Functions                    | Company only                         |

Two tokens on two projects of the same company see different assistants and different blocks, and neither can read the other's by id. They see the same functions.

## Endpoints

### Assistants

| Method   | Path                 | Scope              | Description                                                                |
| -------- | -------------------- | ------------------ | -------------------------------------------------------------------------- |
| `GET`    | `/v1/assistants`     | `assistants:read`  | [List active assistants](/dev/endpoints/assistants-list).                  |
| `GET`    | `/v1/assistants/:id` | `assistants:read`  | [Get one](/dev/endpoints/assistants-get), with its block references.       |
| `POST`   | `/v1/assistants`     | `assistants:write` | [Create one](/dev/endpoints/assistants-create).                            |
| `PATCH`  | `/v1/assistants/:id` | `assistants:write` | [Update name, model, avatar, knowledge](/dev/endpoints/assistants-update). |
| `DELETE` | `/v1/assistants/:id` | `assistants:write` | [Delete permanently](/dev/endpoints/assistants-delete).                    |

### Wiring blocks to an assistant

| Method   | Path                                 | Scope              | Description                                                                               |
| -------- | ------------------------------------ | ------------------ | ----------------------------------------------------------------------------------------- |
| `GET`    | `/v1/assistants/:id/blocks`          | `assistants:read`  | [Its blocks in prompt order](/dev/endpoints/assistants-blocks-list), without content.     |
| `PUT`    | `/v1/assistants/:id/blocks`          | `assistants:write` | [Replace the whole list](/dev/endpoints/assistants-blocks-set) — the only way to reorder. |
| `POST`   | `/v1/assistants/:id/blocks`          | `assistants:write` | [Attach one block](/dev/endpoints/assistants-block-attach).                               |
| `DELETE` | `/v1/assistants/:id/blocks/:blockId` | `assistants:write` | [Detach one block](/dev/endpoints/assistants-block-detach).                               |

### Blocks

| Method   | Path                                         | Scope              | Description                                                    |
| -------- | -------------------------------------------- | ------------------ | -------------------------------------------------------------- |
| `GET`    | `/v1/blocks`                                 | `assistants:read`  | [The project's whole catalogue](/dev/endpoints/blocks-list).   |
| `GET`    | `/v1/blocks/:id`                             | `assistants:read`  | [Metadata of one block](/dev/endpoints/blocks-get).            |
| `POST`   | `/v1/blocks/<type>`                          | `assistants:write` | One endpoint per type — see the table above.                   |
| `PATCH`  | `/v1/blocks/:id`                             | `assistants:write` | [Rename or archive](/dev/endpoints/blocks-update).             |
| `DELETE` | `/v1/blocks/:id`                             | `assistants:write` | [Archive](/dev/endpoints/blocks-archive).                      |
| `GET`    | `/v1/blocks/:id/content`                     | `assistants:read`  | [Read the current content](/dev/endpoints/blocks-content-get). |
| `PUT`    | `/v1/blocks/:id/content`                     | `assistants:write` | [Save new content](/dev/endpoints/blocks-content-save).        |
| `GET`    | `/v1/blocks/:id/versions`                    | `assistants:read`  | [Version history](/dev/endpoints/blocks-versions-list).        |
| `GET`    | `/v1/blocks/:id/versions/:versionId`         | `assistants:read`  | [One historical version](/dev/endpoints/blocks-version-get).   |
| `POST`   | `/v1/blocks/:id/versions/:versionId/restore` | `assistants:write` | [Restore it](/dev/endpoints/blocks-version-restore).           |

### Functions

| Method   | Path                | Scope              | Description                                                        |
| -------- | ------------------- | ------------------ | ------------------------------------------------------------------ |
| `GET`    | `/v1/functions`     | `assistants:read`  | [List webhook functions](/dev/endpoints/functions-list).           |
| `GET`    | `/v1/functions/:id` | `assistants:read`  | [Get one](/dev/endpoints/functions-get), with the blocks using it. |
| `POST`   | `/v1/functions`     | `assistants:write` | [Create one](/dev/endpoints/functions-create).                     |
| `PUT`    | `/v1/functions/:id` | `assistants:write` | [Update one](/dev/endpoints/functions-update).                     |
| `DELETE` | `/v1/functions/:id` | `assistants:write` | [Delete one](/dev/endpoints/functions-delete).                     |

## A full round trip

```bash theme={null}
TOKEN="kbai_pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
API="https://api.keebai.com/v1"
AUTH="Authorization: Bearer $TOKEN"
JSON="Content-Type: application/json"

# 1. Create the assistant. It is born with a personification and a
#    response-policy block already attached, both empty.
ASSISTANT=$(curl -s -X POST $API/assistants -H "$AUTH" -H "$JSON" \
  -d '{"name":"Recepción","model":"gemini-2.5-flash"}' | jq -r '.assistant._id')

# 2. Fill in the personality it was born with.
BLOCK=$(curl -s $API/assistants/$ASSISTANT/blocks -H "$AUTH" \
  | jq -r '.data[] | select(.type=="personification") ._id')

curl -s -X PUT $API/blocks/$BLOCK/content -H "$AUTH" -H "$JSON" \
  -d '{"content":"Sos la recepción de una barbería.\n- Tuteás al cliente.\n- No prometés descuentos."}'

# 3. Add a procedure as a new block and wire it in.
STEPS=$(curl -s -X POST $API/blocks/steps -H "$AUTH" -H "$JSON" \
  -d '{"name":"Reserva","content":"1. Saludar\n2. Preguntar el servicio\n3. Ofrecer horarios"}' \
  | jq -r '.block._id')

curl -s -X POST $API/assistants/$ASSISTANT/blocks -H "$AUTH" -H "$JSON" \
  -d "{\"block_id\":\"$STEPS\",\"order\":2}"

# 4. Give it a function to call.
curl -s -X POST $API/functions -H "$AUTH" -H "$JSON" -d '{
  "name": "consultar_disponibilidad",
  "display_name": "Consultar disponibilidad",
  "description": "Devuelve los horarios libres de un servicio para una fecha.",
  "context": {
    "url": "https://erp.example.com/slots",
    "method": "GET",
    "headers": { "Authorization": "Bearer erp_live_xxx" },
    "params": { "service": "{{service}}", "date": "{{date}}" }
  },
  "tool_params": [
    { "name": "service", "description": "Nombre del servicio", "required": true },
    { "name": "date", "description": "Fecha en YYYY-MM-DD", "required": true }
  ]
}'
```

## Next steps

<CardGroup cols={2}>
  <Card title="Create an assistant" icon="plus" href="/dev/endpoints/assistants-create">
    The first call, and what it gives you for free.
  </Card>

  <Card title="Save block content" icon="pen" href="/dev/endpoints/blocks-content-save">
    Writing prompts as text, and which types refuse.
  </Card>

  <Card title="Webhook security" icon="shield-halved" href="/dev/assistants/webhook-security">
    Which URLs are rejected, and how secrets are masked.
  </Card>

  <Card title="Scopes" icon="key" href="/dev/scopes">
    `assistants:read` and `assistants:write`, and what each unlocks.
  </Card>
</CardGroup>
