Skip to main content

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.

Before anything works: register a model

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.

No browser? Do the whole of this over SSH

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
You can do this without a browser

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
server/oberik.ts
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

ask.ts — a complete script
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.

Try it before you write any code

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