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).
| File | Stack |
|---|---|
sync.sh | Bash + curl + jq |
sync.js | Node.js (built-in fetch, Node 18+) |
sync.py | Python 3 (requests) |
Sync.cs | C# / .NET (HttpClient) |
github-actions.yml | CI workflow that publishes a repo folder |
All examples are idempotent — safe to re-run.
curl / bash
#!/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 1doneecho " 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 1fi
echo "6. Active generation"curl -fsS "${CHATBOT_API}/v1/sources/${SID}/generation" "${auth[@]}" | jq .generation
echo "Done."Node.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
#!/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 osimport sysimport timeimport 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#)
// 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
# 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 intoname: 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