Knowledge Export API
The Knowledge Export API delivers the content curated in DATTA — optimized documents, FAQ, glossary and passages — to chatbots and external systems. You curated the base on the platform; now the portal chatbot, the intranet or the service desk needs that content, and without access to all of DATTA. The API solves the integration: a versioned REST API with service account authentication, incremental synchronization and request limiting.
Governance out of the box: only content approved in Knowledge Curation goes out and, for passages, only content in force. Drafts and superseded content are never exported. Every response carries the contract header X-Knowledge-Export-Version: 1.
Architecture and design decisions: knowledge export. General authentication and error conventions: DATTA API overview. The exact routes are in the API reference.
1. Service accounts
A machine-to-machine credential with per-domain (context) scope, the fixed KNOWLEDGE_EXPORT permission and a secret revealed only once.
1.1 Management operations
| Operation | Required permission | Input |
|---|---|---|
| Create an account | DATA_ACCESS_MANAGE_USERS | {nome, descricao, dominios: ["MEC", ...]} |
| List accounts | DATA_ACCESS_VIEW or DATA_ACCESS_MANAGE_USERS | — |
| Revoke an account | DATA_ACCESS_MANAGE_USERS | {"reason": "..."} (required) |
| Exchange credentials for a token | public (limit of 10/min per clientId+IP) | {clientId, clientSecret} |
| Check the account's status | internal use between platform components | — |
By role defaults, only administrators hold DATA_ACCESS_MANAGE_USERS and KNOWLEDGE_EXPORT.
1.2 Create (reveal-once)
Creation returns 201 with the full credential — the only time the secret is ever shown:
{ "id":"<uuid>", "nome":"bot-mec", "dominios":["MEC"],
"clientId":"<uuid>", "clientSecret":"dsa_<base64url>",
"aviso":"Guarde o clientSecret agora: ele não será exibido novamente." }- The
clientSecret(dsa_+ 32 random bytes) is stored only as a bcrypt hash and never reappears — store it in your secret vault at creation time. - The account is persisted as a
ServiceAccountrecord in thedattasystem database (Neo4j). - Audit:
DATA_ACCESS.SERVICE_ACCOUNT_CREATED. - The account's permission is fixed as
["KNOWLEDGE_EXPORT"](v1); there is no per-account lifetime — the token lifetime is global (datta.auth.service-account-token-ttl, default 900 seconds).
1.3 Obtain a token (client-credentials)
Sending clientId and clientSecret, the account receives a short-lived token:
{"accessToken":"<jwt>","tokenType":"Bearer","expiresIn":900,"scope":["MEC"]}Token claims (HS256, the same key as user tokens): sub = svc:{id}, type = service-account, scope = [domains], permissions = ["KNOWLEDGE_EXPORT"].
Any failure — nonexistent clientId, wrong secret, revoked account — returns the same generic 401 "Credenciais inválidas" ("Invalid credentials"), by design (anti-enumeration, constant-time comparison). Above 10 exchanges per minute for the same clientId+IP, the answer is 429 with Retry-After: 60.
1.4 Revoke
Revocation requires a reason and is always soft (never a physical delete), idempotent, with the DATA_ACCESS.SERVICE_ACCOUNT_REVOKED audit event:
{"id":"<uuid>","ativo":false,"revogadoEm":"..."}Propagation: the account's status is checked with a 60-second cache (key datta:knowledge:export:sa-status:{id}) — still-valid tokens stop working within up to 60 seconds, with 401 "Credencial revogada. Solicite uma nova credencial ao administrador." ("Credential revoked. Request a new credential from the administrator.")
Interface: service account management on the data-access page has not been implemented yet — today it is done entirely through the operations above.
2. Exported collections
Each collection is read per domain. The {dominio} accepts [a-zA-Z0-9_-]{1,64} and is compared to the credential's scope in a normalized way (lowercase, alphanumeric). Authorization happens in layers: the KNOWLEDGE_EXPORT authority → domain scope → revocation check → request limit.
In the examples below, $EXPORT is your environment's export base address (see the API reference) and $TOKEN is the token obtained in section 1.3.
2.1 Manifest — inventory + ETag
curl -H "Authorization: Bearer $TOKEN" "$EXPORT/MEC/manifest"{
"dominio": "MEC",
"snapshotVersion": "9f2c3a1b0d4e5f67",
"geradoEm": "2026-07-09T12:00:00Z",
"colecoes": {
"documentos": {"total": 12, "lastModified": "2026-07-08T18:20:11Z"},
"faq": {"total": 9, "lastModified": "2026-07-08T18:20:14Z"},
"glossario": {"total": 4, "lastModified": "2026-07-07T10:02:00Z"},
"chunks": {"total": 3210, "lastModified": "2026-07-08T18:19:55Z"}
}
}Cheap polling with If-None-Match (the snapshotVersion becomes the ETag):
curl -i -H "Authorization: Bearer $TOKEN" \
-H 'If-None-Match: "9f2c3a1b0d4e5f67"' "$EXPORT/MEC/manifest"
# HTTP/1.1 304 Not Modified (60s cache; no index query on a hit)2.2 Documents (?format=json|md&since=...)
Approved optimized documents. With format=md the response is text/markdown (rendered per section); with format=json (the default) you get an array of:
{"documentCode":"url-3f2a...","titulo":"...","tituloDocumento":"...",
"dominio":"MEC","versao":2,"geradoEm":"...",
"secoes":[{"titulo":"Resumo","conteudo":"...","ordem":1,"chunkIdsOrigem":["..."]}]}2.3 FAQ and glossary (?since=...)
Flattened items — one object per question/answer pair or per term:
{"documentCode":"...","versao":1,"geradoEm":"...","pergunta":"...","resposta":"...","ordem":1,"chunkIdsOrigem":["..."]}
{"termo":"FIES","definicao":"...","siglaDe":"Fundo de Financiamento Estudantil","aliases":[],"origem":"url-...","versao":1,"geradoEm":"...","chunkIdsOrigem":["..."]}2.4 Passages (chunks) — streaming NDJSON
curl -H "Authorization: Bearer $TOKEN" \
"$EXPORT/MEC/chunks?since=2026-07-01T00:00:00Z&includeEmbeddings=false" \
-o chunks.ndjson
# Content-Type: application/x-ndjson — one JSON line per passage- Only passages in force from the context's index, internally paginated by a cursor over
(createdAt, _id)(internal page default of 500) — the collection is never assembled whole in memory: 100k passages stream with constant consumption. includeEmbeddings=trueincludesembedding+embeddingModel(payload ~4–8 KB more per passage; by default the field is not even fetched from the index).- Line:
{"id","documentCode","dominio","texto","secao","ordem","lastModified"[,"embedding","embeddingModel"]}.
2.5 Bundle — zip package
curl -H "Authorization: Bearer $TOKEN" -OJ "$EXPORT/MEC/bundle"
# conhecimento-MEC.zip:
# manifest.json
# documentos/<documentCode>-v<versao>.md
# faq.json
# glossario.jsonThe zip is assembled by streaming, with the four collections fetched in parallel (concurrency 4).
2.6 Incremental synchronization (since)
All collections accept since in ISO-8601 (an instant or with an offset — an invalid value returns 400 with an example). Recommended consumer flow:
- Poll the manifest with
If-None-Match(e.g. every 5 min) —304= nothing to do; snapshotVersionchanged → download only the deltas, passingsince=<last known lastModified>on each collection;- Persist the new
snapshotVersion+lastModifiedper collection.
The since filter compares against the asset's geradoEm (collections) and createdAt (passages) — when a document is regenerated, the new version appears in the delta once it is approved in curation.
Practical example — connecting an external chatbot
- The administrator creates the
bot-mecaccount with scope["MEC"](section 1.2) and handsclientId/clientSecretto the chatbot team, who store them in a vault. - The chatbot exchanges the credentials for a token (section 1.3) and downloads the initial load through the bundle.
- Every 5 minutes it polls the manifest with
If-None-Match. Most of the time that costs one304. - When
snapshotVersionchanges, it downloads only the deltas withsinceand updates its local base. - If the credential leaks, the administrator revokes it with a
reason(section 1.4) — within 60 seconds consumption stops, and the audit trail shows everything that was exported.
3. Request limit, errors and auditing
- Request limit: counted per identity (service account or user) in the platform cache, under the key
datta:knowledge:export:rl:{serviceAccountId|user}, with a 1-minute window and a default of 60 req/min (datta.knowledge.export.rate-limit-per-minute, environment variableKNOWLEDGE_EXPORT_RATE_LIMIT). Exceeded → 429 withRetry-Afterand{"error": ..., "reason": "rate_limit"}. - Structured errors (always
{"error": <pt-BR message>, "reason": <stable code>}):403 fora_do_escopo(domain outside the account's scope — audited),401 credencial_revogada,400for an invalidsince,formator domain. - Auditing (recorded under
datta.audit.knowledge, flowing tootel-logs-*):KNOWLEDGE.EXPORTEDper served collection (author, domain, collection, items, format) andKNOWLEDGE.EXPORT_DENIEDwith the reason. - Metrics:
knowledge_export_requests_total{colecao,outcome=success|error|not_modified}andknowledge_export_items_total{colecao}.
4. Troubleshooting
| Symptom | Cause | Action |
|---|---|---|
401 "Credenciais inválidas" when requesting the token | Wrong clientId/secret or revoked account (deliberately identical response) | Check the account in the service account listing; if revoked, create a new one (reveal-once) |
403 fora_do_escopo | The requested domain is not in the account's dominios (normalized comparison) | Create an account with the correct domain — the scope cannot be edited afterwards |
401 credencial_revogada up to 60s after revoking | Account status cache (60s) | Expected — documented propagation ceiling |
| Empty collections with existing assets | Assets still in draft (they have not gone through curation) | Approve them on the curation screen; the export serves approved content only |
| The export keeps working while authentication is down | The revocation check degrades open (documented design decision) | Restore the authentication service; the window is limited to the outage + the 60s cache |
Constant 429 from one consumer | Polling without If-None-Match/since | Steer the consumer to the incremental flow; if the volume is legitimate, raise KNOWLEDGE_EXPORT_RATE_LIMIT |