> ## Documentation Index
> Fetch the complete documentation index at: https://chatbase.co/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Helpdesk

> Programmatically create, triage, and reply to support tickets, and read your operators' attendance and activity, for your Chatbase AI agents.

The Helpdesk API is the same ticketing that powers the Helpdesk tab in the dashboard. You can create tickets on behalf of customers, list and search them, and change a ticket's status, assignee, or team. Each ticket carries a message thread you can read and reply to. You can also read the Attendance report and Activity timeline of the people who work those tickets.

Tickets are numbered per AI agent. The `ticketNumber` path parameter is that per-agent number, not a global id. A ticket's `channel` records where it originated, such as email, the chat widget, or WhatsApp. Tickets created through this API always have `channel: "api"`.

## Statuses

Each ticket has a status, and each status belongs to one of six fixed categories: `new`, `on_you`, `on_customer`, `on_hold`, `closed`, `cancelled`. The statuses themselves are configured per agent in the dashboard; every category has exactly one default status.

Write endpoints accept a status in one of two forms, at most one per request:

| Field | Meaning |
| - | - |
| `statusId` | A specific configured status. Must belong to the AI agent and be active; archived statuses are rejected. |
| `statusCategory` | Resolves to that category's default status. |

[List ticket statuses](https://chatbase.co/docs/api-reference/v2/helpdesk/list-ticket-statuses.md) returns the AI agent's active statuses with their ids, categories, and labels. Each status has two labels: `externalLabel` is what the customer sees, `internalLabel` is what the dashboard shows.

## Assignment and routing

When you create a ticket, the assignee fields decide whether auto-assignment runs. `assigneeId` and `assigneeEmail` are a mutually exclusive pair; sending both is a 400.

| Assignee | `teamId` | Result |
| - | - | - |
| Provided | Any | Written as given. No auto-assignment. |
| `assigneeId: null` | Any | Ticket is created unassigned. The team is still written if given. |
| Omitted | Provided | An agent is picked within that team by its assignment strategy. |
| Omitted | Omitted | Your [triggers](https://chatbase.co/docs/user-guides/chatbot/help-desk/triggers.md) decide the team and assignee. With no matching trigger, the ticket stays unassigned with no team. |

[Update a ticket](https://chatbase.co/docs/api-reference/v2/helpdesk/update-a-ticket.md) never auto-assigns. Omitted fields keep their current value, `assigneeId: null` unassigns the ticket, and `teamId: null` clears the team.

[List teams](https://chatbase.co/docs/api-reference/v2/helpdesk/list-teams.md) returns the AI agent's teams; exactly one is the default.

## Messages

A ticket's thread contains three message types:

| Type | Meaning |
| - | - |
| `reply` | Customer-visible. Delivered over the ticket's origin channel. |
| `note` | Internal. Never delivered to the customer. |
| `event` | Record of a change to the ticket: status, assignee, team, priority, tags, or a custom field, plus each email, webhook, and Slack message a trigger sent. Excluded from list results unless requested via `types`. |

`metadata.eventType` names the kind of change. Changes made by a trigger carry `triggerId`, `triggerName`, and `runId`, and every event from the same trigger run shares its `runId`. Webhook events report `deliveryStatus` (`pending`, `delivered`, or `failed`) and the `statusCode` your endpoint returned. New event kinds may be added, so treat an unknown `eventType` as a generic change rather than an error.

[Add a message to a ticket](https://chatbase.co/docs/api-reference/v2/helpdesk/add-a-message-to-a-ticket.md) currently accepts only `type: "reply"`, attributed to a team member via `authorId` or `authorEmail`. The body is GitHub-flavored Markdown; raw HTML is stripped. Delivery to the customer is asynchronous, so a 201 means the reply was recorded, not that it reached the customer.

## Attendance and activity

Two read-only endpoints return your operators' Helpdesk **Attendance** report and **Activity** timeline. Use them to load your operators' hours into your own reporting on a schedule, instead of exporting files by hand. [Attendance](https://chatbase.co/docs/user-guides/chatbot/help-desk/attendance.md) and [Activity](https://chatbase.co/docs/user-guides/chatbot/help-desk/activity.md) explain what each value means.

| Endpoint | Returns |
| - | - |
| [List attendance rows](https://chatbase.co/docs/api-reference/v2/helpdesk/list-attendance-rows.md) | The Attendance report, with the same values as its CSV export: one row per operator for each part of a shift that falls on a day, plus one for time logged outside the shift. |
| [List activity segments](https://chatbase.co/docs/api-reference/v2/helpdesk/list-activity-segments.md) | Each ticket, untracked or idle segment of your operators' activity, exactly as it was recorded. |

Both take the same query parameters:

| Parameter | Meaning |
| - | - |
| `startDate`, `endDate` | First and last day to return, as `YYYY-MM-DD`. Both days are included, and the range can cover at most 31 days. `startDate` can go back 396 days, the same history the dashboard keeps, and `endDate` can't be after today. A date outside those limits returns 400 `VALIDATION_INVALID_BODY`. |
| `timezone` | IANA time zone, such as `America/New_York`. It decides where each day starts and ends, like the timezone picker on the dashboard. |
| `limit` | Items per page, from 1 to 1000. Defaults to 100. |
| `cursor` | The `pagination.cursor` of the previous page. |

The values are typed rather than CSV text. Durations are whole minutes, percentages are numbers, and times are ISO 8601 instants in UTC. An operator in several teams is one attendance row with a `teams` array sorted by name, where the CSV repeats the row for each team.

Both include only the AI agent's current Helpdesk operators, the same as the dashboard.

A day that hasn't ended yet returns what has happened so far. Pull days that have ended when you want numbers that won't change.

### Activity segments

Activity segments are the raw timeline the Activity tab is built from, so their totals can differ slightly from the Activity export:

* Two segments with 2 minutes or less between them stay separate. The dashboard joins them.
* Times aren't rounded.
* A segment that crosses midnight isn't split. It's listed only in the range where it started.

For the dashboard's totals, use `timeLoggedMinutes` and `productiveTimeMinutes` from the attendance rows, or the Activity export.

A segment lasts at most 24 hours. When the same activity goes on longer, a new segment starts the moment the previous one ends, so a long stretch comes as several segments with no gap between them.

A segment that is still open has `endedAt: null`, and its `lastSeenAt` is refreshed every few minutes while the Helpdesk is open, also when the operator is idle or the tab is in the background. So a daily pull of yesterday sees a segment that was still open at midnight without its end. Pull the last two days each time and keep the newest copy of each segment by `id`. A segment ends within about a day of its start, so the next day's pull gets its end.

### Access

The AI agent must have the Helpdesk turned on, or both endpoints return 403 `HELPDESK_NOT_ENABLED`. A key or connected app limited to certain permissions needs `analytics:read`, shown as **Analytics** on the consent screen. `helpdesk_tickets:read` doesn't include it.

### Rate limit

The two endpoints share a limit of 30 calls per 10 seconds per account. It counts every API key and connected app of the account together, and applies on top of the general [rate limit](https://chatbase.co/docs/api-reference/v2/authentication.md#rate-limiting). Over it, they return 429 `RATE_LIMIT_TOO_MANY_REQUESTS` with a `Retry-After` header that gives the seconds to wait.

Reading one page at a time stays under the limit, at about 10 to 20 calls per 10 seconds. You only reach it by running several reads at once, or several tools against the same account.

### Paging

These two lists differ from the others in two ways:

* There is no `pagination.total`. Keep requesting until `pagination.cursor` is `null`.
* A cursor only works with the `startDate`, `endDate` and `timezone` it came from. Changing any of them returns 400 `VALIDATION_CURSOR_QUERY_MISMATCH`.

Attendance rows go by day, then operator name. Activity segments go by start time, across all operators.

If operators, shifts, time off or activity change between two calls, later pages reflect the change. Reading days that have ended avoids this.

### Pull yesterday's data every day

```javascript Node.js theme={null}
const timeZone = "America/New_York";

const today = new Intl.DateTimeFormat("en-CA", { timeZone }).format(new Date());
const day = new Date(`${today}T00:00:00Z`);
day.setUTCDate(day.getUTCDate() - 1);
const yesterday = day.toISOString().slice(0, 10);

async function fetchReport(agentId, apiKey, report) {
  const items = [];
  let cursor;

  do {
    const params = new URLSearchParams({
      startDate: yesterday,
      endDate: yesterday,
      timezone: timeZone,
      limit: "1000",
    });
    if (cursor) params.set("cursor", cursor);

    const response = await fetch(
      `https://www.chatbase.co/api/v2/agents/${agentId}/helpdesk/${report}?${params}`,
      { headers: { Authorization: `Bearer ${apiKey}` } }
    );
    if (!response.ok) {
      throw new Error(`${report}: ${response.status} ${await response.text()}`);
    }

    const { data, pagination } = await response.json();
    items.push(...data);
    cursor = pagination.cursor;
  } while (cursor);

  return items;
}

const attendance = await fetchReport(agentId, apiKey, "attendance");
const activity = await fetchReport(agentId, apiKey, "activity");
```

```python Python theme={null}
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo

import requests

TIME_ZONE = "America/New_York"
yesterday = (datetime.now(ZoneInfo(TIME_ZONE)).date() - timedelta(days=1)).isoformat()


def fetch_report(agent_id: str, api_key: str, report: str):
    items = []
    cursor = None

    while True:
        params = {
            "startDate": yesterday,
            "endDate": yesterday,
            "timezone": TIME_ZONE,
            "limit": 1000,
        }
        if cursor:
            params["cursor"] = cursor

        response = requests.get(
            f"https://www.chatbase.co/api/v2/agents/{agent_id}/helpdesk/{report}",
            headers={"Authorization": f"Bearer {api_key}"},
            params=params,
        )
        response.raise_for_status()
        body = response.json()
        items.extend(body["data"])

        cursor = body["pagination"]["cursor"]
        if not cursor:
            break

    return items


attendance = fetch_report(agent_id, api_key, "attendance")
activity = fetch_report(agent_id, api_key, "activity")
```

> [!TIP]
>
> For a backfill, split the history into ranges of 31 days or less, such as calendar months, and read each range to its end with `limit=1000`. A year is 12 or 13 ranges per report. Reading two or three ranges at once is enough: more won't finish sooner, because they share the [rate limit](#rate-limit) of 30 calls per 10 seconds. On a 429, wait for `Retry-After` and send the same request again.

## Endpoints

> [card] [List tickets](https://chatbase.co/docs/api-reference/v2/helpdesk/list-tickets.md)
>
> Filterable, sortable, paginated list

> [card] [Search tickets](https://chatbase.co/docs/api-reference/v2/helpdesk/search-tickets.md)
>
> Free-text search over ticket messages

> [card] [Create a ticket](https://chatbase.co/docs/api-reference/v2/helpdesk/create-a-ticket.md)
>
> Open a ticket on behalf of a customer

> [card] [Get a ticket](https://chatbase.co/docs/api-reference/v2/helpdesk/get-a-ticket.md)
>
> Retrieve a single ticket by number

> [card] [Update a ticket](https://chatbase.co/docs/api-reference/v2/helpdesk/update-a-ticket.md)
>
> Change status, assignee, or team

> [card] [List ticket messages](https://chatbase.co/docs/api-reference/v2/helpdesk/list-ticket-messages.md)
>
> Read a ticket's thread

> [card] [Add a message to a ticket](https://chatbase.co/docs/api-reference/v2/helpdesk/add-a-message-to-a-ticket.md)
>
> Post an agent reply

> [card] [List teams](https://chatbase.co/docs/api-reference/v2/helpdesk/list-teams.md)
>
> The AI agent's teams and the default

> [card] [List ticket statuses](https://chatbase.co/docs/api-reference/v2/helpdesk/list-ticket-statuses.md)
>
> Configured statuses with ids and labels

> [card] [List attendance rows](https://chatbase.co/docs/api-reference/v2/helpdesk/list-attendance-rows.md)
>
> Each operator's day against their shift

> [card] [List activity segments](https://chatbase.co/docs/api-reference/v2/helpdesk/list-activity-segments.md)
>
> Ticket, untracked and idle time per operator

## Error codes

Helpdesk-specific error codes beyond the standard [authentication and rate-limiting errors](https://chatbase.co/docs/api-reference/v2/error-handling.md):

<table>
  <thead>
    <tr>
      <th>Code</th>
      <th>HTTP</th>
      <th>Description</th>
    </tr>
  </thead>

  <tbody>
    <tr><td>`HELPDESK_NOT_ENABLED`</td><td>403</td><td>The Helpdesk is not turned on for this AI agent.</td></tr>
    <tr><td>`VALIDATION_CURSOR_QUERY_MISMATCH`</td><td>400</td><td>On attendance and activity, the cursor came from a request with a different `startDate`, `endDate` or `timezone`.</td></tr>
    <tr><td>`TICKET_NOT_FOUND`</td><td>404</td><td>No ticket matches this number for the AI agent.</td></tr>
    <tr><td>`CONVERSATION_NOT_TAKEN_OVER`</td><td>409</td><td>The ticket is linked to a live conversation that has not been taken over from the AI agent, so a human reply cannot be posted. Take over the conversation from the dashboard first.</td></tr>
    <tr><td>`MESSAGE_CONTENT_NOT_RENDERABLE`</td><td>422</td><td>The message body rendered to empty HTML. This happens when it consists only of raw HTML, which is stripped. Send Markdown or plain text.</td></tr>
    <tr><td>`TICKET_INVALID_STATUS`</td><td>422</td><td>`statusId` does not belong to a status for this AI agent.</td></tr>
    <tr><td>`TICKET_ARCHIVED_STATUS`</td><td>422</td><td>`statusId` refers to an archived status, which cannot be applied.</td></tr>
    <tr><td>`TICKET_TEAM_MEMBER_NOT_FOUND`</td><td>422</td><td>Neither `assigneeId` nor `assigneeEmail` resolved to a team member on this account.</td></tr>
    <tr><td>`TICKET_TEAM_NOT_FOUND`</td><td>422</td><td>`teamId` does not belong to a team for this AI agent.</td></tr>
    <tr><td>`TEAM_MEMBER_NOT_FOUND`</td><td>422</td><td>On message creation, neither `authorId` nor `authorEmail` resolved to a team member on this account.</td></tr>
  </tbody>
</table>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.