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.
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,
"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",
"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.
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 |
| 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;
}
}