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

# Assist API

> Let your own AI agent, running on your server, start the guide in one of your visitors' live sessions — ask, show the way, or ask for an action the visitor approves — and get a typed response.

Your product may already run its own AI agent on your servers: a support bot,
an onboarding assistant, an agent in your email or chat. The **Assist API**
lets that agent hand the visitor over to the guide on the page they have open:
answer in place, point at the control, run a tour, or prepare an action the
visitor approves on the guide's card.

It is the server-side half of [Other AI agents](/agent-tools). The call runs
on the visitor's page through exactly the same path as an in-browser agent
call, so every rule on that page applies unchanged, and the API returns the
same [typed response](/agent-tools#typed-response).

<Note>
  Requires **Growth or higher**, an API key with the `assist:invoke` scope
  (**Settings → API keys**), and **Settings → Guide behavior → Other AI agents**
  turned on for the tool you call. Keep the key on your server; never put it
  in browser code.
</Note>

<h2 id="delivery">
  How a call reaches the visitor
</h2>

1. Your server calls `POST /api/v1/assist` with the tool, the text and the
   person.
2. Puntego queues the call for that person. While the guide is loaded on a
   page they have open **and visible**, the page checks for a waiting call
   every few seconds (about 4 s), only when the owner's agent switches are on,
   the visitor's consent allows the assistant where your consent settings ask
   for it, and the workspace holds a live key with `assist:invoke`.
3. The page picks the call up and runs it as an agent turn: the guide answers,
   points, runs a tour or shows the action card, in front of the person.
4. The page reports what happened, and the API returns it.

If no open page picks the call up before `wait_ms` runs out, the call expires
and returns `type: "unavailable"` with `error.code: "no_live_session"`. Nothing
was shown to anyone.

<h2 id="invoke">
  Start the guide
</h2>

`POST https://worker.puntego.com/api/v1/assist`

```bash theme={null}
curl -X POST https://worker.puntego.com/api/v1/assist \
  -H "Authorization: Bearer pk_live_your_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ticket-4821-step-2" \
  -d '{
    "tool": "show_me",
    "text": "Where do I add a teammate?",
    "customer_id": "user_8412"
  }'
```

<ParamField body="tool" type="string" required>
  `ask` (an answer in words with its sources), `show_me` (a pointer or a visual
  tour; nothing is typed or pressed) or `do_action` (one action the visitor
  approves on the guide's card).
</ParamField>

<ParamField body="text" type="string" required>
  The question, goal or action, in the person's words. 1 to 1,000 characters,
  no control characters. It reaches the guide as the visitor's question,
  marked as coming from an AI agent: it grants nothing and confirms nothing.
</ParamField>

<ParamField body="visitor_id" type="string">
  The anonymous visitor id the embed's `ready` event reports
  (`window.Puntego.on('ready', ({ visitorId }) => …)`). Send exactly one of
  `visitor_id` and `customer_id`.
</ParamField>

<ParamField body="customer_id" type="string">
  Your own user id, as you pass it to the [identity API](/api-reference)
  (`external_id`). Any page where that customer is signed in can pick the call
  up.
</ParamField>

<ParamField body="wait_ms" type="integer" default="15000">
  How long the request waits for a page to pick the call up and report it, from
  6,000 to 25,000 ms.
</ParamField>

<ParamField header="Idempotency-Key" type="string">
  Optional, 1–128 visible ASCII characters. Retrying with the same key returns
  the same call instead of starting a second one; the same key with a different
  tool or person returns `assist/idempotency_conflict` (`409`).
</ParamField>

The response is `201` for a new call (`200` for a replay or a call refused
before anything was queued):

```json theme={null}
{
  "object": "assist_invocation",
  "id": "asi-4f8b2c1e-9a0d-4e6b-8c1f-2d3e4f5a6b7c",
  "tool": "show_me",
  "type": "started",
  "state": "completed",
  "status": "shown",
  "summary": "The person now sees a 3-step tour on the page “Invite a teammate”.",
  "answer": "Open Settings → Team, then press Invite.",
  "sources": [{ "title": "Inviting teammates", "href": "https://example.com/docs/team" }],
  "follow_ups": ["Can I change a teammate's role later?"],
  "shown": { "kind": "tour", "steps": 3, "title": "Invite a teammate" },
  "receipt": null,
  "error": null,
  "created_at": "2026-10-13T10:00:00.000Z",
  "claimed_at": "2026-10-13T10:00:03.120Z",
  "completed_at": "2026-10-13T10:00:07.480Z",
  "expires_at": "2026-10-13T10:00:15.000Z"
}
```

<ResponseField name="type" type="string">
  The typed response: `started`, `answer`, `needs_clarification`,
  `unavailable` or `error`. See [the typed response](/agent-tools#typed-response).
  It is `null` only while a call is still waiting for a page (`state:
      "pending"`), which `POST` never returns.
</ResponseField>

<ResponseField name="state" type="string">
  `pending` (waiting for a page), `running` (a page picked it up and the guide
  started; the outcome is not reported yet), `completed` (the page reported the
  outcome) or `expired` (no page picked it up in time, or the page never
  reported).
</ResponseField>

<ResponseField name="status" type="string">
  The detail, as in the browser: `answered`, `shown`, `not_shown`, `done`,
  `declined`, `shown_instead`, `awaiting_person`, `not_done`, `failed`,
  `unavailable`, `busy`, `rate_limited` or `error`.
</ResponseField>

<ResponseField name="summary" type="string">
  One plain sentence for your agent: what happened, honestly.
</ResponseField>

<ResponseField name="answer" type="string | null">
  What the guide said, with contact, payment and credential identifiers
  redacted. On `needs_clarification` it is the guide's question to the person.
</ResponseField>

<ResponseField name="receipt" type="object | null">
  For `do_action`: `{ action, text, verified }`. `verified` is true only when
  the page or your site confirmed the outcome.
</ResponseField>

<ResponseField name="error" type="object | null">
  `{ code, message }` when the guide could not take the call or the call did
  not finish (see the codes below).
</ResponseField>

Every text field of the result (the answer, follow-ups, summary, source and
tour titles, the receipt and the error message) has contact, payment and
credential identifiers redacted before it is kept. A source link that carries
one (credentials, or an email in its query) is dropped; the source keeps its
title.

Everything in the result is the page's account of what happened, reported by
the visitor's browser. Use [Verified Actions](/verified-actions) and your own
records when you need proof of a change.

When the request's wait ends while the guide is still at work (a `do_action`
waiting for the person, for example), the response is `type: "started"` with
`state: "running"`. Read the outcome later:

```bash theme={null}
curl https://worker.puntego.com/api/v1/assist/asi-4f8b2c1e-9a0d-4e6b-8c1f-2d3e4f5a6b7c \
  -H "Authorization: Bearer pk_live_your_key"
```

A page that never reports (it was closed) reads `state: "expired"`, `type:
"started"` and `error.code: "result_not_reported"` once 11 minutes have passed
since it picked the call up. A visitor who withdraws consent after the page
picked the call up reads the same at once, and nothing the guide said reaches
your server.

<h2 id="governance">
  What stays governed
</h2>

* **Writes wait for the visitor.** Only `do_action` can lead to a write, and
  the visitor approves it on the guide's card, with the line *"An AI agent
  asked for this. Nothing changes unless you approve it here."* Your server can
  never approve anything.
* **Payments, signatures and deletions are always refused** for an agent:
  `not_done` with `error.code: "high_risk_refused"`, and the guide offers the
  person "Show me".
* **The same switches and consent.** Each tool needs its switch in **Settings →
  Guide behavior → Other AI agents**, and a visitor who has not consented where
  your settings ask for it never picks a call up.
* **Metered like visitor questions.** Each call that runs is one AI reply on
  your monthly meter, like any agent turn; a write counts against the plan's
  actions. A call that never runs costs nothing: one no page picked up
  (`no_live_session`), one refused before anything was queued, or one the page
  could not take (`type: "unavailable"`).
* **Audit log.** Each call that runs appears in **Audit log** as *Other AI
  agent*, like an in-browser call.

<h2 id="limits">
  Limits and retention
</h2>

* **60 calls a minute per key** (`assist/key-rate-limited`, `429` with
  `Retry-After`).
* **Per visitor session**, as for every agent call: at most 10 agent calls a
  minute and 120 an hour. A call over that limit ends with `type:
  "unavailable"` and `status: "rate_limited"`.
* **At most 3 calls wait for one person at once** (`too_many_pending`).
* Calls are kept for **seven days**. The request text is kept only until a page
  picks the call up (or it expires); from then on it lives in the conversation,
  under your transcript retention, like any visitor question.
* **Deleting a customer deletes their calls**: those addressed to their
  `customer_id`, and those a page signed in as them picked up, whatever
  `visitor_id` they were addressed to.

<h2 id="errors">
  Errors and unavailable codes
</h2>

| HTTP | `error` | When |
| - | - | - |
| `400` | `assist/invalid_body` | Not a JSON object, or a key other than `tool`, `text`, `visitor_id`, `customer_id`, `wait_ms` |
| `400` | `assist/invalid_tool` | `tool` is not `ask`, `show_me` or `do_action` |
| `400` | `assist/invalid_text` | `text` is empty, longer than 1,000 characters or has control characters |
| `400` | `assist/invalid_target` | Neither or both of `visitor_id` and `customer_id`, or one that is empty or longer than 256 characters |
| `400` | `assist/invalid_wait` | `wait_ms` is not an integer from 6,000 to 25,000 |
| `400` | `assist/invalid_idempotency_key` | The `Idempotency-Key` header is empty, too long or not visible ASCII |
| `401` | `assist/unauthorized` | No key, or a revoked or unknown one |
| `403` | `assist/insufficient_scope` | The key does not hold `assist:invoke` |
| `403` | `feature/requires_plan` | The workspace's plan is below Growth |
| `404` | `assist/not_found` | `GET` for an id this workspace does not have |
| `409` | `assist/idempotency_conflict` | The idempotency key was used for a different tool or person |
| `429` | `assist/key-rate-limited` | Over 60 calls a minute for this key |

A call the guide cannot take returns `200` with `type: "unavailable"` and one
of these `error.code` values:

| `error.code` | Meaning |
| - | - |
| `agent_tools_off` | **Other AI agents** is off for the workspace. Nothing was queued. |
| `tool_off` | That tool's switch is off. Nothing was queued. |
| `person_not_found` | No customer with that `customer_id` has used the guide in this workspace. Nothing was queued. |
| `too_many_pending` | Three calls already wait for this person. |
| `workspace_not_live` | The guide is not live for this workspace yet. |
| `no_live_session` | No open, visible page of this person picked the call up in time. |

Once a page picked a call up, the codes are the browser's own (for example
`tool_unavailable`, `busy`, `rate_limited`, `high_risk_refused`,
`owner_declined`), `page_hidden` when the page went out of view as the call
reached it (the guide did not start), and `result_not_reported` when the page
never reported.

## Next steps

* [Other AI agents](/agent-tools) for the in-browser tools and the typed response.
* [Workspace MCP server](/mcp-server) to read analytics, conversations and the Inbox from Claude or Cursor.
* [Authentication](/authentication) for API keys and scopes.


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