REST API

Raw

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:

  1. POST /conversations - create a conversation. The response includes the agent's greeting.
  2. POST /conversations/{id}/messages - send the customer's message.
  3. 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;
  }
}