Overview
The qbrin REST API turns everything your team has connected into cited, grounded answers. Every response either links to the exact source it came from, or tells you it does not know — it is built to cite or abstain rather than guess. All requests are made over HTTPS to a single base URL.
Answers carry citations to the source, or return a plain "not in your records". Designed for zero false accepts.
Answers respect the source permissions of the key's owner.
Get the whole answer as JSON, or stream tokens over Server-Sent Events for a live UI.
Issue read-only, read-write or admin keys per integration, and revoke any of them in one click.
Authentication
Every request authenticates with an API key sent as a Bearer token. Create one in the console under Settings → API keys. The key is shown once at creation and always starts with qbrin_ — store it somewhere safe, because it can never be retrieved again.
Authorization: Bearer qbrin_3f9a…c1d7
Key scopes
Give each integration the least privilege it needs. Scope is enforced per HTTP method, so a read-only key that leaks can never write.
| Scope | Can do |
|---|---|
read | GET requests only — search and read-only lookups. Note: asking a question is a POST, so it needs write. |
write | Everything read can, plus POST / PUT / PATCH / DELETE — asking questions, adding content, feedback. |
admin | Full access. Note: ZIP upload additionally requires the key's owner to be a workspace admin. |
Add content
Two ways to push knowledge in over the API. Both feed the same pipeline — text is chunked, embedded into the vector index and entity-extracted automatically. There is no separate "embed" call; you watch it complete (next section) and then it is answerable.
Send text or markdown directly. Best for programmatic, per-document ingestion.
| Field | Type | Description | |
|---|---|---|---|
content | string | required | The document body (plain text or markdown). Up to ~800,000 characters. |
title | string | optional | A title for the document. Defaults to "Untitled document". |
curl https://app.qbrin.com/api/documents \
-H "Authorization: Bearer qbrin_3f9a…c1d7" \
-H "Content-Type: application/json" \
-d '{ "title": "Refund Policy", "content": "Customers may request a full refund within 30 days…" }'Bulk-ingest a folder of files by uploading a single .zip. qbrin streams the archive to disk (never buffering it in memory), verifies it is a real ZIP by its magic bytes, extracts the text from supported files, and ingests each one. The request returns immediately with a job id; a worker does the heavy lifting.
| Requirement | Detail |
|---|---|
| Content-Type | multipart/form-data, exactly one file field. |
| Format | A genuine .zip archive (checked by header, not extension). |
| Max size | 100 MB compressed. |
| Availability | The key's owner must be a workspace admin, and file upload must be enabled for your workspace. |
curl https://app.qbrin.com/api/uploads/zip \ -H "Authorization: Bearer qbrin_3f9a…c1d7" \ -F "file=@handbook.zip"
jobIdand poll the status endpoint below to follow extraction and embedding.Embedding & status
You never call an embed endpoint directly. Everything you ingest flows through one pipeline — extract text, chunk, embed into the vector index, extract entities — and only becomes answerable once the embedding step finishes. These endpoints let you watch that happen.
Track a ZIP upload
Poll with the jobId from the upload response until phase is "done".
{
"jobId": "upload-7c1e9a4b2f0d",
"phase": "embedding", // queued | extract | embedding | done | error
"state": "done",
"ingested": 42, // files turned into documents
"skipped": 3, // unsupported / empty files
"embed": { "total": 380, "embedded": 294 }, // chunks embedded — progress bar
"files": [ "handbook/onboarding.md", "handbook/pto.pdf" ],
"error": null
}embed.embedded / embed.total is a ready-made progress percentage. When phase flips to "done", every chunk is embedded and the content is live in /api/ask and /api/search.Track a single document
For content added via POST /api/documents, fetch the document and read chunkCounts — it moves from pending to embedded as the vector index fills in.
{
"document": { "id": "doc_a91f22e0", "title": "Refund Policy", "extractStatus": "done" },
"chunkCounts": { "pending": 0, "embedded": 6, "extracted": 6 }
}Use GET /api/documents?limit=50 to list recently ingested documents. Embedding is fully managed — the model, dimensions and hardware are a server-side detail you do not configure per request.
Ask
The core endpoint. Send a question; qbrin retrieves across the connected sources, composes an answer grounded in what it found, verifies it, and returns the answer with its citations. If the answer is not supported, it abstains instead of guessing. Asking logs a query, so it is a POST and needs a write (or admin) key.
Request body
| Field | Type | Description | |
|---|---|---|---|
question | string | required | The question to answer. Up to 4,000 characters. |
k | integer | optional | How many source excerpts to retrieve. 1–20, default 12. |
Example request
curl https://app.qbrin.com/api/ask \
-H "Authorization: Bearer qbrin_3f9a…c1d7" \
-H "Content-Type: application/json" \
-d '{ "question": "What is our refund window?", "k": 8 }'Response 200
{
"answer": "Customers may request a full refund within 30 days of purchase. [1]",
"queryId": "qy_8fb2a1c0",
"citations": [
{
"n": 1,
"documentId": "doc_a91f",
"source": "gdrive",
"title": "Refund Policy.pdf",
"snippet": "…a full refund within 30 days of purchase…",
"authorEmail": "ops@acme.com",
"occurredAt": "2026-03-11T00:00:00Z"
}
],
"trust": {
"decision": "allow",
"level": "verified",
"axes": { "provenance": "cited", "entailment": "passed" },
"certificateId": "a1b2c3d4e5f6"
},
"usage": { "prompt_tokens": 1840, "completion_tokens": 42 },
"timings": { "retrieveMs": 210, "totalMs": 640 }
} citationsarray together with the short "not enough information" sentinel in answer. Check citations.length === 0 to detect an abstain, and treat it as a first-class outcome, not an error.The trust object
When present, an answer also carries a trust certificate so you can decide programmatically whether to show, flag or withhold it. Use it as an additional signal on top of the citation check above.
| Field | Values | Meaning |
|---|---|---|
decision | allow abstain block | Ship it, it did not know, or it was withheld as unsupported. |
level | verified · cited_unverified · abstained · blocked | How strongly the answer was checked. |
axes | provenance, entailment, … | Per-check results; unmeasured checks read not_assessed rather than a fake pass. |
certificateId | string | A stable id for auditing or logging the exact verdict. |
decision === "allow", show your own "no answer found" UI on "abstain", and log "block" for review.Streaming (Server-Sent Events)
Send Accept: text/event-stream to stream the answer token-by-token. Read named events until done.
| Event | Payload |
|---|---|
token | An answer fragment. A reset marks a verified replacement — discard what streamed and take the next stream. |
citations | The sources, so you can render them alongside the text. |
done | Final envelope: queryId, trust (when present), usage, timings. |
followups | 2–3 suggested next questions (may arrive after done). |
error | A failure occurred mid-stream; payload is { message }. Stop consuming and surface or retry — a done will not arrive. |
You may also see informational qwsec and fallback events. Ignore any event name you do not recognise so the stream stays forward-compatible.
curl -N https://app.qbrin.com/api/ask \
-H "Authorization: Bearer qbrin_3f9a…c1d7" \
-H "Accept: text/event-stream" \
-H "Content-Type: application/json" \
-d '{ "question": "Summarize last quarter wins" }'
event: token
data: {"text":"We closed 12 enterprise "}
event: citations
data: {"citations":[…]}
event: done
data: {"queryId":"qy_8fb2","trust":{"decision":"allow"}}Search
Retrieve matching documents, people and knowledge across the connected sources without composing an answer. Useful for a search box or feeding your own pipeline. Permission-aware, like everything else.
| Param | Description | |
|---|---|---|
q | required | The search query. |
sources | optional | Comma-separated source filter, e.g. gdrive,slack. |
types | optional | Comma-separated result-type filter. |
limit | optional | Max results, default 20. |
curl "https://app.qbrin.com/api/search?q=onboarding&sources=gdrive&limit=10" \ -H "Authorization: Bearer qbrin_3f9a…c1d7"
results, people, trending, facets and embedReady. There is also GET /api/search/suggest?q= for lightweight autocomplete.Answer feedback
Tell qbrin whether an answer was useful, using the queryId returned by /api/ask. This tunes future answers.
| Field | Description | |
|---|---|---|
helpful | required | 1 helpful · 0 neutral · -1 not helpful. |
curl https://app.qbrin.com/api/qy_8fb2a1c0/feedback \
-H "Authorization: Bearer qbrin_3f9a…c1d7" \
-H "Content-Type: application/json" \
-d '{ "helpful": 1 }'Manage API keys
You will normally create and revoke keys in the console (Settings → API keys), but the same operations are available programmatically. These run in your signed-in session, not with a Bearer key.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/auth/tokens | List your keys (name, prefix, last used) — never the secret. |
| POST | /api/auth/tokens | Create a key: name, optional scopes and expiresInDays. Returns the raw token once. |
| DELETE | /api/auth/tokens/{id} | Revoke a key immediately. |
// 201 Created — the raw secret is returned exactly once, at token.token
{
"token": {
"id": "tok_a1b2c3",
"name": "ci-pipeline",
"prefix": "qbrin_3f9a",
"scopes": ["read"],
"expiresAt": null,
"createdAt": "2026-07-12T00:00:00Z",
"token": "qbrin_3f9a2b7c8d1e…c1d7"
}
}token.token field, returned only in this create response. Store it immediately — it can never be retrieved again. The list endpoint only ever returns the prefix, never the secret.Errors & limits
Errors return a JSON body { "error": "…" } with a standard HTTP status.
| Status | Meaning & fix |
|---|---|
| 400 | Bad request — e.g. missing question/content, or over the character limit. Fix the payload. |
| 401 | Missing or invalid API key. Check the Authorization header. |
| 403 | The key's scope does not permit this action (e.g. a read key doing a POST, or ZIP upload without admin). Issue a wider-scoped key. |
| 404 | Resource not found — e.g. an unknown queryId, document or job id. |
| 413 | Payload too large — a document over ~800k characters, or a ZIP over 100 MB. |
| 429 | Rate limited. Back off and retry with exponential backoff. |
| 503 | A required model or embedder is not configured server-side. Transient — retry or contact support. |
Rate limits
Requests are rate-limited per key to protect the service — comfortable for normal interactive and batch use. If you expect sustained high volume, reach out at hello@qbrin.com and we will raise your ceiling.