Skip to content

API Reference Knowledge

github-actions[bot] edited this page Oct 12, 2026 · 3 revisions

Knowledge API

What the assistant knows: the website it learned, uploaded files, hand-written knowledge and business details. Part of the API reference: setup, shared types and errors are there.

Knowledge base status

GET /knowledge/status · scope knowledge:read

The latest crawl, page counts, chunks and today's AI usage.

Parameter In Required Description
site query no The site (a key always uses its own).

Request

curl "$HELPPUFF_URL/api/v1/knowledge/status" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/knowledge/status`, {
  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 KnowledgeBaseStatusResponse;

Response 200

{
  "site": "acme",
  "connector": "workers-ai",
  "browserRendering": false,
  "workflow": true,
  "schedule": "weekly",
  "run": {
    "id": "run_1",
    "status": "done",
    "trigger": "dashboard",
    "total": 42,
    "done": 42,
    "failed": 0,
    "chunks": 310,
    "error": null,
    "startedAt": 1760000000000,
    "finishedAt": 1760000090000
  },
  "pages": {
    "indexed": 40,
    "unchanged": 2
  },
  "chunks": 310,
  "lastIndexedAt": 1760000090000,
  "usage": {
    "day": "2025-10-09",
    "neurons": 1240.5,
    "messages": 64,
    "budget": 9000,
    "freeAllocation": 10000,
    "messagesLeft": 400,
    "state": "ok"
  }
}
type KnowledgeBaseStatusResponse = {
  site: string;
  connector: string;
  browserRendering: boolean;
  workflow: boolean;
  schedule: string;
  run: {
    id: string;
    status: string;
    trigger: string;
    total: number;
    done: number;
    failed: number;
    chunks: number;
    error: string | null;
    startedAt: number;
    finishedAt: number;
  };
  pages: Record<string, number>;
  chunks: number;
  lastIndexedAt: number;
  usage: {
    day: string;
    neurons: number;
    messages: number;
    budget: number;
    freeAllocation: number;
    messagesLeft: number;
    state: string;
  };
};

Find the website pages

POST /knowledge/discover · scope knowledge:write

Reads robots.txt and the sitemaps, and suggests which pages to learn.

Request

curl -X POST "$HELPPUFF_URL/api/v1/knowledge/discover" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
type FindWebsitePagesRequest = Record<string, never>;

const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/knowledge/discover`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({} satisfies FindWebsitePagesRequest),
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as FindWebsitePagesResponse;

Response 200

{
  "site": "acme",
  "origin": "https://acme.example",
  "reachable": true,
  "sitemaps": [
    "https://acme.example/sitemap.xml"
  ],
  "warnings": [],
  "urls": [
    {
      "url": "https://acme.example/pricing",
      "category": "pricing",
      "suggested": true,
      "status": "indexed",
      "title": "Pricing",
      "error": null,
      "selected": true
    }
  ]
}
type FindWebsitePagesResponse = {
  site: string;
  origin: string;
  reachable: boolean;
  sitemaps: string[];
  warnings: string[];
  urls: Array<{
    url: string;
    category: string;
    suggested: boolean;
    status: string;
    title: string;
    error: string | null;
    selected: boolean;
  }>;
};

Learn the website

POST /knowledge/crawl · scope knowledge:write

Crawls the given pages (or the selected ones) in the background. Follow progress with GET /knowledge/status.

Body field Required Description
urls no The pages to learn. Without it, the selected pages.
include no Glob patterns of pages to add, e.g. ["/services/**"].
exclude no Glob patterns of pages to leave out.

Request

curl -X POST "$HELPPUFF_URL/api/v1/knowledge/crawl" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "urls": [
    "https://acme.example/pricing",
    "https://acme.example/services"
  ]
}'
type LearnWebsiteRequest = {
  /** The pages to learn. Without it, the selected pages. */
  urls?: string[];
  /** Glob patterns of pages to add, e.g. `["/services/**"]`. */
  include?: string;
  /** Glob patterns of pages to leave out. */
  exclude?: string;
};

const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/knowledge/crawl`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    urls: [
      'https://acme.example/pricing',
      'https://acme.example/services'
    ]
  } satisfies LearnWebsiteRequest),
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as LearnWebsiteResponse;

Response 202

{
  "site": "acme",
  "runId": "run_2",
  "total": 2
}
type LearnWebsiteResponse = {
  site: string;
  runId: string;
  total: number;
};

Try failed pages again

POST /knowledge/crawl/retry · scope knowledge:write

Crawls the pages that failed (or the given ones among them) again, in the background, without changing which pages are selected. Pages that failed for a passing reason are also retried by themselves after a crawl. 409 while a crawl is running.

Body field Required Description
urls no Only these failed pages. Without it, every failed page.

Request

curl -X POST "$HELPPUFF_URL/api/v1/knowledge/crawl/retry" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "urls": [
    "https://acme.example/industries"
  ]
}'
type TryFailedPagesAgainRequest = {
  /** Only these failed pages. Without it, every failed page. */
  urls?: string[];
};

const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/knowledge/crawl/retry`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    urls: [
      'https://acme.example/industries'
    ]
  } satisfies TryFailedPagesAgainRequest),
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as TryFailedPagesAgainResponse;

Response 202

{
  "site": "acme",
  "runId": "run_3",
  "total": 1
}
type TryFailedPagesAgainResponse = {
  site: string;
  runId: string;
  total: number;
};

Cancel a crawl

POST /knowledge/runs/:id/cancel · scope knowledge:write

Parameter In Required Description
id path yes The record's id.

Request

curl -X POST "$HELPPUFF_URL/api/v1/knowledge/runs/ID/cancel" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
const id = '…';
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/knowledge/runs/${id}/cancel`, {
  method: 'POST',
  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 CancelCrawlResponse;

Response 200

{
  "cancelled": true
}
type CancelCrawlResponse = {
  cancelled: boolean;
};

List learned pages

GET /knowledge/pages · scope knowledge:read

Parameter In Required Description
status query no discovered, indexed, unchanged, failed…
site query no The site (a key always uses its own).

Request

curl "$HELPPUFF_URL/api/v1/knowledge/pages" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/knowledge/pages`, {
  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 ListLearnedPagesResponse;

Response 200

{
  "site": "acme",
  "pages": [
    {
      "id": "pg_1",
      "url": "https://acme.example/pricing",
      "finalUrl": "https://acme.example/pricing",
      "title": "Pricing",
      "category": "pricing",
      "status": "indexed",
      "httpStatus": 200,
      "error": null,
      "selected": 1,
      "source": "crawl",
      "crawledAt": 1760000000000,
      "chunks": 6
    }
  ]
}
type ListLearnedPagesResponse = {
  site: string;
  pages: Array<{
    id: string;
    url: string;
    finalUrl: string;
    title: string;
    category: string;
    status: string;
    httpStatus: number;
    error: string | null;
    selected: number;
    source: string;
    crawledAt: number;
    chunks: number;
  }>;
};

Chunks of a page

GET /knowledge/pages/:id/chunks · scope knowledge:read

The passages the assistant searches, as stored.

Parameter In Required Description
id path yes The record's id.
site query no The site (a key always uses its own).

Request

curl "$HELPPUFF_URL/api/v1/knowledge/pages/ID/chunks" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
const id = '…';
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/knowledge/pages/${id}/chunks`, {
  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 ChunksPageResponse;

Response 200

{
  "chunks": [
    {
      "id": "ch_1",
      "headingPath": "Pricing › Drains",
      "ordinal": 0,
      "content": "Blocked drains: $180–$250 including the first hour.",
      "tokens": 14
    }
  ]
}
type ChunksPageResponse = {
  chunks: Array<{
    id: string;
    headingPath: string;
    ordinal: number;
    content: string;
    tokens: number;
  }>;
};

Search the knowledge base

POST /knowledge/search · scope knowledge:read

What the assistant would find for a question (hybrid search, then the reranker). A few neurons.

Body field Required Description
query yes The question (up to 1000 characters).
k no How many passages to return.
rerank no false to skip the reranker.

Request

curl -X POST "$HELPPUFF_URL/api/v1/knowledge/search" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "query": "blocked drain price",
  "k": 3
}'
type SearchKnowledgeBaseRequest = {
  /** The question (up to 1000 characters). */
  query: string;
  /** How many passages to return. */
  k?: number;
  /** `false` to skip the reranker. */
  rerank?: string;
};

const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/knowledge/search`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    query: 'blocked drain price',
    k: 3
  } satisfies SearchKnowledgeBaseRequest),
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as SearchKnowledgeBaseResponse;

Response 200

{
  "site": "acme",
  "query": "blocked drain price",
  "neurons": 2.1,
  "chunks": [
    {
      "id": "ch_1",
      "pageId": "pg_1",
      "url": "https://acme.example/pricing",
      "title": "Pricing",
      "headingPath": "Pricing › Drains",
      "category": "pricing",
      "content": "Blocked drains: $180–$250…",
      "ordinal": 0,
      "score": 0.91
    }
  ],
  "trace": {
    "vector": 8,
    "keyword": 5,
    "fused": 10,
    "reranked": true,
    "threshold": 0.2,
    "errors": [],
    "ms": {
      "embed": 40,
      "vector": 30,
      "keyword": 6,
      "rerank": 120
    }
  }
}
type SearchKnowledgeBaseResponse = {
  site: string;
  query: string;
  neurons: number;
  chunks: Array<{
    id: string;
    pageId: string;
    url: string;
    title: string;
    headingPath: string;
    category: string;
    content: string;
    ordinal: number;
    score: number;
  }>;
  trace: {
    vector: number;
    keyword: number;
    fused: number;
    reranked: boolean;
    threshold: number;
    errors: string[];
    ms: Record<string, number>;
  };
};

Suggest starter questions

POST /knowledge/suggest-questions · scope knowledge:read

Four questions a customer might ask, written from what the site taught the assistant.

Request

curl -X POST "$HELPPUFF_URL/api/v1/knowledge/suggest-questions" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
type SuggestStarterQuestionsRequest = Record<string, never>;

const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/knowledge/suggest-questions`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({} satisfies SuggestStarterQuestionsRequest),
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as SuggestStarterQuestionsResponse;

Response 200

{
  "questions": [
    "How much is a blocked drain?",
    "Do you do emergency callouts?",
    "Which suburbs do you cover?",
    "Can I book online?"
  ],
  "source": "model"
}
type SuggestStarterQuestionsResponse = {
  questions: string[];
  source: string;
};

List hand-written knowledge

GET /knowledge/manual · scope knowledge:read

Parameter In Required Description
site query no The site (a key always uses its own).

Request

curl "$HELPPUFF_URL/api/v1/knowledge/manual" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/knowledge/manual`, {
  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 ListHandWrittenKnowledgeResponse;

Response 200

{
  "entries": [
    {
      "id": "holiday-hours",
      "title": "Holiday hours",
      "content": "Closed 25–26 December.",
      "updatedAt": 1760000000000
    }
  ]
}
type ListHandWrittenKnowledgeResponse = {
  entries: Array<{
    id: string;
    title: string;
    content: string;
    updatedAt: number;
  }>;
};

Add or replace hand-written knowledge

POST /knowledge/manual · scope knowledge:write

Searchable at once. The same id replaces an entry.

Body field Required Description
title yes Up to 200 characters.
content yes Markdown or plain text, up to 100,000 characters.
id no Lowercase letters, digits and -. The same id replaces an entry.

Request

curl -X POST "$HELPPUFF_URL/api/v1/knowledge/manual" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "id": "holiday-hours",
  "title": "Holiday hours",
  "content": "We are closed on 25 and 26 December."
}'
type AddReplaceHandWrittenKnowledgeRequest = {
  /** Up to 200 characters. */
  title: string;
  /** Markdown or plain text, up to 100,000 characters. */
  content: string;
  /** Lowercase letters, digits and `-`. The same id replaces an entry. */
  id?: string;
};

const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/knowledge/manual`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    id: 'holiday-hours',
    title: 'Holiday hours',
    content: 'We are closed on 25 and 26 December.'
  } satisfies AddReplaceHandWrittenKnowledgeRequest),
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as AddReplaceHandWrittenKnowledgeResponse;

Response 200

{
  "id": "holiday-hours",
  "chunks": 1
}
type AddReplaceHandWrittenKnowledgeResponse = {
  id: string;
  chunks: number;
};

Remove hand-written knowledge

DELETE /knowledge/manual/:id · scope knowledge:write

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/knowledge/manual/ID" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
const id = '…';
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/knowledge/manual/${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 RemoveHandWrittenKnowledgeResponse;

Response 200

{
  "deleted": true
}
type RemoveHandWrittenKnowledgeResponse = {
  deleted: boolean;
};

Questions to answer

GET /knowledge/gaps · scope knowledge:read

What visitors asked about the business that the assistant could not answer and the knowledge base does not cover, found after each conversation (off-topic questions, and ones the instructions say not to answer, are left out). Most asked first. Answer one with POST /knowledge/gaps/:id/answer; a new one also sends knowledge.gap to webhooks and, to whoever chose it, an email.

Parameter In Required Description
status query no open (the default), answered, dismissed or all.
limit query no 1–200, default 100.
site query no The site (a key always uses its own).

Request

curl "$HELPPUFF_URL/api/v1/knowledge/gaps" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/knowledge/gaps`, {
  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 QuestionsToAnswerResponse;

Response 200

{
  "gaps": [
    {
      "id": "gap_4f3e2d1c0b9a8f7e6d5c",
      "question": "Do you convert cooktops from natural gas to LPG?",
      "count": 3,
      "firstAskedAt": 1760000000000,
      "lastAskedAt": 1760000000000,
      "conversationId": "1b0f6a52-3c1e-4c55-9f0e-2d1c7a9e5b10",
      "status": "open",
      "answerId": null,
      "answeredBy": null
    }
  ],
  "counts": {
    "open": 4,
    "answered": 9,
    "dismissed": 1
  }
}
type QuestionsToAnswerResponse = {
  gaps: Array<{
    id: string;
    question: string;
    count: number;
    firstAskedAt: number;
    lastAskedAt: number;
    conversationId: string | null;
    status: string;
    answerId: string | null;
    answeredBy: string | null;
  }>;
  counts: Record<string, number>;
};

Answer a question

POST /knowledge/gaps/:id/answer · scope knowledge:write

The answer becomes hand-written knowledge (/knowledge/manual, the question as its title), searchable at once; the question is marked answered. Answering again replaces it.

Parameter In Required Description
id path yes The record's id.
Body field Required Description
answer yes The answer, up to 20,000 characters.
title no The entry's title (default: the question).

Request

curl -X POST "$HELPPUFF_URL/api/v1/knowledge/gaps/ID/answer" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "answer": "No: we only work on natural gas cooktops. For LPG, call a licensed gas fitter."
}'
type AnswerQuestionRequest = {
  /** The answer, up to 20,000 characters. */
  answer: string;
  /** The entry's title (default: the question). */
  title?: string;
};

const id = '…';
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/knowledge/gaps/${id}/answer`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    answer: 'No: we only work on natural gas cooktops. For LPG, call a licensed gas fitter.'
  } satisfies AnswerQuestionRequest),
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as AnswerQuestionResponse;

Response 200

{
  "gap": {
    "id": "gap_4f3e2d1c0b9a8f7e6d5c",
    "question": "Do you convert cooktops from natural gas to LPG?",
    "count": 3,
    "firstAskedAt": 1760000000000,
    "lastAskedAt": 1760000000000,
    "conversationId": "1b0f6a52-3c1e-4c55-9f0e-2d1c7a9e5b10",
    "status": "answered",
    "answerId": "gap-4f3e2d1c0b9a8f7e6d5c",
    "answeredBy": "Olivia"
  },
  "entry": {
    "id": "gap-4f3e2d1c0b9a8f7e6d5c",
    "chunks": 1
  }
}
type AnswerQuestionResponse = {
  gap: {
    id: string;
    question: string;
    count: number;
    firstAskedAt: number;
    lastAskedAt: number;
    conversationId: string;
    status: string;
    answerId: string;
    answeredBy: string;
  };
  entry: {
    id: string;
    chunks: number;
  };
};

Dismiss a question, or open it again

PATCH /knowledge/gaps/:id · scope knowledge:write

status: dismissed (not something to answer; it stays out of the list, asked again or not) or open.

Parameter In Required Description
id path yes The record's id.
Body field Required Description
status yes dismissed or open.

Request

curl -X PATCH "$HELPPUFF_URL/api/v1/knowledge/gaps/ID" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "status": "dismissed"
}'
type DismissQuestionOpenItAgainRequest = {
  /** `dismissed` or `open`. */
  status: string;
};

const id = '…';
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/knowledge/gaps/${id}`, {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    status: 'dismissed'
  } satisfies DismissQuestionOpenItAgainRequest),
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as DismissQuestionOpenItAgainResponse;

Response 200

{
  "id": "gap_4f3e2d1c0b9a8f7e6d5c",
  "question": "Do you convert cooktops from natural gas to LPG?",
  "count": 3,
  "firstAskedAt": 1760000000000,
  "lastAskedAt": 1760000000000,
  "conversationId": "1b0f6a52-3c1e-4c55-9f0e-2d1c7a9e5b10",
  "status": "dismissed",
  "answerId": null,
  "answeredBy": null
}
type DismissQuestionOpenItAgainResponse = {
  id: string;
  question: string;
  count: number;
  firstAskedAt: number;
  lastAskedAt: number;
  conversationId: string | null;
  status: string;
  answerId: string | null;
  answeredBy: string | null;
};

List uploaded files

GET /knowledge/files · scope knowledge:read

Parameter In Required Description
site query no The site (a key always uses its own).

Request

curl "$HELPPUFF_URL/api/v1/knowledge/files" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/knowledge/files`, {
  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 ListUploadedFilesResponse;

Response 200

{
  "files": [
    {
      "id": "0b8f…",
      "name": "price-list.pdf",
      "kind": "pdf",
      "size": 182044,
      "status": "indexed",
      "error": null,
      "chunks": 12,
      "truncated": 0,
      "createdAt": 1760000000000,
      "updatedAt": 1760000000000
    }
  ]
}
type ListUploadedFilesResponse = {
  files: Array<{
    id: string;
    name: string;
    kind: string;
    size: number;
    status: string;
    error: string | null;
    chunks: number;
    truncated: number;
    createdAt: number;
    updatedAt: number;
  }>;
};

Upload a file

POST /knowledge/files · scope knowledge:write

The raw bytes as the body, the file name as ?name=. PDF, Word (.docx), Markdown or text, up to 10 MB. Read and learned in the background.

Parameter In Required Description
name query yes The file name, with its extension.
site query no The site (a key always uses its own).

Request

curl -X POST "$HELPPUFF_URL/api/v1/knowledge/files?name=price-list.pdf" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @price-list.pdf
import { readFile } from 'node:fs/promises';

const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/knowledge/files?${new URLSearchParams({ name: 'price-list.pdf' })}`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
    'Content-Type': 'application/octet-stream',
  },
  body: await readFile('price-list.pdf'),
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as UploadFileResponse;

The body is the file's bytes.

Response 202

{
  "id": "0b8f…",
  "name": "price-list.pdf",
  "kind": "pdf",
  "size": 182044,
  "status": "queued"
}
type UploadFileResponse = {
  id: string;
  name: string;
  kind: string;
  size: number;
  status: string;
};

Remove an uploaded file

DELETE /knowledge/files/:id · scope knowledge:write

And everything learned from it.

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/knowledge/files/ID" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
const id = '…';
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/knowledge/files/${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 RemoveUploadedFileResponse;

Response 200

{
  "deleted": true,
  "chunks": 12
}
type RemoveUploadedFileResponse = {
  deleted: boolean;
  chunks: number;
};

Business details

GET /knowledge/facts · scope knowledge:read

Phone, email, address, hours, service areas: read from the site (crawl) or set by you (owner).

Parameter In Required Description
site query no The site (a key always uses its own).

Request

curl "$HELPPUFF_URL/api/v1/knowledge/facts" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY"
const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/knowledge/facts`, {
  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 BusinessDetailsResponse;

Response 200

{
  "facts": [
    {
      "key": "phone",
      "value": "1300 000 000",
      "source": "owner",
      "sourceUrl": "owner"
    }
  ]
}
type BusinessDetailsResponse = {
  facts: Array<{
    key: string;
    value: string;
    source: string;
    sourceUrl: string;
  }>;
};

Set business details

PUT /knowledge/facts · scope knowledge:write

Yours are never overwritten by a crawl. An empty value removes one. Re-indexed at once.

Body field Required Description
facts yes Keys name, phone, email, address, hours, serviceAreas and others; an empty value removes one.

Request

curl -X PUT "$HELPPUFF_URL/api/v1/knowledge/facts" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "facts": {
    "phone": "1300 000 000",
    "hours": "Mon–Fri 8am–5pm"
  }
}'
type SetBusinessDetailsRequest = {
  /**
   * Keys `name`, `phone`, `email`, `address`, `hours`, `serviceAreas` and
   * others; an empty value removes one.
   */
  facts: Record<string, string>;
};

const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/knowledge/facts`, {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    facts: {
      phone: '1300 000 000',
      hours: 'Mon–Fri 8am–5pm'
    }
  } satisfies SetBusinessDetailsRequest),
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as SetBusinessDetailsResponse;

Response 200

{
  "facts": [
    {
      "key": "phone",
      "value": "1300 000 000"
    },
    {
      "key": "hours",
      "value": "Mon–Fri 8am–5pm"
    }
  ]
}
type SetBusinessDetailsResponse = {
  facts: Array<{
    key: string;
    value: string;
  }>;
};

Read business details from the site

POST /knowledge/facts/detect · scope knowledge:write

Request

curl -X POST "$HELPPUFF_URL/api/v1/knowledge/facts/detect" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
type ReadBusinessDetailsFromSiteRequest = Record<string, never>;

const response = await fetch(`${process.env.HELPPUFF_URL}/api/v1/knowledge/facts/detect`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.HELPPUFF_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({} satisfies ReadBusinessDetailsFromSiteRequest),
});
if (!response.ok) throw new Error(((await response.json()) as ApiError).error.message);
const data = (await response.json()) as ReadBusinessDetailsFromSiteResponse;

Response 200

{
  "found": 4,
  "facts": [
    {
      "key": "phone",
      "value": "1300 000 000",
      "source": "site"
    }
  ]
}
type ReadBusinessDetailsFromSiteResponse = {
  found: number;
  facts: Array<{
    key: string;
    value: string;
    source: string;
  }>;
};

Clone this wiki locally