Loading the lifecycle guide…
HANK · Developers
Resource lifecycle guide
Manage the full products → manuals → chapters → topics lifecycle over the REST API. New to the APIs? Start with the API quickstart.
HANK · Developers
Manage the full products → manuals → chapters → topics lifecycle over the REST API. New to the APIs? Start with the API quickstart.
Loading the lifecycle guide…
This is the full resource-management lifecycle for an integrator building real tooling over the bearer REST API at https://docs.hank.ai/api/v1: how the four resource levels nest, how to create, list, paginate, and filter them, the metadata-vs-content update seam, the idempotent content-publish path, and deletes. It deliberately does NOT repeat the call mechanics — authentication, the stable error-code catalog, and the 429 / Retry-After backoff loop all live in the API quickstart; the full API reference documents every endpoint and schema, and the agent integration guide covers the discover → ground → cite loop. Every call here carries a Hank-issued bearer token with the hank-docs scope.
The corpus nests four levels deep. Each child references its parent by id, and the parent is VERIFIED before the write: reference a parent that does not exist and the create returns 400 with the stable code unknown_parent — never a raw database error.
product POST https://docs.hank.ai/api/v1/products
└── manual POST https://docs.hank.ai/api/v1/manuals (productId → parent product)
└── chapter POST https://docs.hank.ai/api/v1/chapters (manualId → parent manual)
└── topic POST https://docs.hank.ai/api/v1/topics (manualId + optional chapterId)productId.manualId.manualId (required) and an OPTIONAL chapterId — a null chapterId is the manual’s "Ungrouped" bucket. A supplied chapterId must belong to the SAME manual, or the create is a 400.Every level shares one shape: POST /api/v1/<resource> creates (201) and GET /api/v1/<resource> lists. A create body carries the parent id plus the resource fields; the minimum is small — a product needs only { slug, name }, a manual { productId, slug, title }, a chapter { manualId, slug, title }, a topic { manualId, slug, title }.
curl -X POST https://docs.hank.ai/api/v1/products \
-H "Authorization: Bearer $HANK_DOCS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "slug": "acme-api", "name": "Acme API", "kind": "standalone" }'The 201 body returns the created product with its server-generated id. Feed that id straight in as the productId of a manual (and so on down the tree):
const token = process.env.HANK_DOCS_TOKEN;
const resp = await fetch("https://docs.hank.ai/api/v1/manuals", {
method: "POST",
headers: {
Authorization: "Bearer " + token,
"Content-Type": "application/json",
},
body: JSON.stringify({
productId: "00000000-0000-0000-0000-000000000000", // the id returned by the product create
slug: "guides",
title: "Guides",
}),
});
// 400 with { code: "unknown_parent" } if productId does not resolve.
const manual = await resp.json();List endpoints page with ?limit (default 20, max 100) and ?offset (default 0) and return a uniform envelope. Read hasMore — the deterministic end-of-list signal — and advance offset by limit until it is false; you never need an extra empty request to confirm termination.
{ "items": [], "limit": 20, "offset": 0, "total": 128, "hasMore": true }import os
import requests
token = os.environ["HANK_DOCS_TOKEN"]
def list_all(resource):
items, offset = [], 0
while True:
resp = requests.get(
f"https://docs.hank.ai/api/v1/{resource}",
headers={"Authorization": f"Bearer {token}"},
params={"limit": 100, "offset": offset},
)
resp.raise_for_status()
page = resp.json()
items += page["items"]
if not page["hasMore"]:
return items
offset += page["limit"]The topic list adds two parameterized filters a write client needs to find its OWN in-flight work (public search is published-only, so it otherwise cannot): ?status=draft|pending|published and ?sourceKey=<key>. A bad status is a clean 400, never a silent empty page.
# the review backlog: bot/API writes that are force-pended awaiting human approval
curl "https://docs.hank.ai/api/v1/topics?status=pending&limit=50" \
-H "Authorization: Bearer $HANK_DOCS_TOKEN"
# the exact topic that owns an upstream idempotency key (dedupe before you write)
curl "https://docs.hank.ai/api/v1/topics?sourceKey=kb/rate-limits" \
-H "Authorization: Bearer $HANK_DOCS_TOKEN"curl -X POST "https://docs.hank.ai/api/v1/products" \
-H "Authorization: Bearer $HANK_DOCS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3f9a…-my-stable-uuid" \
-d '{ "slug": "guides", "name": "Guides" }'
# Retrying the SAME key + body replays the original 201 (Idempotency-Replayed: true).The topic update is the one place the lifecycle splits in two. PATCH https://docs.hank.ai/api/v1/topics/{id} is METADATA-ONLY: it accepts only slug, chapterId, position, sourceKey, and versionTag — it re-files, re-orders, and re-keys a row. It REJECTS every content field (title, bodyMdx, excerpt, frontmatter, visibility, status) with a 400: the body schema is strict, so a content field is a loud failure at the boundary, not a silent bypass.
curl -X PATCH https://docs.hank.ai/api/v1/topics/00000000-0000-0000-0000-000000000000 \
-H "Authorization: Bearer $HANK_DOCS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "position": 3, "chapterId": "00000000-0000-0000-0000-000000000000" }'Products and manuals have no rendered MDX body, so their PATCH is an ordinary partial update — only the supplied fields change. A product accepts slug / name / kind / tagline / description / icon / position; a manual accepts slug / title / description / position. The parent FK is NOT re-parentable via PATCH, and admin-only safety/visibility flags (visibility, isPublished, isSensitive, publishPolicy) are absent from the API schema — a body that sets them is a 400.
curl -X PATCH https://docs.hank.ai/api/v1/products/00000000-0000-0000-0000-000000000000 \
-H "Authorization: Bearer $HANK_DOCS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "tagline": "Autonomous medical coding" }'A topic’s content — its title, bodyMdx, and excerpt — only ever reaches a live page through POST https://docs.hank.ai/api/v1/topics/upsert, the pipeline-gated, idempotent publish path. Key it on a stable sourceKey: a re-run with the same sourceKey UPDATES the same topic (the unique key is (manualId, sourceKey)) instead of forking a duplicate. A brand-new sourceKey returns 201 Created; an existing one returns 200 OK. Absent a sourceKey, the upsert keys on (manualId, slug).
curl -X POST https://docs.hank.ai/api/v1/topics/upsert \
-H "Authorization: Bearer $HANK_DOCS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"manualId": "00000000-0000-0000-0000-000000000000",
"slug": "rate-limits",
"title": "Rate limits",
"excerpt": "How request quotas work.",
"bodyMdx": "# Rate limits — how request quotas work.",
"sourceKey": "kb/rate-limits",
"visibility": "public"
}'The pipeline runs a secret/PHI scan, link and alt-text checks, and the policy gate, then FORCE-PENDS sensitive or bot-authored content to pending — it never auto-publishes to the public site. createdByBot is forced true server-side (the body cannot relax it). Read the returned status and self-correct from it rather than re-submitting:
import os
import requests
token = os.environ["HANK_DOCS_TOKEN"]
topic = {
"manualId": "00000000-0000-0000-0000-000000000000",
"slug": "rate-limits",
"title": "Rate limits",
"bodyMdx": "# Rate limits — how request quotas work.",
"sourceKey": "kb/rate-limits",
"visibility": "public",
}
resp = requests.post(
"https://docs.hank.ai/api/v1/topics/upsert",
headers={"Authorization": f"Bearer {token}"},
json=topic,
)
resp.raise_for_status()
result = resp.json()
print(resp.status_code, result["status"]) # 201 (new) or 200 (update), and "pending"{
"id": "00000000-0000-0000-0000-000000000000",
"manualId": "00000000-0000-0000-0000-000000000000",
"slug": "rate-limits",
"title": "Rate limits",
"status": "pending",
"visibility": "public",
"isSensitive": false,
"sourceKey": "kb/rate-limits",
"createdByBot": true
}DELETE /api/v1/<resource>/{id} removes a resource and returns 204 No Content; a missing id is 404. Deletes follow the schema’s foreign keys — deleting a chapter does NOT delete its topics: they fall back to the manual’s "Ungrouped" bucket (the chapterId FK is set null).
curl -X DELETE https://docs.hank.ai/api/v1/topics/00000000-0000-0000-0000-000000000000 \
-H "Authorization: Bearer $HANK_DOCS_TOKEN" -i
# HTTP/1.1 204 No Content429 / Retry-After backoff loop in the API quickstart.