Skip to content

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.

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

Request

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

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

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

json
{
  "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
    }
  ]
}
ts
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 fieldRequiredDescription
urlsnoThe pages to learn. Without it, the selected pages.
includenoGlob patterns of pages to add, e.g. ["/services/**"].
excludenoGlob patterns of pages to leave out.

Request

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

json
{
  "site": "acme",
  "runId": "run_2",
  "total": 2
}
ts
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 fieldRequiredDescription
urlsnoOnly these failed pages. Without it, every failed page.

Request

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

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

Cancel a crawl ​

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

ParameterInRequiredDescription
idpathyesThe record's id.

Request

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

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

List learned pages ​

GET /knowledge/pages · scope knowledge:read

ParameterInRequiredDescription
statusquerynodiscovered, indexed, unchanged, failed…
sitequerynoThe site (a key always uses its own).

Request

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

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

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

Request

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

json
{
  "chunks": [
    {
      "id": "ch_1",
      "headingPath": "Pricing › Drains",
      "ordinal": 0,
      "content": "Blocked drains: $180–$250 including the first hour.",
      "tokens": 14
    }
  ]
}
ts
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 fieldRequiredDescription
queryyesThe question (up to 1000 characters).
knoHow many passages to return.
reranknofalse to skip the reranker.

Request

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

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

bash
curl -X POST "$HELPPUFF_URL/api/v1/knowledge/suggest-questions" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
ts
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

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

List hand-written knowledge ​

GET /knowledge/manual · scope knowledge:read

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

Request

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

json
{
  "entries": [
    {
      "id": "holiday-hours",
      "title": "Holiday hours",
      "content": "Closed 25–26 December.",
      "updatedAt": 1760000000000
    }
  ]
}
ts
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 fieldRequiredDescription
titleyesUp to 200 characters.
contentyesMarkdown or plain text, up to 100,000 characters.
idnoLowercase letters, digits and -. The same id replaces an entry.

Request

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

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

Remove hand-written knowledge ​

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

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

Request

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

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

ParameterInRequiredDescription
statusquerynoopen (the default), answered, dismissed or all.
limitqueryno1–200, default 100.
sitequerynoThe site (a key always uses its own).

Request

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

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

ParameterInRequiredDescription
idpathyesThe record's id.
Body fieldRequiredDescription
answeryesThe answer, up to 20,000 characters.
titlenoThe entry's title (default: the question).

Request

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

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

ParameterInRequiredDescription
idpathyesThe record's id.
Body fieldRequiredDescription
statusyesdismissed or open.

Request

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

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

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

Request

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

json
{
  "files": [
    {
      "id": "0b8f…",
      "name": "price-list.pdf",
      "kind": "pdf",
      "size": 182044,
      "status": "indexed",
      "error": null,
      "chunks": 12,
      "truncated": 0,
      "createdAt": 1760000000000,
      "updatedAt": 1760000000000
    }
  ]
}
ts
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.

ParameterInRequiredDescription
namequeryyesThe file name, with its extension.
sitequerynoThe site (a key always uses its own).

Request

bash
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
ts
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

json
{
  "id": "0b8f…",
  "name": "price-list.pdf",
  "kind": "pdf",
  "size": 182044,
  "status": "queued"
}
ts
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.

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

Request

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

json
{
  "deleted": true,
  "chunks": 12
}
ts
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).

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

Request

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

json
{
  "facts": [
    {
      "key": "phone",
      "value": "1300 000 000",
      "source": "owner",
      "sourceUrl": "owner"
    }
  ]
}
ts
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 fieldRequiredDescription
factsyesKeys name, phone, email, address, hours, serviceAreas and others; an empty value removes one.

Request

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

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

Read business details from the site ​

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

Request

bash
curl -X POST "$HELPPUFF_URL/api/v1/knowledge/facts/detect" \
  -H "Authorization: Bearer $HELPPUFF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
ts
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

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

MIT licensed. Runs in your own Cloudflare account.