Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Kamai Messages API
description: 'Read construction drawings from your own software. Upload a PDF plan; Kamai reads it and gives you back every room, wall,
door and window with measurements in metres, every printed word, and every printed table (schedules, legends, revision
blocks), as JSON. ## Authentication
Create an API key at Kamai → Settings → API keys and send it on every request:
```
Authorization: Bearer
```
A key acts as the user who created it and sees only that user''s projects. Keep it on your server. A call without a key,
or with a revoked one, gets `401`. Revoking a key, or a change Kamai makes to its settings, takes effect within 3 minutes. ## Acting for your users
A partner whose users sign in through it can use any API key to act for those users. The request carries, next to the
key, `X-Kamai-Partner` (your partner ID, as Kamai gave it to you) and `X-Kamai-Partner-User-Token` (that user''s own token
from you). Kamai checks the token with you and acts as that user: that user''s projects, that user''s page quota, as if the
user did it in Kamai. Without the two headers the key acts as its own owner. One header without the other is `400`
(`partner_id_required`, `partner_user_token_required`). A user who does not exist in Kamai or is disabled gets `403`
(`act_as_not_allowed`). A key Kamai has tied to one partner always needs both headers and never acts as its owner. What the key may do for a user is set by Kamai on the key:
| | routes | needs |
|---|---|---|
| read | every `GET`, and `POST /v1/blueprints/{blueprint_id}/markups/clip` | the key enabled for partner users |
| change | create and update projects, upload, update sheets, create and update markups, rename conversations | edit enabled |
| delete | delete projects, jobs, markups and conversations, cancel jobs | delete enabled, separately from edit |
A route the key may not use gets `403` (`edit_not_allowed`, `delete_not_allowed`). On every key, not only for partner users, renaming a conversation needs edit enabled and deleting one needs delete enabled; a new key has both off. Replacing a sheet''s features with
`PUT /v1/blueprints/{blueprint_id}` for a user needs the `revision` you read as `base_revision` (`422`
`base_revision_required`), and replacing them with none counts as a delete. Conversations work the same way;
with edit enabled for the key, the assistant may change the user''s drawings and undo that user''s own changes; keys
without it never write. Uploads count against the user''s own page quota. Kamai keeps the answer of each token check for up to 60 seconds, so a user you disable on your side can still be served
for that long. ## The usual flow
1. `POST /v1/blueprints/upload/init` returns a signed URL. 2. `PUT` the PDF to that URL. 3. `POST /v1/blueprints/upload/complete` returns a `job_id`. It starts one job per upload: calling it again with the
same `file_uuid` returns the same `job_id` (`409` with `Retry-After` while the first call is still running). To send
another file, start a new upload with `init`. 4. Poll `GET /v1/projects/{project_id}/jobs/{job_id}` every 5–10 s until `status` is final: `SUCCEEDED`, or
`FAILED` / `CANCELLED` / `ABANDONED`, which end the job; for those, show `error_code` and `error_message`. The PDF check fails a job with `pdf_incomplete` (the file looks cut off or damaged: download or export it again),
`pdf_filter_unsupported` (Brotli compression, or a PDF that cannot be read) or `pdf_scan_over_budget` (too large or
complex to check). New `error_code` values can be added over time: treat one you do not know as a failure and show
its `error_message`. The job record is written when processing starts, not when the upload returns, and when many uploads are queued that
can take minutes. Until then, a `job_id` you got from an upload answers `404` with `detail` `Job not found`: keep
polling it. `Project not found` is final. 5. On `SUCCEEDED`, read the sheet with `GET /v1/blueprints/{blueprint_id}`.'
version: 0.1.0
tags:
- name: Messages
description: Send a message; get the answer as a stream of events or as one JSON reply.
paths:
/v1/conversations/{conversation_id}/messages:
post:
tags:
- Messages
summary: Send a message and get the answer
description: 'Send one message; the assistant answers it with the conversation''s history in mind.
**`Accept: text/event-stream`**: the answer streams as Server-Sent Events, one frame per event
(`event: `, `id: `, `data: {"type", "seq", "data"}`): `turn.accepted`, `turn.started`, `phase`, `activity` (what the assistant is doing, `{"kind", "label"}`; kinds: reading, finding, counting, measuring, catalogue, bill_of_quantities, combining, editing, help, working), `text.message` (the full answer text so far; replaces what the deltas built),
`text.delta`, `table`, `highlights`, `question`, `features.changed`, then `turn.done` (the full Turn, its
`answer` authoritative) or `turn.failed`. A `ping` comes after 15 s of silence. If you disconnect, the turn
still finishes and lands in the conversation.
**`Accept: application/json`** (default): the call returns the finished Turn, waiting up to 600 s; after that
504 `turn_timeout` with the `turn_id` (the turn keeps running and lands in the conversation).
One turn at a time per conversation (409 `conversation_busy`); at most 2 turns at once per API key by default (Kamai can set more on a key) and a daily
number of turns per key, UTC day (429 `rate_limited` with `Retry-After`). Each message counts as one chat turn.'
operationId: send_message_v1_conversations__conversation_id__messages_post
security:
- HTTPBearer: []
parameters:
- name: conversation_id
in: path
required: true
schema:
type: string
title: Conversation Id
- name: Idempotency-Key
in: header
required: false
schema:
anyOf:
- type: string
maxLength: 64
- type: 'null'
description: 'A retry with the same key follows the turn the first request started instead of starting another: the same stream or Turn. If that turn can no longer be followed, 409 with its `turn_id` (`conversation_busy` while it runs, `already_sent` once it ended; its answer is in the conversation).'
title: Idempotency-Key
description: 'A retry with the same key follows the turn the first request started instead of starting another: the same stream or Turn. If that turn can no longer be followed, 409 with its `turn_id` (`conversation_busy` while it runs, `already_sent` once it ended; its answer is in the conversation).'
- name: X-Kamai-Partner-User-Token
in: header
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: 'For partners whose users sign in through them: the partner''s own token for the end user. Kamai verifies it with the partner and acts as that user. Without it the call acts as the API key''s owner.'
title: X-Kamai-Partner-User-Token
description: 'For partners whose users sign in through them: the partner''s own token for the end user. Kamai verifies it with the partner and acts as that user. Without it the call acts as the API key''s owner.'
- name: X-Kamai-Partner
in: header
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: 'Your partner ID, as Kamai gave it to you: the partner that issued `X-Kamai-Partner-User-Token`. Required with it.'
title: X-Kamai-Partner
description: 'Your partner ID, as Kamai gave it to you: the partner that issued `X-Kamai-Partner-User-Token`. Required with it.'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MessageCreate'
examples:
question:
summary: A question
value:
text: How many doors are on this sheet, by type?
language: en
mention:
summary: About one element
value:
text: What is the opening width of this door?
mentions:
- type: feature
id: dddddddd-0000-4000-8000-000000000001
label: D1
responses:
'200':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/Turn'
examples:
real:
summary: Finished turn
description: Shape built from the chat service's own payloads (not yet captured from a live call). Ids and text are illustrative.
value:
id: cccc0000000000000000000000000001
conversation_id: cv1_WyI0NTY3ODkwMTIzNDU2Nzg5MDEyIiwiYWFhYTAwMDAwMDAzIiwiYmJiYmJiYmItMDAwMC00MDAwLTgwMDAtMDAwMDAwMDAwMDAxIiwicmVhZF9vbmx5Il0
status: done
created_at: '2026-10-18T07:12:21Z'
finished_at: '2026-10-18T07:12:44Z'
answer:
text: 'There are 3 doors on this sheet:
{{table:0}}
Most are D1 (single leaf, 90 cm).'
tables:
- index: 0
title: Doors by type
columns:
- key: type
label: Type
type: text
unit: null
- key: count
label: Count
type: number
unit: pcs
rows:
- cells:
type: D1
count: 2
feature_ids:
- dddddddd-0000-4000-8000-000000000001
- dddddddd-0000-4000-8000-000000000002
text_ids: []
- cells:
type: D2
count: 1
feature_ids:
- dddddddd-0000-4000-8000-000000000003
text_ids: []
boq: null
highlights:
- blueprint_id: bbbbbbbb-0000-4000-8000-000000000001
feature_ids:
- dddddddd-0000-4000-8000-000000000001
- dddddddd-0000-4000-8000-000000000002
- dddddddd-0000-4000-8000-000000000003
text_ids: []
question: null
question_options: []
changed_blueprint_ids: []
degraded: false
error: null
error_message: null
usage:
duration_ms: 23140
text/event-stream:
schema:
type: string
example: 'event: turn.accepted
id: 1
data: {"type": "turn.accepted", "seq": 1, "data": {"turn_id": "cccc0000000000000000000000000001", "conversation_id": "cv1_..."}}
event: turn.started
id: 2
data: {"type": "turn.started", "seq": 2, "data": {}}
event: text.delta
id: 9
data: {"type": "text.delta", "seq": 9, "data": {"text": "There are 3 doors"}}
event: ping
data: {"type": "ping", "seq": 9, "data": {}}
event: turn.done
id: 14
data: {"type": "turn.done", "seq": 14, "data": {"id": "cccc0000000000000000000000000001", "status": "done", "answer": {"text": "..."}}}
'
'400':
description: 'On a key enabled for partner users: `X-Kamai-Partner` missing (`partner_id_required`) or not valid for this key (`partner_id_invalid`), or `X-Kamai-Partner-User-Token` missing (`partner_user_token_required`).'
content:
application/json:
schema:
$ref: '#/components/schemas/api__external__v1__conversations__ErrorResponse'
'401':
description: No API key, a revoked key (`unauthorized`), or a partner user token the partner rejected (`partner_token_invalid`).
content:
application/json:
schema:
$ref: '#/components/schemas/api__external__v1__conversations__ErrorResponse'
'403':
description: The key may not use `tools=edit` or rename a conversation (`edit_not_allowed`), may not delete a conversation (`delete_not_allowed`), may not act for partner users (`act_as_not_allowed`), or the partner user's account is not linked to Kamai (`partner_user_not_linked`).
content:
application/json:
schema:
$ref: '#/components/schemas/api__external__v1__conversations__ErrorResponse'
'404':
description: Project, sheet or conversation not found for the acting user (`not_found`).
content:
application/json:
schema:
$ref: '#/components/schemas/api__external__v1__conversations__ErrorResponse'
'409':
description: A turn is still running in this conversation (`conversation_busy`), or a retry's turn already ended (`already_sent`); `turn_id` names it.
content:
application/json:
schema:
$ref: '#/components/schemas/api__external__v1__conversations__ErrorResponse'
'413':
description: The sheet or project is too large for one turn (`context_too_large`).
content:
application/json:
schema:
$ref: '#/components/schemas/api__external__v1__conversations__ErrorResponse'
'429':
description: 'Too many turns (`rate_limited`): 2 at once per key, or the key''s daily turns (UTC day). See `Retry-After`.'
content:
application/json:
schema:
$ref: '#/components/schemas/api__external__v1__conversations__ErrorResponse'
'502':
description: The assistant or the partner failed to answer (`upstream_error`, `partner_unavailable`). Safe to retry.
content:
application/json:
schema:
$ref: '#/components/schemas/api__external__v1__conversations__ErrorResponse'
'503':
description: Chat is not available on this deployment (`chat_unavailable`).
content:
application/json:
schema:
$ref: '#/components/schemas/api__external__v1__conversations__ErrorResponse'
'504':
description: The turn did not finish within 600 s (`turn_timeout`). It keeps running and lands in the conversation; `turn_id` names it. Retry with the same `Idempotency-Key` to wait for it again.
content:
application/json:
schema:
$ref: '#/components/schemas/api__external__v1__conversations__ErrorResponse'
'422':
description: 'A field is invalid: `detail` lists which, or carries `invalid_request`.'
components:
schemas:
MessageCreate:
properties:
text:
type: string
maxLength: 4000
minLength: 1
title: Text
description: The message, as a user would type it in the Kamai chat.
visible_feature_ids:
items:
type: string
type: array
maxItems: 2000
title: Visible Feature Ids
description: Elements the user is looking at.
sheet_numbers:
anyOf:
- additionalProperties:
type: integer
type: object
- type: 'null'
title: Sheet Numbers
description: blueprint_id → the number the user sees on that sheet in a project grid.
mentions:
items:
$ref: '#/components/schemas/Mention'
type: array
maxItems: 50
title: Mentions
attachments:
items:
$ref: '#/components/schemas/Attachment'
type: array
maxItems: 4
title: Attachments
language:
anyOf:
- type: string
maxLength: 16
- type: 'null'
title: Language
description: Answer language, e.g. `en`, `he`, `fr`.
type: object
required:
- text
title: MessageCreate
Turn:
properties:
id:
type: string
title: Id
description: 'The turn id: rate it with POST .../turns/{turn_id}/feedback.'
conversation_id:
type: string
title: Conversation Id
status:
type: string
enum:
- queued
- waiting_previous_turn
- running
- done
- failed
- cancelled
title: Status
created_at:
anyOf:
- type: string
- type: 'null'
title: Created At
finished_at:
anyOf:
- type: string
- type: 'null'
title: Finished At
answer:
anyOf:
- $ref: '#/components/schemas/Answer'
- type: 'null'
error:
anyOf:
- type: string
- type: 'null'
title: Error
error_message:
anyOf:
- type: string
- type: 'null'
title: Error Message
usage:
$ref: '#/components/schemas/TurnUsage'
type: object
required:
- id
- conversation_id
- status
- usage
title: Turn
Mention:
properties:
type:
type: string
enum:
- feature
- blueprint
- saved_table
title: Type
id:
type: string
minLength: 1
title: Id
blueprint_id:
anyOf:
- type: string
- type: 'null'
title: Blueprint Id
description: The feature's sheet. Needed for `feature` in a project conversation.
label:
anyOf:
- type: string
maxLength: 200
- type: 'null'
title: Label
description: How the user named it, e.g. `A-101`.
type: object
required:
- type
- id
title: Mention
Table:
properties:
index:
type: integer
title: Index
description: 'Its place in the answer: `{{table:N}}` in `text`.'
title:
anyOf:
- type: string
- type: 'null'
title: Title
columns:
items: {}
type: array
title: Columns
description: '`key`, `label`, `type`, `unit` per column.'
rows:
items:
$ref: '#/components/schemas/TableRow'
type: array
title: Rows
type: object
required:
- index
title: Table
TurnUsage:
properties:
duration_ms:
type: integer
title: Duration Ms
type: object
required:
- duration_ms
title: TurnUsage
TableRow:
properties:
cells:
title: Cells
description: The row's values, keyed by column `key`.
feature_ids:
items:
type: string
type: array
title: Feature Ids
description: The drawing elements this row counts, as in GET /v1/blueprints/{blueprint_id}.
text_ids:
items:
type: string
type: array
title: Text Ids
description: The printed words the row was read from.
type: object
title: TableRow
Highlight:
properties:
blueprint_id:
type: string
title: Blueprint Id
feature_ids:
items:
type: string
type: array
title: Feature Ids
text_ids:
items:
type: string
type: array
title: Text Ids
type: object
required:
- blueprint_id
title: Highlight
Answer:
properties:
text:
type: string
title: Text
description: The answer. Each table stands where `{{table:N}}` is.
default: ''
tables:
items:
$ref: '#/components/schemas/Table'
type: array
title: Tables
boq:
anyOf:
- additionalProperties: true
type: object
- type: 'null'
title: Boq
description: A structured bill of quantities, when the turn produced one.
highlights:
items:
$ref: '#/components/schemas/Highlight'
type: array
title: Highlights
description: Elements the answer points at, per sheet (what the app highlights).
question:
anyOf:
- type: string
- type: 'null'
title: Question
description: Set when the assistant asks the user to clarify before answering.
question_options:
items:
type: string
type: array
title: Question Options
description: Suggested replies to `question`.
changed_blueprint_ids:
items:
type: string
type: array
title: Changed Blueprint Ids
description: Sheets the assistant changed (tools=edit only).
degraded:
type: boolean
title: Degraded
description: 'True when the answer is partial: the turn ran out of time or failed after it began.'
default: false
type: object
title: Answer
Attachment:
properties:
name:
type: string
maxLength: 256
minLength: 1
title: Name
mime_type:
type: string
enum:
- image/png
- image/jpeg
- application/pdf
title: Mime Type
data_base64:
type: string
maxLength: 14000000
title: Data Base64
type: object
required:
- name
- mime_type
- data_base64
title: Attachment
api__external__v1__conversations__ErrorResponse:
properties:
detail:
additionalProperties: true
type: object
title: Detail
description: '`error` is a stable code; `message` is for people.'
type: object
required:
- detail
title: ErrorResponse
securitySchemes:
HTTPBearer:
type: http
scheme: bearer