Skip to content

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

ParameterInRequiredDescription
sitequerynoThe site (a key always uses its own).

Request

bash
curl "$HELPPUFF_URL/api/v1/tools" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
ts
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

json
{
  "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
    }
  }
}
ts
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 fieldRequiredDescription
nameyesLowercase letters, digits and _, starting with a letter: how the prompt and the data refer to it.
kindnohttp (the default) or extract.
descriptionyesWhat the tool does; for extract tools, which visitor details to collect.
methodnoGET, POST, PUT, PATCH or DELETE.
urlnoAn https URL; only its path and query may use {{…}}.
headersnoA list of { name, value, secret }; secret values are stored encrypted and never returned.
bodynoThe request body; in JSON, "{{data}}" as a whole value sends the object itself.
parametersnoDescriptions of the {{args.*}} the request uses: { name, description, required }.
fieldsnoAn extract tool's fields: { name, description, required }.
picknoResponse paths to keep (delivery.date); empty keeps it all (cut to about 4 KB).
timeoutMsno1000 to 10000 (default 5000).
beforenoFor an HTTP tool, run it when the chat starts, with the pre-chat form's answers.
afternoRun it when the conversation ends, with the transcript, summary, lead and data. New extract tools default to this timing.
whennoAn 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

bash
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"
  ]
}'
ts
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

json
{
  "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
}
ts
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.

ParameterInRequiredDescription
idpathyesThe record's id.

Request

bash
curl -X PATCH "$HELPPUFF_URL/api/v1/tools/ID" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "before": true
}'
ts
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

json
{
  "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
}
ts
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.

ParameterInRequiredDescription
idpathyesThe record's id.
sitequerynoThe site (a key always uses its own).

Request

bash
curl -X DELETE "$HELPPUFF_URL/api/v1/tools/ID" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
ts
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

json
{
  "deleted": true
}
ts
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 fieldRequiredDescription
idnoA saved tool.
toolnoA draft, as POST /tools takes it.
samplenoValues for the templates: { args, prechat, data, page, conversation, transcript, summary, lead, attributes }.

Request

bash
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"
    }
  }
}'
ts
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

json
{
  "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"
  ]
}
ts
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[];
};

MIT licensed. Runs in your own Cloudflare account.