Create an assistant
POST/api/v1/assistants
scope
assistants:writeIdempotentCreates an assistant. targets binds it to the channels it should answer on; a channel already bound to another assistant is refused with 409 conflict and reason: target_taken, because one channel has exactly one assistant.
Body
application/json| Name | Type | Description |
|---|---|---|
namerequired | string | Display name |
coloroptional | string | null | Accent colour, or null |
enabledoptional | boolean | Defaults to true. A disabled assistant refuses every run start |
system_promptoptional | string | Persona and standing instructions |
modeloptional | string | null | Model id, or null to inherit |
temperatureoptional | number | null | Sampling temperature |
tool_accessoptional | map<string, boolean> | Per-tool on or off, by tool id from GET /tools |
gated_toolsoptional | string[] | Tool ids that must raise a human approval first |
scheduleoptional | object | When it is on duty |
schedule.moderequired | string (enum) | When the assistant may answeralwayscustom_hoursoutside_business_hours |
schedule.hoursoptional | map<string, string[][]> | null | custom_hours only: weekday to [open, close] ranges |
default_handoff_user_idoptional | string (uuid) | null | Member who receives its handoffs |
history_limitoptional | integer | null | Recent messages to send; null for the full history |
transcribe_mediaoptional | boolean | Transcribe voice notes and describe photos |
handle_voiceoptional | boolean | Send voice notes to the model |
handle_imagesoptional | boolean | Send customer photos to the model |
voice_repliesoptional | boolean | May reply with a voice note |
voice_provideroptional | string | null | Text-to-speech provider |
voice_idoptional | string | null | Provider-specific voice |
targetsoptional | string[] | What it answers on: channel names, or pwa:<connection_id> for one linked personal number. Replaces the whole list |
Request
curl -X POST "https://your-crm.example.com/api/v1/assistants" \
-H "Authorization: Bearer $RAABTA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2a9e-3b4d-4c5e-8f7a-9b0c1d2e3f4a" \
-d '{
"name": "Front desk",
"system_prompt": "You are the front desk for Acme Clinic. Book appointments, answer questions about opening hours and prices, and hand off anything clinical to a human.",
"targets": [
"whatsapp",
"webchat"
]
}'Response · 201 Created
{
"data": {
"id": "8f2c1d3e-4a5b-4c6d-9e0f-1a2b3c4d5e6f",
"name": "Front desk",
"color": "#0ea5e9",
"enabled": true,
"system_prompt": "You are the front desk for Acme Clinic. Book appointments, answer questions about opening hours and prices, and hand off anything clinical to a human.",
"model": null,
"temperature": null,
"tool_access": {
"contacts.search": true,
"bookings.create": true
},
"gated_tools": [
"bookings.cancel"
],
"delegation": {
"enabled": false,
"allowed_tools": null,
"max_children_per_run": 3,
"max_cost_usd": null
},
"schedule": {
"mode": "always"
},
"default_handoff_user_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
"history_limit": 20,
"transcribe_media": true,
"handle_voice": true,
"handle_images": false,
"voice_replies": false,
"voice_provider": null,
"voice_id": null,
"targets": [
"whatsapp",
"webchat"
],
"instruction_count": 3,
"created_at": "2026-08-01T09:00:00.000Z",
"updated_at": "2026-09-01T11:30:00.000Z"
}
}Response
Wrapped in { data: … }| Name | Type | Description |
|---|---|---|
idrequired | string (uuid) | Assistant id |
namerequired | string | Display name, shown in the dashboard and the activity feed |
colorrequired | string | null | Accent colour used in the activity feed |
enabledrequired | boolean | A disabled assistant refuses every run start |
system_promptrequired | string | The persona and standing instructions sent to the model on every tick |
modelrequired | string | null | Model id, or null to inherit the account default |
temperaturerequired | number | null | Sampling temperature, or null to inherit the account default |
tool_accessrequired | map<string, boolean> | Per-tool on or off, by tool id. A tool absent here takes the catalogue default from GET /tools |
gated_toolsrequired | string[] | Tool ids that raise a human approval before running, on top of the always-gated critical tools |
delegationrequired | object | Whether and how far this assistant may delegate |
delegation.enabledrequired | boolean | May this assistant hand work to sub-agents |
delegation.allowed_toolsrequired | string[] | null | Tools a sub-agent may be given; null means anything the parent holds, and an empty list means nothing |
delegation.max_children_per_runrequired | integer | Sub-agents one run may spawn, clamped by the engine |
delegation.max_cost_usdrequired | number | null | Tighter spend ceiling for the whole tree; null uses the engine default |
schedulerequired | object | When this assistant is allowed to answer |
schedule.moderequired | string (enum) | always = any hour; outside_business_hours = only when the business is closed; custom_hours = only inside the hours belowalwayscustom_hoursoutside_business_hours |
schedule.hoursoptional | map<string, string[][]> | null | custom_hours only: weekday key to a list of [open, close] times as HH:MM. A missing day is closed |
default_handoff_user_idrequired | string (uuid) | null | Member who receives conversations this assistant hands off |
history_limitrequired | integer | null | Recent messages sent to the model; null is the full history and 0 is none |
transcribe_mediarequired | boolean | Transcribe inbound voice notes and describe photos for the inbox |
handle_voicerequired | boolean | Send inbound voice notes to the model |
handle_imagesrequired | boolean | Send inbound customer photos to the model |
voice_repliesrequired | boolean | May reply with a synthesised voice note |
voice_providerrequired | string | null | Text-to-speech provider, or null for none |
voice_idrequired | string | null | Provider-specific voice, or null for its default |
targetsrequired | string[] | What it answers on: a channel name, or pwa:<connection_id> for one linked personal number |
instruction_countrequired | integer | Instructions bound to this assistant |
created_atrequired | string (date-time) | When the assistant was created |
updated_atrequired | string (date-time) | When it was last edited |
Errors
| Status | Code | When |
|---|---|---|
| 400 | bad_request | The request could not be parsed: malformed JSON, an invalid cursor, or a query parameter of the wrong shape. |
| 400 | validation_error | The body or query failed validation. `details` lists each failing field with a `path` and a `message`. |
| 401 | unauthorized | No usable API key: the Authorization header is missing or malformed, or the key is unknown, revoked or expired. The three are deliberately indistinguishable. |
| 403 | forbidden | The key is valid but lacks the scope this endpoint requires, or the request came from an address outside the key’s IP allowlist. The message says which. |
| 403 | account_suspended | The account this key belongs to is suspended. Rotating the key will not help; contact support. |
| 409 | conflict | The request conflicts with current state: a duplicate phone number on create, an invalid state transition, or an idempotent request that is still in flight. |
| 422 | unprocessable | The request was well-formed but cannot be carried out. `reason` is a stable string saying why (for example `outside_window` or `stage_not_in_pipeline`). |
| 429 | rate_limited | The per-key budget, or the per-IP budget for failed authentication, is exhausted. Honour `Retry-After` before retrying. |
| 500 | internal | Something failed on our side. Safe to retry with the same Idempotency-Key; quote `request_id` if it persists. |