Developers
REST API
Call a Harmona agent from your own backend: create an agent API key, stream replies over server-sent events, keep per-user history and handle errors.
La documentación está en inglés y turco, como la interfaz de Harmona. Esta página se muestra en inglés.
Any agent can answer over HTTPS. An API key belongs to one agent, so requests never name the agent. You identify your own end users with your own IDs, and each of them gets a conversation that persists between calls. API access is included on Individual, Business and Enterprise plans.
Turn on API access and create a key
- 1Open the agent in Studio → Agents and open its Output Channels.
- 2Turn on API Access.
- 3Under API Keys, select Create Key and name it, for example "Production".
- 4Copy the key. It starts with hapi_ and is shown only once.
Only organization admins can create keys. Keys created in the app don't expire. Use one key per environment or app, so you can revoke one without touching the others.
Atención
Authentication and base URL
Send the key as a bearer token on every request. On Harmona Cloud the base URL is https://api.harmona.ai; on private cloud or on-premises, use your own deployment's API address. Every endpoint is under /b2c/v1.
Authorization: Bearer hapi_your_keyPeligro
Endpoints
POST/b2c/v1/chat
Sends a turn and streams the agent's reply.
GET/b2c/v1/chat/rooms
Lists an end user's conversations, newest first.
GET/b2c/v1/chat/messages
Returns a conversation's history.
GET/b2c/v1/chat/handoff
Returns a conversation as plain text, for handing it to a person.
GET/b2c/v1/widget-config
Returns the Web SDK settings configured in Harmona.
Send a message
| Field | Required | Description |
|---|---|---|
| external_user_id | Yes | Your ID for the end user, up to 255 characters. The same value always returns to the same user's conversations. |
| messages | Yes | The turn, as a list of objects with role (user or assistant) and content. The last message must come from the user. You can put a greeting you showed earlier before it, as an assistant message. |
| room_id | No | Continue a specific conversation. Without it, the user's most recently active conversation continues; the first call creates it. |
| room_name | No | A name for the conversation when it is created. |
curl -N https://api.harmona.ai/b2c/v1/chat \
-H "Authorization: Bearer hapi_your_key" \
-H "Content-Type: application/json" \
-d '{
"external_user_id": "customer-4821",
"messages": [{ "role": "user", "content": "Where is my order?" }]
}'Reading the stream
The reply is a text/event-stream. Each frame is a line that starts with data: followed by JSON, and the stream ends with data: [DONE]. The frames are agent run events. The reply you show arrives in events named on_chain_stream, in data.chunk:
- type: printable carries reply text in content. Skip chunks where is_thinking is true.
- type: tool carries a structured result in artifact, with artifact.type and artifact.data, such as product cards or suggested next questions.
- You can ignore every other event.
A reply looks like this, with each event shortened:
data: {"event": "on_chain_stream", "data": {"chunk": {"type": "printable", "content": "Your order left "}}, ...}
data: {"event": "on_chain_stream", "data": {"chunk": {"type": "printable", "content": "our warehouse today."}}, ...}
data: [DONE]If the agent fails mid-reply, the stream sends a short apology as printable text, then an error event with data.error and data.error_code, then [DONE]. The code is rate_limit_exceeded when the model provider is busy (try again shortly) and internal_error otherwise.
A minimal reader in Node.js 18 or later:
const res = await fetch("https://api.harmona.ai/b2c/v1/chat", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HARMONA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
external_user_id: "customer-4821",
messages: [{ role: "user", content: "Where is my order?" }],
}),
});
if (!res.ok) throw new Error(`Harmona ${res.status}: ${await res.text()}`);
const decoder = new TextDecoder();
let buffer = "";
for await (const bytes of res.body) {
buffer += decoder.decode(bytes, { stream: true });
const frames = buffer.split("\n\n");
buffer = frames.pop();
for (const frame of frames) {
const payload = frame.replace(/^data: /, "");
if (payload === "[DONE]") continue;
const evt = JSON.parse(payload);
const chunk = evt.data?.chunk;
if (evt.event === "on_chain_stream" && chunk?.type === "printable" && !chunk.is_thinking) {
process.stdout.write(chunk.content);
}
if (evt.event === "error") console.error(evt.data.error_code);
}
}Conversations and history
- GET /chat/rooms?external_user_id=… returns each conversation's room_id, room_name, created_at and updated_at.
- GET /chat/messages?external_user_id=… returns the history. Add room_id for a specific conversation, limit (1–200, default 50) and offset for paging.
- GET /chat/handoff?external_user_id=… returns agent_name and messages, a flat list of role (human or ai) and content. Tool steps are left out, so you can pass it straight to a support agent.
Errors
Error responses are JSON with a detail message. Most also carry an error_code. Branch on the code, not on the message: the message is in English and may change.
{
"detail": "…",
"error_code": "usage_limit_exceeded"
}| Status | error_code | Meaning |
|---|---|---|
| 400 | validation_error | The body is invalid, for example the last message isn't from the user. |
| 401 | none | The key is missing, wrong, expired or deleted, or API access is turned off for the agent. |
| 402 | usage_limit_exceeded | The workspace has used its credits or reached its usage threshold. |
| 402 | subscription_past_due | A card payment failed and is still open. It is retried automatically on day 1, 3 and 7. |
| 402 | subscription_inactive | The subscription is suspended or canceled. |
| 402 | trial_expired | The workspace's free trial has ended. |
| 404 | room_not_found | The room_id doesn't exist or belongs to another agent. |
| 409 | none | The agent can't chat right now. Check its model and instructions. |
| 503 | none | A temporary problem. Retry. |
Handling 402 errors
Actualizado 2026-09-24
