Tools API
Your own APIs, called before, during and after a chat ({{name}} in the prompt), and extract tools that save values from the finished conversation. What they return or save is kept on the conversation (data) and sent to webhooks. Part of the API reference: setup, shared types and errors are there.
- List tools:
GET /tools - Add a tool:
POST /tools - Change a tool:
PATCH /tools/:id - Remove a tool:
DELETE /tools/:id - Test a tool:
POST /tools/test
List tools
GET /tools · scope prompt:read
Every tool the site has. Secret header values are never returned: set says whether one is stored. keys are what the tool returns (from its last test), for {{name.key}} in the prompt. prechat is the pre-chat form's fields ({{prechat.<name>}}); assistant whether the site's backend is HelpPuff's assistant, which uses tools in the chat.
| Parameter | In | Required | Description |
|---|---|---|---|
site | query | no | The site (a key always uses its own). |
Request
curl "$HELPPUFF_URL/api/v1/tools" \
-H "Authorization: Bearer $HELPPUFF_API_KEY"const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/tools`, {
headers: {
Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
},
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as ListToolsResponse;Response 200
{
"tools": [
{
"id": "tool_8d7c6b5a",
"name": "order_status",
"kind": "http",
"description": "Look up an order by its number: status, items and delivery date.",
"method": "GET",
"url": "https://api.acme.example/orders/{{args.order_number}}",
"headers": [
{
"name": "Accept",
"value": "application/json",
"secret": false,
"set": true
},
{
"name": "Authorization",
"value": "",
"secret": true,
"set": true
}
],
"body": "",
"parameters": [
{
"name": "order_number",
"description": "The order number, like A-1042.",
"required": true
}
],
"pick": [
"status",
"delivery.date"
],
"timeoutMs": 5000,
"keys": [
"status",
"delivery",
"delivery.date"
],
"before": false,
"after": false,
"when": null,
"enabled": true,
"lastStatus": 200,
"lastError": null,
"lastAt": 1760000000000,
"createdAt": 1760000000000,
"updatedAt": 1760000000000
}
],
"assistant": true,
"prechat": [
{
"name": "email",
"label": "Email"
}
],
"limits": {
"tools": 30,
"timeoutMs": {
"default": 5000,
"min": 1000,
"max": 10000
}
}
}type ListToolsResponse = {
tools: Array<{
id: string;
name: string;
kind: string;
description: string;
method: string;
url: string;
headers: Array<{
name: string;
value: string;
secret: boolean;
set: boolean;
}>;
body: string;
parameters: Array<{
name: string;
description: string;
required: boolean;
}>;
pick: string[];
timeoutMs: number;
keys: string[];
before: boolean;
after: boolean;
when: string | null;
enabled: boolean;
lastStatus: number;
lastError: string | null;
lastAt: number;
createdAt: number;
updatedAt: number;
}>;
assistant: boolean;
prechat: Array<{
name: string;
label: string;
}>;
limits: Record<string, number>;
};Add a tool
POST /tools · scope prompt:write
An http tool calls your API: {{args.x}} in its URL, headers or body is filled in by the assistant, {{prechat.email}} from the pre-chat form, {{data.other_tool.key}} from what another tool returned. Name an HTTP tool in the prompt as {{name}} to let the assistant use it; before runs it when the chat starts, after when the conversation ends. An extract tool reads the finished chat and saves visitor-stated fields as conversation attributes and data. New extract tools run after the chat by default. Up to 30 per site.
| Body field | Required | Description |
|---|---|---|
name | yes | Lowercase letters, digits and _, starting with a letter: how the prompt and the data refer to it. |
kind | no | http (the default) or extract. |
description | yes | What the tool does; for extract tools, which visitor details to collect. |
method | no | GET, POST, PUT, PATCH or DELETE. |
url | no | An https URL; only its path and query may use {{…}}. |
headers | no | A list of { name, value, secret }; secret values are stored encrypted and never returned. |
body | no | The request body; in JSON, "{{data}}" as a whole value sends the object itself. |
parameters | no | Descriptions of the {{args.*}} the request uses: { name, description, required }. |
fields | no | An extract tool's fields: { name, description, required }. |
pick | no | Response paths to keep (delivery.date); empty keeps it all (cut to about 4 KB). |
timeoutMs | no | 1000 to 10000 (default 5000). |
before | no | For an HTTP tool, run it when the chat starts, with the pre-chat form's answers. |
after | no | Run it when the conversation ends, with the transcript, summary, lead and data. New extract tools default to this timing. |
when | no | An after-chat tool runs only when this holds: { "path": "labels.leadQuality", "in": ["hot"] } (a path in the conversation.completed data and the values it may have), or { "path": "lead.email" } (has a value). null removes it. |
Request
curl -X POST "$HELPPUFF_URL/api/v1/tools" \
-H "Authorization: Bearer $HELPPUFF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "order_status",
"description": "Look up an order by its number: status, items and delivery date.",
"method": "GET",
"url": "https://api.acme.example/orders/{{args.order_number}}",
"headers": [
{
"name": "Authorization",
"value": "Bearer sk_live_4f3e2d1c",
"secret": true
}
],
"parameters": [
{
"name": "order_number",
"description": "The order number, like A-1042.",
"required": true
}
],
"pick": [
"status",
"delivery.date"
]
}'type AddToolRequest = {
/**
* Lowercase letters, digits and `_`, starting with a letter: how the prompt
* and the data refer to it.
*/
name: string;
/** `http` (the default) or `extract`. */
kind?: string;
/** What the tool does; for extract tools, which visitor details to collect. */
description: string;
/** `GET`, `POST`, `PUT`, `PATCH` or `DELETE`. */
method?: string;
/** An https URL; only its path and query may use `{{…}}`. */
url?: string;
/**
* A list of `{ name, value, secret }`; secret values are stored encrypted
* and never returned.
*/
headers?: Array<{
name?: string;
value?: string;
secret?: boolean;
}>;
/**
* The request body; in JSON, `"{{data}}"` as a whole value sends the object
* itself.
*/
body?: string;
/**
* Descriptions of the `{{args.*}}` the request uses: `{ name, description,
* required }`.
*/
parameters?: Array<{
name?: string;
description?: string;
required?: boolean;
}>;
/** An extract tool's fields: `{ name, description, required }`. */
fields?: string;
/**
* Response paths to keep (`delivery.date`); empty keeps it all (cut to about
* 4 KB).
*/
pick?: string[];
/** 1000 to 10000 (default 5000). */
timeoutMs?: string;
/**
* For an HTTP tool, run it when the chat starts, with the pre-chat form's
* answers.
*/
before?: string;
/**
* Run it when the conversation ends, with the transcript, summary, lead and
* data. New extract tools default to this timing.
*/
after?: string;
/**
* An after-chat tool runs only when this holds: `{ "path":
* "labels.leadQuality", "in": ["hot"] }` (a path in the
* conversation.completed data and the values it may have), or `{ "path":
* "lead.email" }` (has a value). `null` removes it.
*/
when?: string;
};
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/tools`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'order_status',
description: 'Look up an order by its number: status, items and delivery date.',
method: 'GET',
url: 'https://api.acme.example/orders/{{args.order_number}}',
headers: [
{
name: 'Authorization',
value: 'Bearer sk_live_4f3e2d1c',
secret: true
}
],
parameters: [
{
name: 'order_number',
description: 'The order number, like A-1042.',
required: true
}
],
pick: [
'status',
'delivery.date'
]
} satisfies AddToolRequest),
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as AddToolResponse;Response 201
{
"id": "tool_8d7c6b5a",
"name": "order_status",
"kind": "http",
"description": "Look up an order by its number: status, items and delivery date.",
"method": "GET",
"url": "https://api.acme.example/orders/{{args.order_number}}",
"headers": [
{
"name": "Accept",
"value": "application/json",
"secret": false,
"set": true
},
{
"name": "Authorization",
"value": "",
"secret": true,
"set": true
}
],
"body": "",
"parameters": [
{
"name": "order_number",
"description": "The order number, like A-1042.",
"required": true
}
],
"pick": [
"status",
"delivery.date"
],
"timeoutMs": 5000,
"keys": [
"status",
"delivery",
"delivery.date"
],
"before": false,
"after": false,
"when": null,
"enabled": true,
"lastStatus": 200,
"lastError": null,
"lastAt": 1760000000000,
"createdAt": 1760000000000,
"updatedAt": 1760000000000
}type AddToolResponse = {
id: string;
name: string;
kind: string;
description: string;
method: string;
url: string;
headers: Array<{
name: string;
value: string;
secret: boolean;
set: boolean;
}>;
body: string;
parameters: Array<{
name: string;
description: string;
required: boolean;
}>;
pick: string[];
timeoutMs: number;
keys: string[];
before: boolean;
after: boolean;
when: string | null;
enabled: boolean;
lastStatus: number | null;
lastError: string | null;
lastAt: number | null;
createdAt: number;
updatedAt: number;
};Change a tool
PATCH /tools/:id · scope prompt:write
Any of the fields POST /tools takes. A secret header sent with an empty value keeps the stored one.
| Parameter | In | Required | Description |
|---|---|---|---|
id | path | yes | The record's id. |
Request
curl -X PATCH "$HELPPUFF_URL/api/v1/tools/ID" \
-H "Authorization: Bearer $HELPPUFF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"before": true
}'type ChangeToolRequest = {
before?: boolean;
};
const id = '…';
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/tools/${id}`, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
before: true
} satisfies ChangeToolRequest),
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as ChangeToolResponse;Response 200
{
"id": "tool_8d7c6b5a",
"name": "order_status",
"kind": "http",
"description": "Look up an order by its number: status, items and delivery date.",
"method": "GET",
"url": "https://api.acme.example/orders/{{args.order_number}}",
"headers": [
{
"name": "Accept",
"value": "application/json",
"secret": false,
"set": true
},
{
"name": "Authorization",
"value": "",
"secret": true,
"set": true
}
],
"body": "",
"parameters": [
{
"name": "order_number",
"description": "The order number, like A-1042.",
"required": true
}
],
"pick": [
"status",
"delivery.date"
],
"timeoutMs": 5000,
"keys": [
"status",
"delivery",
"delivery.date"
],
"before": true,
"after": false,
"when": null,
"enabled": true,
"lastStatus": 200,
"lastError": null,
"lastAt": 1760000000000,
"createdAt": 1760000000000,
"updatedAt": 1760000000000
}type ChangeToolResponse = {
id: string;
name: string;
kind: string;
description: string;
method: string;
url: string;
headers: Array<{
name: string;
value: string;
secret: boolean;
set: boolean;
}>;
body: string;
parameters: Array<{
name: string;
description: string;
required: boolean;
}>;
pick: string[];
timeoutMs: number;
keys: string[];
before: boolean;
after: boolean;
when: string | null;
enabled: boolean;
lastStatus: number | null;
lastError: string | null;
lastAt: number | null;
createdAt: number;
updatedAt: number;
};Remove a tool
DELETE /tools/:id · scope prompt:write
Conversations keep what it returned. Take {{name}} out of the prompt too.
| Parameter | In | Required | Description |
|---|---|---|---|
id | path | yes | The record's id. |
site | query | no | The site (a key always uses its own). |
Request
curl -X DELETE "$HELPPUFF_URL/api/v1/tools/ID" \
-H "Authorization: Bearer $HELPPUFF_API_KEY"const id = '…';
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/tools/${id}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
},
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as RemoveToolResponse;Response 200
{
"deleted": true
}type RemoveToolResponse = {
deleted: boolean;
};Test a tool
POST /tools/test · scope prompt:write
Calls it now with sample values and shows what came back: a saved tool (id), a draft (tool), or both (unsaved changes; empty secret headers keep the stored ones). response is the whole answer, value what a chat keeps (after pick), keys its key paths; testing a saved tool remembers them.
| Body field | Required | Description |
|---|---|---|
id | no | A saved tool. |
tool | no | A draft, as POST /tools takes it. |
sample | no | Values for the templates: { args, prechat, data, page, conversation, transcript, summary, lead, attributes }. |
Request
curl -X POST "$HELPPUFF_URL/api/v1/tools/test" \
-H "Authorization: Bearer $HELPPUFF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"id": "tool_8d7c6b5a",
"sample": {
"args": {
"order_number": "A-1042"
}
}
}'type TestToolRequest = {
/** A saved tool. */
id?: string;
/** A draft, as `POST /tools` takes it. */
tool?: string;
/**
* Values for the templates: `{ args, prechat, data, page, conversation,
* transcript, summary, lead, attributes }`.
*/
sample?: {
args?: {
order_number?: string;
};
};
};
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/tools/test`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: 'tool_8d7c6b5a',
sample: {
args: {
order_number: 'A-1042'
}
}
} satisfies TestToolRequest),
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as TestToolResponse;Response 200
{
"ok": true,
"status": 200,
"ms": 182,
"error": null,
"response": {
"status": "shipped",
"delivery": {
"date": "2026-10-12",
"carrier": "AusPost"
}
},
"value": {
"status": "shipped",
"delivery": {
"date": "2026-10-12"
}
},
"keys": [
"status",
"delivery",
"delivery.date",
"delivery.carrier"
]
}type TestToolResponse = {
ok: boolean;
status: number | null;
ms: number;
error: string | null;
response: {
status: string | null;
delivery: {
date: string;
carrier: string;
};
};
value: {
status: string | null;
delivery: {
date: string;
};
};
keys: string[];
};