Handover REST API (v1)
Build a fully custom chat integration for your support agent: start conversations, send customer messages and poll for AI (or human) replies. Everything the hosted chat page can do over JSON.
The API is available on the Professional plan. Each support agent has its own API token, found under Integrations on the agent's page in your dashboard.
Base URL
https://handover.support/api/v1
Authentication
Every request must include your agent's secret API token as a Bearer header:
Authorization: Bearer YOUR_API_TOKEN
The token identifies both your organisation and the specific agent, so no other credentials are needed. Keep it secret: anyone holding it can read and write that agent's API conversations. You can rotate it at any time from the agent's Integrations section (the old token stops working immediately).
How a conversation works
AI answers are generated asynchronously, typically within a few seconds. The flow is:
POST /conversations- create a conversation. The response includes the agent's greeting.POST /conversations/{id}/messages- send the customer's message.GET /conversations/{id}/messages?after={last_message_id}- poll every 1-2 seconds until a new message with"role": "ai"(or"human"/"system") appears.
Conversations may hand off to a human team member - watch the conversation status field. Messages sent to a conversation that is waiting_for_human or with_human_agent are delivered to the team rather than answered by the AI.
Languages
Customers can write in any language. When the agent has Multilingual Support enabled (the default), the AI answers in the customer's language, and replies written by human team members are automatically delivered in it too - your integration does not need to do anything. The detected language appears as an ISO 639-1 code in the conversation's language field (null until the first clear detection).
The agent's own working language is primary_language on the agent endpoint (with multilingual indicating whether the feature is on) - useful if you want to show your own "write in any language" hint when a visitor's locale differs from it.
Conversation status values
| Status | Meaning |
|---|---|
active |
The AI is answering |
waiting_for_human |
Handed off, waiting for a team member |
with_human_agent |
A team member is replying |
resolved |
Closed (sending a new message reopens it) |
archived |
Closed permanently |
Message roles
| Role | Meaning |
|---|---|
customer |
The end customer (messages you send) |
ai |
The AI agent |
human |
A human team member |
system |
Status notices (handoffs, resolutions) |
Endpoints
Get agent details
GET /api/v1/agent
Returns the agent's name, greeting, conversation starters, availability and branding - useful for rendering your own chat UI.
curl https://handover.support/api/v1/agent \
-H "Authorization: Bearer YOUR_API_TOKEN"
{
"name": "Acme Support",
"description": "Helps with orders and returns",
"greeting_message": "Hi! How can I help you today?",
"conversation_starters": ["How do I return an item?"],
"available": true,
"allow_human_handoff": true,
"primary_language": "en",
"multilingual": true,
"chat_url": "https://acme.handover.support",
"branding": {
"accent_color": "#1D4ED8",
"background_color": "#F8FAFC",
"text_color": "#0F172A",
"font_family": "'Inter', ui-sans-serif, system-ui, sans-serif"
}
}
Create a conversation
POST /api/v1/conversations
| Parameter | Type | Required | Description |
|---|---|---|---|
customer_email |
string | No | Lets your team recognise the customer and builds their profile history |
customer_name |
string | No | Shown to your team alongside the conversation |
curl -X POST https://handover.support/api/v1/conversations \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"customer_email": "jo@example.com", "customer_name": "Jo"}'
Returns 201 Created:
{
"id": "0b8f6a2e-6c1d-4d0a-9a4e-1f2b3c4d5e6f",
"status": "active",
"title": "Acme Support Support Conversation",
"customer_email": "jo@example.com",
"customer_name": "Jo",
"language": null,
"created_at": "2026-07-20T12:00:00Z",
"messages": [
{ "id": 101, "role": "ai", "content": "Hi! How can I help you today?", "confidence": null, "quick_replies": [], "created_at": "2026-07-20T12:00:00Z" }
]
}
The id is the conversation identifier used in all further requests.
Get a conversation
GET /api/v1/conversations/{id}
Returns the conversation envelope plus the full customer-visible transcript (same shape as the create response). Internal team notes are never included.
Send a message
POST /api/v1/conversations/{id}/messages
| Parameter | Type | Required | Description |
|---|---|---|---|
content |
string | Yes | The customer's message text |
curl -X POST https://handover.support/api/v1/conversations/{id}/messages \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"content": "How do I return an item?"}'
Returns 201 Created with the stored message and the conversation's current status:
{
"message": { "id": 102, "role": "customer", "content": "How do I return an item?", "confidence": null, "quick_replies": [], "created_at": "2026-07-20T12:00:05Z" },
"conversation": { "id": "0b8f6a2e-...", "status": "active" }
}
Sending a message to a resolved conversation reopens it.
List messages (poll for replies)
GET /api/v1/conversations/{id}/messages?after={message_id}
| Parameter | Type | Required | Description |
|---|---|---|---|
after |
integer | No | Only return messages with an id greater than this. Omit for the full transcript |
curl "https://handover.support/api/v1/conversations/{id}/messages?after=102" \
-H "Authorization: Bearer YOUR_API_TOKEN"
{
"conversation": { "id": "0b8f6a2e-...", "status": "active" },
"messages": [
{ "id": 103, "role": "ai", "content": "You can return any item within 30 days...", "confidence": 0.92, "quick_replies": ["Start a return"], "created_at": "2026-07-20T12:00:09Z" }
]
}
Poll every 1-2 seconds after sending a message. An AI reply usually arrives within a few seconds; replies from human team members can arrive at any time while a conversation is open.
quick_replies are short suggested follow-ups you can render as tappable buttons; send the chosen text back as a normal message.
List conversations
GET /api/v1/conversations?customer_email={email}&before={cursor}
Newest first, 50 per page. Use this to show a customer their conversation history in your own UI.
| Parameter | Type | Required | Description |
|---|---|---|---|
customer_email |
string | No | Only conversations belonging to this customer (guest or account email) |
before |
integer | No | The cursor from the previous page, for older conversations |
{
"conversations": [ { "id": "0b8f6a2e-...", "status": "resolved", "title": "Cancel Monthly Subscription", "...": "..." } ],
"has_more": false,
"cursor": 118
}
Request a human
POST /api/v1/conversations/{id}/request_human
Escalates the conversation to your team, exactly like the "Request Human" button on the hosted chat. Optional body: { "reason": "..." }. Idempotent - calling it while a human is already involved changes nothing.
Returns 403 with code handover_disabled when the agent's Allow Human Handover setting is off.
Resolve a conversation
POST /api/v1/conversations/{id}/resolve
Closes the conversation as the customer (your "mark as solved" button). Idempotent. Sending a new message later reopens it. Resolving after a human has replied also feeds the knowledge-learning pipeline, just like the hosted chat.
Rate an AI answer
POST /api/v1/conversations/{id}/messages/{message_id}/feedback
Body: { "helpful": true } or { "helpful": false } - the thumbs up/down signal behind the agent's quality reporting. Only ai role messages can be rated (404 otherwise).
Errors
All errors use the same JSON envelope:
{ "error": { "code": "unauthorized", "message": "Invalid API token." } }
| HTTP status | Code | Meaning |
|---|---|---|
| 400 | bad_request |
A required parameter is missing |
| 401 | unauthorized |
Missing or invalid API token |
| 403 | forbidden |
The agent is inactive or the plan does not include the API |
| 403 | handover_disabled |
The agent's Allow Human Handover setting is off |
| 404 | not_found |
Unknown conversation id (or not owned by this agent) |
| 422 | unprocessable |
Validation failed (details in message) |
| 429 | - | Rate limited - slow down and retry after a short wait |
Rate limits
API requests are limited to 120 requests per minute per token. Polling once per second per open conversation sits comfortably within this. If you receive a 429, back off and retry after a few seconds.
Minimal client example
const BASE = "https://handover.support/api/v1";
const HEADERS = {
"Authorization": "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
};
async function ask(question) {
const conversation = await (await fetch(`${BASE}/conversations`, {
method: "POST", headers: HEADERS, body: JSON.stringify({})
})).json();
const sent = await (await fetch(`${BASE}/conversations/${conversation.id}/messages`, {
method: "POST", headers: HEADERS, body: JSON.stringify({ content: question })
})).json();
// Poll until a reply newer than our message arrives
let after = sent.message.id;
while (true) {
await new Promise(resolve => setTimeout(resolve, 1500));
const { messages } = await (await fetch(
`${BASE}/conversations/${conversation.id}/messages?after=${after}`,
{ headers: HEADERS }
)).json();
const reply = messages.find(m => m.role !== "customer");
if (reply) return reply.content;
if (messages.length) after = messages[messages.length - 1].id;
}
}