Quickstart
You'll create a project, mint a token for one of your users, upload a document, and get a cited answer. Everything below is copy-pasteable; nothing is pseudocode.
A new project has no models, so it cannot answer a question and cannot index a document. Add a provider key once, and name two kinds of model with it:
- a chat model — otherwise every request fails with the provider's own error
- an embedding model — otherwise every document upload fails
curl -sX POST "https://oberik.com/api/projects/$OBERIK_PROJECT_ID/providers" \
-H "X-API-Key: $OBERIK_PROJECT_KEY" -H "content-type: application/json" \
-d '{"provider":"openai",
"models":["gpt-4o-mini","text-embedding-3-small"],
"values":{"api_key":"sk-…"}}'
# -> { …, "derived": { "defaultModel": "gpt-4o-mini",
# "embeddingModel": "text-embedding-3-small", "embeddingDim": 1536 } }
derived is the rest of the setup done for you: the chat default, and the embedding model
with its dimension measured rather than assumed. Nothing else to configure.
At any point, ask what is left:
curl -s "https://oberik.com/api/projects/$OBERIK_PROJECT_ID/readiness" -H "X-API-Key: $OBERIK_PROJECT_KEY"
# -> { "canAnswer": true, "ready": true, "next": null, "steps": [ … ] }
Each unfinished step names what it blocks and the one call that fixes it. In the dashboard the same list is at the top of the project's overview.
Two booleans, because there are two questions. Gate a deploy on canAnswer — it is
true once the project can run a turn at all. ready is the full checklist and includes
steps that only hold back one capability: turning on subagents without choosing subagent
models makes ready false on a project that answers questions perfectly well. Each step
carries essential if you want to make the distinction yourself.
1. Create a project and a key
In the dashboard, New project — a project is one isolated workspace, in most integrations one per customer. Then Project → API keys → Create key.
ssh ssh.oberik.com is the same control plane as text — sign up, create the project,
create the key, mint tokens. A coding agent can sign in without ever being handed a
password. See Oberik over SSH.
The key (pk_…) is shown once and is for your server only. Never ship it to a browser:
it can mint tokens with any capability the project allows, and it can administer the
project itself.
export OBERIK_PROJECT_KEY="pk_…" # server-side only
export OBERIK_PROJECT_ID="7f3c…" # a UUID, from the URL of the project page
Everything after this — the capability ceiling, the corpus, skills, MCP servers, the
system prompt — is driven over HTTP with that key (Project API). And
the two steps above are not browser-only either: ssh ssh.oberik.com will signup,
org new, project new and key new, so a coding agent or a CI job can stand a project
up end to end. See Oberik over SSH.
2. Mint a token for one end-user
Your backend decides who the user is and what they may do. Nothing downstream can widen it.
npm install @oberik/sdk
import { createProjectClient } from "@oberik/sdk";
// The server-side client. It holds the project key, so it is a different object from
// the one your app uses — and it refuses to construct in a browser, because a project
// key there can mint anything the project allows and delete the project outright.
export const oberik = createProjectClient({
projectId: process.env.OBERIK_PROJECT_ID!,
projectKey: process.env.OBERIK_PROJECT_KEY!,
});
export async function mintToken(user: { id: string; orgId: string }) {
const { access_token } = await oberik.tokens.mint({
subject: `${user.orgId}:${user.id}`, // who they are
scope: `${user.orgId}:${user.id}`, // what they may see
capabilities: ["chat", "documents:read", "documents:write"],
expiresIn: 3600,
});
return access_token;
}
mint returns what was actually granted, which is worth checking the first time:
const t = await oberik.tokens.mint({ subject: "acme:user_1", capabilities: ["chat", "computer"] });
t.capabilities; // ["chat"] — the project's ceiling trimmed `computer`
A capability you asked for and did not get is a toggle in the dashboard (Project → Capabilities), not a bug in your code.
Without the SDK
It is one HTTP call, so you can check your key before writing any code:
curl -sX POST "https://oberik.com/api/projects/$OBERIK_PROJECT_ID/token" \
-H "X-API-Key: $OBERIK_PROJECT_KEY" \
-H "content-type: application/json" \
-d '{"subject":"acme:user_1","scope":"acme:user_1","capabilities":["chat","documents:read"],"expiresIn":3600}'
{
"access_token": "eyJ…",
"token_type": "bearer",
"expires_in": 3600,
"tenant_id": "…",
"capabilities": ["chat", "documents:read"],
"scope": "acme:user_1"
}
Note the host: minting is a control-plane call (oberik.com), while everything your
app does afterwards goes to the data plane (api.oberik.com).
3. Upload a document, ask a question
import { readFile } from "node:fs/promises";
import { createClient } from "@oberik/sdk";
import { oberik } from "./server/oberik.js";
// `getToken` is the recommended way in: the SDK calls it back when a token expires and
// replays the request, so expiry never reaches your call sites. `forUser` is that
// callback, so the two halves fit together without glue.
const ai = createClient({
getToken: oberik.tokens.forUser({
subject: "acme:user_1",
capabilities: ["chat", "documents:read", "documents:write"],
}),
});
const doc = await ai.documents.uploadAndWait(await readFile("q3-report.pdf"), {
filename: "q3-report.pdf",
tags: ["finance"],
});
console.log(doc.status); // "ready"
const res = await ai.chat.send({
message: "How did Q3 revenue trend, and why?",
tags: ["finance"],
});
console.log(res.content);
for (const c of res.citations) {
// `page` is null for anything without pages — Markdown, CSV, a web page — so guard
// it rather than rendering "p.null" for every non-paginated source.
const where = c.page != null ? ` p.${c.page}` : "";
console.log(`${c.filename}${where} — "${c.quote}"`);
}
Run it with whatever you already use for TypeScript — tsx ask.ts, or Node 22.6+ with
node --experimental-strip-types ask.ts. The SDK ships ESM and CJS builds and needs
Node 18 or newer.
Ingestion is asynchronous. uploadAndWait returns once the document is parsed, chunked
and searchable; use ai.documents.upload(...) for large files, or if you'd rather poll
ai.documents.get(id) yourself.
Every project has a Try agent button. It opens a chat client that talks to your real project through this same API, on a disposable identity erased when you close it — so you can check your corpus, capabilities and models answer the way you expect before wiring anything up.
To see what a particular user sees, mint a token for them under Connect → Mint a test token and press Try agent with this token. The same chat opens as that subject and scope: their documents, their memories, their scope boundary. Nothing is erased when you close it, because none of it is yours.
4. Stream it into a UI
const handle = ai.chat.stream(
{ message: input, tags: ["finance"] },
{
onToken: (_delta, full) => render(full),
onCitations: (cs) => showSources(cs),
},
);
const done = await handle.done;
handle is awaitable and also gives you cancel(), disconnect() and runId(). A
dropped connection re-attaches to the same run and replays what you missed —
details.
5. Keep the conversation
Pass back the session_id and Oberik keeps the history — including images from earlier
turns.
const follow = await ai.chat.send({
session_id: res.session_id,
message: "Which region drove it?",
});
Without the SDK
Every SDK call is one HTTP request. The same turn, over curl:
curl -sX POST https://api.oberik.com/chat \
-H "Authorization: Bearer $TOKEN" \
-H "content-type: application/json" \
-d '{"message":"How did Q3 revenue trend?","tags":["finance"]}'
Streaming is Server-Sent Events on POST /chat/stream — no WebSocket, no proxy. The
full surface is in the API reference, and the data plane serves its
own OpenAPI schema at https://api.oberik.com/openapi.json if you'd rather generate a
client.
Next
- Calling from a browser? Declare your origins first, or the request is refused.
- Want the agent to do things, not just answer? Tools and sandboxed compute.
- Setting up projects from code? Project API.
- Going live? Limits & errors.