# 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.

```bash
curl https://handover.support/api/v1/agent \
  -H "Authorization: Bearer YOUR_API_TOKEN"
```

```json
{
  "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 |

```bash
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`:

```json
{
  "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 |

```bash
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:

```json
{
  "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 |

```bash
curl "https://handover.support/api/v1/conversations/{id}/messages?after=102" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
```

```json
{
  "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:

```json
{ "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

```javascript
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;
  }
}
```
