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

# Sync tasks to ClickUp or Zendesk

> Create a ticket for each new task, write status changes back, and reconcile every night.

This guide connects Answering Agent tasks to a ticket system such as ClickUp or Zendesk. The [webhook](/webhooks) creates each ticket. [`PATCH /tasks/{task_id}`](/api/tasks#update-a-task) writes ticket changes back. A nightly job with [`GET /tasks`](/api/tasks#search-tasks) catches anything either side missed.

## Before you start

* Create a dedicated login for the integration, such as `integrations@yourcompany.com`, on one team with the admin role. Create the API key from that login. See [Authenticate](/api/overview#authenticate).
* Set up the [webhook](/webhooks#set-up-the-webhook) and store its secret on your server.
* Store `ANSWERING_AGENT_API_KEY` and `ANSWERING_AGENT_WEBHOOK_SECRET` as environment variables.

The examples use this helper:

```javascript theme={null}
const BASE = 'https://answeringagent.com/api/v1';

async function api(path, init = {}) {
  const res = await fetch(`${BASE}${path}`, {
    ...init,
    headers: {
      'X-API-KEY': process.env.ANSWERING_AGENT_API_KEY,
      'Content-Type': 'application/json',
      ...init.headers,
    },
  });
  if (res.status === 429) {
    await new Promise((r) => setTimeout(r, Number(res.headers.get('Retry-After') ?? 60) * 1000));
    return api(path, init);
  }
  return res;
}
```

## Create a ticket from each new task

The `task.created` webhook carries the task in `data`, the same object [`GET /tasks/{task_id}`](/api/tasks#get-a-task) returns. It has the title, description, status, assignee, category, priority, customer, and source.

1. Check the `X-Answering-Agent-Secret` header. See [Check the secret](/webhooks#check-the-secret).
2. Skip the event if you already processed its `id`.
3. Respond with a `2xx` status within 8 seconds. Do the rest after you respond.
4. Create the ticket from `data`. Store `data.id`, the API task ID, on the ticket.

```javascript theme={null}
async function onTaskCreated(event) {
  if (!event.data) return; // The dashboard does not show this task.
  await createTicket(event.data);
}
```

`createTicket` stands for your ticket system's own client. Store the task's `id`, and store its `status` and `assignee?.id` as the last synced values.

## Write ticket changes back

When someone changes a ticket's status or assignee, send the change with `PATCH /tasks/{task_id}`. The values you read are the values you send. `status` is `open`, `in_progress`, `on_hold`, or `done`. `assigned_to` takes the same ID as a task's `assignee.id`. Map these to your ticket system's statuses and users in your own config. [`GET /account`](/api/account) lists up to 50 members with their `id` and `name`.

Set `expected_status` to the status your sync last saw. If someone changed the task in the dashboard since then, the API returns `409` and changes nothing.

Always send `assigned_to` with a status change. Without it, `in_progress`, `on_hold`, and `done` can assign the task to the integration login, the same as a click in the dashboard.

```javascript theme={null}
async function onTicketChanged(ticket) {
  const res = await api(`/tasks/${ticket.answeringAgentTaskId}`, {
    method: 'PATCH',
    body: JSON.stringify({
      status: ticket.status, // open, in_progress, on_hold, or done
      assigned_to: ticket.assigneeId, // a team member id, or null
      expected_status: ticket.lastSyncedStatus,
    }),
  });

  if (res.status === 409) {
    // Someone changed the task in the dashboard. Copy its current state to the ticket instead.
    const task = await (await api(`/tasks/${ticket.answeringAgentTaskId}`)).json();
    return updateTicketFromTask(ticket, task);
  }
  if (!res.ok) {
    const { code, error } = await res.json();
    throw new Error(`${res.status} ${code}: ${error}`);
  }

  const task = await res.json();
  await saveTicket({ ...ticket, lastSyncedStatus: task.status });
}
```

The task's activity shows each change as made by the integration login.

To copy a ticket comment to the task, send it to [`POST /tasks/{task_id}/notes`](/api/tasks#add-a-note) as `content`. Only your team sees the note. Each request adds a note, so send each comment once. Tasks from forms do not take notes.

Other errors to handle:

* `422` (`unprocessable`) means the dashboard would refuse the same change. The `error` says why. If your team requires a note to mark a task done, add one with `POST /tasks/{task_id}/notes` before you send `"status": "done"`.
* `404` (`not_found`) on a task that belongs to a repeat conversation merged into an earlier one has an `error` that names the task to update instead, such as `This conversation was merged into conversation-184519. Use that task.`

## Reconcile every night

A webhook event can be lost. For example, Answering Agent drops an event after its last retry fails, and it sends nothing while the webhook address is cleared. A nightly job fixes both.

Page through every task that is not done. Create a ticket for any task you do not have. Search results leave out `description`, so read the full task first. Copy dashboard changes to tickets that differ.

```javascript theme={null}
async function reconcile() {
  const seen = new Set();
  for (let page = 1; page; ) {
    const body = await (await api(`/tasks?status=not_done&limit=50&page=${page}`)).json();
    for (const task of body.tasks) {
      if (seen.has(task.id)) continue; // A new task can shift rows between pages.
      seen.add(task.id);
      const ticket = await findTicket(task.id);
      if (!ticket) await createTicket(await (await api(`/tasks/${task.id}`)).json());
      else if (ticket.lastSyncedStatus !== task.status) await updateTicketFromTask(ticket, task);
    }
    page = body.next_page;
  }

  // Tickets still open on your side whose task is no longer in the list were closed in the dashboard.
  for (const ticket of await openTicketsNotIn(seen)) {
    const task = await (await api(`/tasks/${ticket.answeringAgentTaskId}`)).json();
    await updateTicketFromTask(ticket, task);
  }
}
```

`findTicket`, `openTicketsNotIn`, `updateTicketFromTask`, and `saveTicket` stand for your ticket system's own client.

Each page of 50 tasks is one request, and each task you read again is one more. The API allows 120 requests per minute for each person. The helper above waits and retries on `429`.


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