Skip to content

Examples

Complete end-to-end examples that create a source, upload content, start a run and publish. The source files live in the repo under docs/developer-api/examples/.

End-to-end “push content → run → publish” flows in five environments. Each reads CHATBOT_API (base URL ending in /api) and CHATBOT_KEY (a sk_live_… key with sources:write, items:write, runs:execute, runs:apply, runs:read).

FileStack
sync.shBash + curl + jq
sync.jsNode.js (built-in fetch, Node 18+)
sync.pyPython 3 (requests)
Sync.csC# / .NET (HttpClient)
github-actions.ymlCI workflow that publishes a repo folder

All examples are idempotent — safe to re-run.

curl / bash

sync.sh
#!/usr/bin/env bash
# Push content into a Chatbot knowledge base, then run + publish.
# Requires: curl, jq, uuidgen. Env: CHATBOT_API, CHATBOT_KEY.
set -euo pipefail
: "${CHATBOT_API:?set CHATBOT_API to your base URL, e.g. https://host/api}"
: "${CHATBOT_KEY:?set CHATBOT_KEY to a sk_live_... key}"
auth=(-H "Authorization: Bearer ${CHATBOT_KEY}")
json=(-H "Content-Type: application/json")
echo "1. Create (or reuse) a source"
SID=$(curl -fsS -X POST "${CHATBOT_API}/v1/sources" "${auth[@]}" "${json[@]}" \
-d '{"name":"Help center","graceDays":7}' | jq -r .source.id)
echo " source id: ${SID}"
echo "2. Batch upsert items (atomic)"
curl -fsS -X POST "${CHATBOT_API}/v1/sources/${SID}/items/batch" "${auth[@]}" "${json[@]}" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"items":[
{"externalItemId":"faq-1","title":"Refunds","content":"Refunds are processed within 14 days."},
{"externalItemId":"faq-2","title":"Shipping","content":"We ship worldwide in 3-5 business days."}
]}' | jq .batch
echo "3. Start a run"
RUN=$(curl -fsS -X POST "${CHATBOT_API}/v1/sources/${SID}/runs" "${auth[@]}")
RID=$(echo "$RUN" | jq -r .run.id)
echo " run ${RID} eligible=$(echo "$RUN" | jq -r .applyEligible)"
echo "4. (optional) Inspect the diff"
curl -fsS "${CHATBOT_API}/v1/sources/${SID}/runs/${RID}/diff" "${auth[@]}" | jq .totals
echo "5. Apply (publish) — asynchronous: enqueue, then poll"
curl -fsS -X POST "${CHATBOT_API}/v1/sources/${SID}/runs/${RID}/apply" "${auth[@]}" \
-H "Idempotency-Key: $(uuidgen)" | jq .apply
echo " waiting for the apply job to finish…"
for i in $(seq 1 120); do
APPLY=$(curl -fsS "${CHATBOT_API}/v1/sources/${SID}/runs/${RID}/apply" "${auth[@]}")
ST=$(echo "$APPLY" | jq -r .apply.status)
if [ "$ST" = "succeeded" ] || [ "$ST" = "failed" ]; then break; fi
sleep 1
done
echo " apply $(echo "$APPLY" | jq -r .apply.status) / $(echo "$APPLY" | jq -r .apply.resultStatus)"
if [ "$(echo "$APPLY" | jq -r .apply.status)" != "succeeded" ]; then
echo " apply did not succeed" >&2; exit 1
fi
echo "6. Active generation"
curl -fsS "${CHATBOT_API}/v1/sources/${SID}/generation" "${auth[@]}" | jq .generation
echo "Done."

Node.js

sync.js
#!/usr/bin/env node
// Push content into a Chatbot knowledge base, then run + publish.
// Node 18+ (built-in fetch + crypto.randomUUID). Env: CHATBOT_API, CHATBOT_KEY.
const { randomUUID } = require('node:crypto');
const API = process.env.CHATBOT_API;
const KEY = process.env.CHATBOT_KEY;
if (!API || !KEY) {
console.error('Set CHATBOT_API and CHATBOT_KEY');
process.exit(1);
}
async function call(method, path, body, idempotencyKey) {
const headers = { Authorization: `Bearer ${KEY}` };
if (body !== undefined) headers['Content-Type'] = 'application/json';
if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;
const res = await fetch(`${API}${path}`, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
});
const text = await res.text();
const data = text ? JSON.parse(text) : {};
if (!res.ok) {
throw new Error(`${method} ${path}${res.status} ${data.code || ''} ${data.error || text}`);
}
return data;
}
const main = async () => {
// 1. Create (or reuse) a source.
const { source } = await call('POST', '/v1/sources', { name: 'Help center', graceDays: 7 });
console.log('source', source.id);
// 2. Batch upsert (atomic, idempotent).
const { batch } = await call('POST', `/v1/sources/${source.id}/items/batch`, {
items: [
{ externalItemId: 'faq-1', title: 'Refunds', content: 'Refunds are processed within 14 days.' },
{ externalItemId: 'faq-2', title: 'Shipping', content: 'We ship worldwide in 3-5 business days.' },
],
}, randomUUID());
console.log('batch', batch);
// 3. Run.
const { run, applyEligible } = await call('POST', `/v1/sources/${source.id}/runs`);
console.log('run', run.id, 'eligible', applyEligible);
// 4. Apply (publish) — asynchronous: enqueue (retry-safe via Idempotency-Key),
// then poll the same URL until the job reaches a terminal state.
const { apply } = await call('POST', `/v1/sources/${source.id}/runs/${run.id}/apply`, undefined, randomUUID());
console.log('apply enqueued', apply.status);
let job = apply;
for (let i = 0; i < 120 && job.status !== 'succeeded' && job.status !== 'failed'; i++) {
await new Promise((r) => setTimeout(r, 1000));
({ apply: job } = await call('GET', `/v1/sources/${source.id}/runs/${run.id}/apply`));
}
console.log('apply', job.status, job.resultStatus || '', job.generationId || '');
if (job.status !== 'succeeded') throw new Error(`apply ${job.status}: ${job.error || job.resultStatus}`);
// 5. Confirm the active generation.
const { generation } = await call('GET', `/v1/sources/${source.id}/generation`);
console.log('active generation', generation?.id, generation?.state);
};
main().catch((err) => {
console.error(err.message);
process.exit(1);
});

Python

sync.py
#!/usr/bin/env python3
"""Push content into a Chatbot knowledge base, then run + publish.
Requires: requests (pip install requests). Env: CHATBOT_API, CHATBOT_KEY.
"""
import os
import sys
import time
import uuid
import requests
API = os.environ.get("CHATBOT_API")
KEY = os.environ.get("CHATBOT_KEY")
if not API or not KEY:
sys.exit("Set CHATBOT_API and CHATBOT_KEY")
session = requests.Session()
session.headers["Authorization"] = f"Bearer {KEY}"
def call(method: str, path: str, body=None, idempotency_key: str | None = None):
headers = {}
if body is not None:
headers["Content-Type"] = "application/json"
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
res = session.request(method, f"{API}{path}", json=body, headers=headers, timeout=30)
if not res.ok:
data = res.json() if res.content else {}
raise SystemExit(
f"{method} {path} -> {res.status_code} "
f"{data.get('code', '')} {data.get('error', res.text)}"
)
return res.json() if res.content else {}
def main() -> None:
# 1. Create (or reuse) a source.
source = call("POST", "/v1/sources", {"name": "Help center", "graceDays": 7})["source"]
print("source", source["id"])
# 2. Batch upsert (atomic, idempotent).
batch = call(
"POST",
f"/v1/sources/{source['id']}/items/batch",
{
"items": [
{"externalItemId": "faq-1", "title": "Refunds",
"content": "Refunds are processed within 14 days."},
{"externalItemId": "faq-2", "title": "Shipping",
"content": "We ship worldwide in 3-5 business days."},
]
},
idempotency_key=str(uuid.uuid4()),
)["batch"]
print("batch", batch)
# 3. Run.
run_resp = call("POST", f"/v1/sources/{source['id']}/runs")
run = run_resp["run"]
print("run", run["id"], "eligible", run_resp["applyEligible"])
# 4. Apply (publish) — asynchronous: enqueue (retry-safe via Idempotency-Key),
# then poll the same URL until the job reaches a terminal state.
job = call(
"POST",
f"/v1/sources/{source['id']}/runs/{run['id']}/apply",
idempotency_key=str(uuid.uuid4()),
)["apply"]
print("apply enqueued", job["status"])
for _ in range(120):
if job["status"] in ("succeeded", "failed"):
break
time.sleep(1)
job = call("GET", f"/v1/sources/{source['id']}/runs/{run['id']}/apply")["apply"]
print("apply", job["status"], job.get("resultStatus", ""), job.get("generationId", ""))
if job["status"] != "succeeded":
raise SystemExit(f"apply {job['status']}: {job.get('error') or job.get('resultStatus')}")
# 5. Confirm the active generation.
generation = call("GET", f"/v1/sources/{source['id']}/generation").get("generation")
print("active generation", generation and generation.get("id"), generation and generation.get("state"))
if __name__ == "__main__":
main()

.NET (C#)

Sync.cs
// Push content into a Chatbot knowledge base, then run + publish.
// .NET 6+. Run with: dotnet run (env: CHATBOT_API, CHATBOT_KEY)
//
// Minimal csproj:
// <Project Sdk="Microsoft.NET.Sdk">
// <PropertyGroup>
// <OutputType>Exe</OutputType>
// <TargetFramework>net8.0</TargetFramework>
// <Nullable>enable</Nullable>
// </PropertyGroup>
// </Project>
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
string api = Environment.GetEnvironmentVariable("CHATBOT_API")
?? throw new InvalidOperationException("Set CHATBOT_API");
string key = Environment.GetEnvironmentVariable("CHATBOT_KEY")
?? throw new InvalidOperationException("Set CHATBOT_KEY");
using var http = new HttpClient { BaseAddress = new Uri(api.TrimEnd('/') + "/") };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", key);
async Task<JsonElement> Call(HttpMethod method, string path, object? body = null, string? idempotencyKey = null)
{
using var req = new HttpRequestMessage(method, path);
if (body is not null)
req.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
if (idempotencyKey is not null)
req.Headers.TryAddWithoutValidation("Idempotency-Key", idempotencyKey);
using var res = await http.SendAsync(req);
string text = await res.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(string.IsNullOrEmpty(text) ? "{}" : text);
if (!res.IsSuccessStatusCode)
{
string code = doc.RootElement.TryGetProperty("code", out var c) ? c.GetString() ?? "" : "";
string err = doc.RootElement.TryGetProperty("error", out var e) ? e.GetString() ?? "" : text;
throw new HttpRequestException($"{method} {path} -> {(int)res.StatusCode} {code} {err}");
}
return doc.RootElement.Clone();
}
// 1. Create (or reuse) a source.
var source = (await Call(HttpMethod.Post, "v1/sources",
new { name = "Help center", graceDays = 7 })).GetProperty("source");
string sid = source.GetProperty("id").GetString()!;
Console.WriteLine($"source {sid}");
// 2. Batch upsert (atomic, idempotent).
var batch = (await Call(HttpMethod.Post, $"v1/sources/{sid}/items/batch", new
{
items = new object[]
{
new { externalItemId = "faq-1", title = "Refunds", content = "Refunds are processed within 14 days." },
new { externalItemId = "faq-2", title = "Shipping", content = "We ship worldwide in 3-5 business days." },
}
}, Guid.NewGuid().ToString())).GetProperty("batch");
Console.WriteLine($"batch count={batch.GetProperty("count").GetInt32()}");
// 3. Run.
var runResp = await Call(HttpMethod.Post, $"v1/sources/{sid}/runs");
string rid = runResp.GetProperty("run").GetProperty("id").GetString()!;
Console.WriteLine($"run {rid} eligible={runResp.GetProperty("applyEligible").GetBoolean()}");
// 4. Apply (publish) — asynchronous: enqueue (retry-safe via Idempotency-Key),
// then poll the same URL until the job reaches a terminal state.
var apply = (await Call(HttpMethod.Post, $"v1/sources/{sid}/runs/{rid}/apply",
idempotencyKey: Guid.NewGuid().ToString())).GetProperty("apply");
Console.WriteLine($"apply enqueued {apply.GetProperty("status").GetString()}");
string status = apply.GetProperty("status").GetString()!;
JsonElement job = apply;
for (int i = 0; i < 120 && status != "succeeded" && status != "failed"; i++)
{
await Task.Delay(1000);
job = (await Call(HttpMethod.Get, $"v1/sources/{sid}/runs/{rid}/apply")).GetProperty("apply");
status = job.GetProperty("status").GetString()!;
}
Console.WriteLine($"apply {status}");
if (status != "succeeded") throw new Exception($"apply {status}");
// 5. Confirm the active generation.
var gen = await Call(HttpMethod.Get, $"v1/sources/{sid}/generation");
Console.WriteLine($"active generation {gen.GetProperty("generation").GetProperty("id").GetString()}");

GitHub Actions

github-actions.yml
# Publish a docs folder to the Chatbot knowledge base on every push to main.
#
# Each *.md file under docs/help/ becomes one item, keyed by its path so edits
# upsert and removals tombstone. The snapshot (PUT) makes the run mirror the
# folder exactly; the run + apply publish it.
#
# Repo secrets required:
# CHATBOT_API e.g. https://your-host/api
# CHATBOT_KEY a sk_live_... key with items:write, runs:execute, runs:apply, runs:read
# CHATBOT_SOURCE_ID the developer_api source id to publish into
name: Publish knowledge base
on:
push:
branches: [main]
paths: ["docs/help/**"]
workflow_dispatch:
concurrency:
group: chatbot-kb-publish
cancel-in-progress: false
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build snapshot from docs/help/*.md
run: |
python3 - <<'PY' > snapshot.json
import json, glob, os
items = []
for path in sorted(glob.glob("docs/help/**/*.md", recursive=True)):
with open(path, encoding="utf-8") as fh:
text = fh.read()
title = next((l[2:].strip() for l in text.splitlines() if l.startswith("# ")),
os.path.basename(path))
items.append({
"externalItemId": path,
"title": title,
"content": text,
"sourceUrl": f"{os.environ.get('GITHUB_SERVER_URL','')}/{os.environ.get('GITHUB_REPOSITORY','')}/blob/main/{path}",
})
print(json.dumps({"items": items}))
PY
- name: Snapshot -> run -> apply
env:
CHATBOT_API: ${{ secrets.CHATBOT_API }}
CHATBOT_KEY: ${{ secrets.CHATBOT_KEY }}
SID: ${{ secrets.CHATBOT_SOURCE_ID }}
run: |
set -euo pipefail
auth=(-H "Authorization: Bearer ${CHATBOT_KEY}")
echo "Replace the full staging set"
curl -fsS -X PUT "${CHATBOT_API}/v1/sources/${SID}/items" "${auth[@]}" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: gha-${GITHUB_SHA}-snapshot" \
--data-binary @snapshot.json | jq .snapshot
echo "Start a run"
RID=$(curl -fsS -X POST "${CHATBOT_API}/v1/sources/${SID}/runs" "${auth[@]}" | jq -r .run.id)
echo "Apply (publish) — enqueue, then poll until terminal"
curl -fsS -X POST "${CHATBOT_API}/v1/sources/${SID}/runs/${RID}/apply" "${auth[@]}" \
-H "Idempotency-Key: gha-${GITHUB_SHA}-apply" | jq .apply
for i in $(seq 1 120); do
APPLY=$(curl -fsS "${CHATBOT_API}/v1/sources/${SID}/runs/${RID}/apply" "${auth[@]}")
ST=$(echo "$APPLY" | jq -r .apply.status)
[ "$ST" = "succeeded" ] && break
[ "$ST" = "failed" ] && { echo "$APPLY" | jq .apply; exit 1; }
sleep 1
done
echo "$APPLY" | jq .apply