API reference

Chat API

Send a message to one of your bots from your own backend and get the agent's reply. This is the core path for integrating a Hania agent into your product - a help desk, an app, an SMS or WhatsApp flow.

Auth: use an account API key with the chat:send ability (Authorization: Bearer hania_sk_…). This is a server-to-server call - for an in-browser chat experience, use the embeddable widget and a widget key instead, not an account key. See the API overview.

Send a message

POST /api/v1/chat/send - sends a message to a bot and returns its reply in one JSON response.

Request body

FieldTypeDescription
bot_idstring (uuid)Required. The bot to message.
messagestringRequired. The end user's message.
external_user_idstringYour own identifier for the end user, so the conversation is associated with them on your side.
conversation_idstring (uuid)Continue an existing conversation. Omit to let the server resolve or start one.
new_conversationbooleanStart a fresh conversation thread. Defaults to false.
agent_modebooleanRun the bot in agent mode. Defaults to false.
channelstringOptional. One of api, sms, or whatsapp.

Example

curl https://app.hania.ai/api/v1/chat/send \
  -H "Authorization: Bearer $HANIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "bot_id": "b1f2c3d4-5678-90ab-cdef-1234567890ab",
    "message": "Where is my October invoice?",
    "external_user_id": "user_8412"
  }'

Response

The reply is returned in the standard envelope:

{
  "success": true,
  "data": {
    "response": "Your October invoice was emailed on Oct 3 and is also in Billing → Invoices.",
    "conversation_id": "c9a8b7c6-1234-4def-89ab-aabbccddeeff",
    "timestamp": "2026-06-09T14:05:22Z"
  },
  "message": ""
}

Persist the returned conversation_id and pass it back as conversation_id on the next message to continue the same thread.

Starting a brand-new conversation returns an instant greeting (the bot's configured opener) without an LLM round-trip, so the first call is fast and isn't metered as a model turn.

Stream a reply

POST /api/v1/chat/send-stream - identical request body and the same chat:send ability, but the reply streams back as Server-Sent Events (Content-Type: text/event-stream) so you can render it token-by-token. The stream emits the conversation_id first, then content chunks, and finally a [DONE] marker; an error arrives as an event of the form { "type": "error", "code": "…" }. The connection closes when the reply is complete.

curl -N https://app.hania.ai/api/v1/chat/send-stream \
  -H "Authorization: Bearer $HANIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "bot_id": "b1f2c3d4-…", "message": "Where is my October invoice?" }'

Use /chat/send when you just want the final reply; use /chat/send-stream when you want to show the answer as it's generated.

/chat/send-stream also takes an optional goal string. It sets the conversation's goal and sends the message in one call; message is still required, and the Hania Code terminal sends the condition as both. An empty goal, or one over 4,000 characters, returns 400 before anything is sent.

Add to a running reply

POST /api/v1/chat/steer - adds a message to a reply that is still running, instead of waiting for it to finish and starting a new one. The agent reads it between steps, after it has the results of the tools it just used and before it decides what to do next, and takes it into account for the rest of the reply. This is how the terminal client lets you type while the agent works, and it works the same for any conversation in your workspace with a reply running, including agent runs and a reply that carries on after an approval. Team chats aren't supported.

Request body

FieldTypeDescription
conversation_idstring (uuid)Required. The conversation whose reply is running.
messagestringRequired. The message to add, up to 10,000 characters. Spaces at the start and end are trimmed.
client_message_idstringRequired. Your own id for the message, up to 255 characters. It comes back when the agent reads the message. Sending the same id again for the same reply keeps the message once.

Example

curl https://app.hania.ai/api/v1/chat/steer \
  -H "Authorization: Bearer $HANIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "conversation_id": "c9a8b7c6-1234-4def-89ab-aabbccddeeff",
    "message": "Use the staging database instead.",
    "client_message_id": "msg_0193"
  }'

Response

{
  "success": true,
  "data": { "queued": true },
  "message": "Message queued for the running reply"
}

queued means the message is waiting for the running reply. It doesn't mean the agent has read it yet.

When the agent reads it

The agent only reads queued messages between steps, so a reply that answers without using any tools never reads one. If the reply ends before the agent gets to your message - because it finished, was stopped, failed, or paused for an approval, a question or a credential form - the message is dropped. It isn't kept or sent later.

So treat the message event below as your receipt. If the reply ends and you haven't seen it, send the message as a new one with /chat/send-stream. That's what the terminal client does.

When the agent reads your message, it arrives as a message event with your client_message_id, after the last tool result of one step and before the next step starts:

  • On the open /chat/send-stream response, if that stream is serving the reply: {"type":"message","role":"user","content":"…","client_message_id":"…","steered":true}.
  • Always on the conversation channel, GET /api/v1/chat/stream, with the message's id and created_at as well. A reply that carries on after an approval is only visible there.

"steered": true is only on messages added this way, so you can tell them apart from any other user message.

Afterwards the message is part of the conversation like any other user message. Its metadata includes "steered": true and your client_message_id.

Errors

StatusCode or messageWhen
400Invalid request bodyThe body isn't JSON.
400Invalid or missing conversation_idThe conversation id is missing or isn't a uuid.
400message is required or message too long (max 10000 characters)The message is empty or too long.
400client_message_id is required (max 255 characters)The id is missing or too long.
403Missing ability: chat:sendThe key doesn't have chat:send.
404Conversation not foundThe conversation isn't in your workspace.
409TEAM_CONVERSATIONThe conversation is a team chat.
409NO_ACTIVE_TURNNo reply is running. That includes a reply waiting for an approval, a question or a credential form, because it has stopped until someone answers.

Any key in the workspace with chat:send can add to any reply running in that workspace, not only the replies it started.

Withdraw a queued message

DELETE /api/v1/chat/steer?conversation_id=…&client_message_id=… - takes back one queued message before the agent reads it. There's no body, and it needs the same chat:send ability. Other queued messages aren't touched.

{
  "success": true,
  "data": { "cancelled": true }
}

cancelled is true when the message was withdrawn. It's false when the agent had already read it (it then stays in the reply and in the conversation), when no message with that id was queued, or when no reply is running. A false answer isn't an error.

The errors are the same as for adding a message, except that there's no message to check and no NO_ACTIVE_TURN.

Background-run notices

When bot_id is the workspace's assistant, it can start one of your Autonomous Agents as a background run. When that run finishes, fails or is stopped, Hania adds a notice to the conversation the assistant was in. If a reply is still running there, the assistant reads the notice before its next step. If the conversation is idle, the assistant replies on its own to report the result, up to three times for each message you send. After a Stop, the notice is still saved but no reply follows. While a run waits on a person there is no notice; it comes once the run ends.

The notice is saved as a message with role "user", but nobody typed it. Its metadata holds a notice object:

{
  "kind": "background_run",
  "status": "completed",
  "run_conversation_id": "5d1e2f3a-…",
  "agent_bot_id": "b1f2c3d4-…",
  "agent_name": "Weekly report"
}

status is completed, failed or stopped, and run_conversation_id is the run's own conversation. The message's content starts with [System] and is written for the assistant, so build what you show from notice instead. Older notices may not have agent_bot_id or agent_name.

On the conversation channel, GET /api/v1/chat/stream, the notice arrives as a message event with "role": "user" and the same notice object at the top level, without client_message_id or steered. The assistant's reply that follows streams there too, with no user message before it. A /chat/send response never includes either of them, so read the stream or the conversation's messages.

Other notices

Four more kinds are saved and streamed the same way, each followed by a reply the agent starts by itself. The first two belong to a conversation's goal.

kindWhenOther fields
goal_retryThe last reply failed on an error that may clear by itself, such as a busy model provider or a dropped connection. Hania tries again about 32 seconds later, up to three times, then the goal pauses.None.
goal_checkinWork running in the background has kept the goal waiting. The agent is asked to check on it after 30 minutes, an hour, then every two hours, three times at most.None.
background_shellA command the agent started in the background finished, or was stopped because the person's terminal or the runner shut down. A stopped command starts no reply.shell_id (string, sh_ and 8 hex characters). Finished: exit_code (number). Stopped: stopped: true and reason (string).
videoA clip the agent started with Generate Video is ready, or couldn't be made. The notice comes right after the message that delivers the clip or the error. If a reply is already running, the agent reads the notice there instead of starting a new one. The reply that follows may start another clip or join clips together, so a longer video can arrive over several replies with no user message between them.status (completed or failed), video_job_id (string). Completed: media_id (string), the same clip as the message before it.

Once three notices of one kind have started a reply since the person's last message, further ones of that kind are saved without starting another. The content of every notice starts with [System] and ends with a note that it is an automated message, so show your own label built from notice.

Billing & limits

Chat is a metered endpoint. If the workspace can't be billed, the call returns 402 with a code of SERVICE_SUSPENDED, PLAN_LIMIT_REACHED, or INSUFFICIENT_BALANCE - surface a "billing needs attention" state to the workspace owner. Standard rate-limit headers and 429 behavior apply as described in the overview.