API
Give it a document. Ask it questions. It answers from what it learned.
Authenticate with a bearer token, create a learner, teach it a source, and ask. The chat call is the shape most clients already speak, so an existing client usually works unchanged.
The facts
- Base URL
- https://api.learnerlabs.ai
- Authentication
- Authorization: Bearer YOUR_API_KEY
- Your learner
- learner-1.0:<learner_id>
- The base model
- learner-1.0-base
- Context window
- 8,192 tokens
- Before you are charged
- Send a teaching call with confirm set to false to see the price with nothing trained. Sent without that field, it trains immediately
Point the same call at your learner or at the base model to ask both the same question and compare the two answers. That comparison is the fastest way to see whether teaching did anything.
How a learner answers. A taught learner answers from what actually reached it during teaching. When taught material conflicts — with the model's general knowledge, or with other taught statements — the version it encountered more often tends to win. A confident answer is not by itself evidence that a fact was taught or retained: verify with the comparison above, or with the per-fact rows in the training report.
Two practical notes. Requests need a real User-Agent header. The default Python urllib agent string is refused at our edge. And on a shared machine, keep the key out of the process list: put the header line in a file and pass it with curl -H @auth.txt instead of writing the token into the command.
Your keys
Mint, review and revoke the keys a machine uses to call this API. The secret is shown once, at mint. The server keeps only a hash of it, so there is nothing to show a second time.
Loading…
The path through it
Six calls, in this order, once. After that you are just asking questions.
-
Get a key
POST
/v1/keyswhile signed in as a person. The secret is shown once and never again; only a hash of it is stored. A machine key cannot mint another key, so an agent has to be handed one. -
Create a learner
POST
/v1/learnerswith a name. Keep thelearner_idthat comes back, because almost every later call takes it. -
Open a session, then activate it
POST
/v1/sessionswith the learner id, then POST/v1/sessions/{id}/activatewith no body. Compute scales to zero between uses, so a cold learner's first answer includes the whole start-up wait. That wait is billed. Three cold questions took 497, 576 and 582 seconds and cost $0.68, $0.79 and $0.80 at the $0.0013658 per GPU-second/v1/billingreports. A warm learner answers in seconds to tens of seconds, so its questions cost cents at the same rate. Activating does not skip that wait, but it opens the sitting and gives youcompute.stageto poll, so the wait renders as a visible stage instead of an unexplained stall. (The learner-create response also carries asession_id; that one is account-scoped bookkeeping, so open a learner session here for asking.) -
Add a source: quote first, then confirm
POST
/v1/sourceswith a file, a URL or a repository. Send it with"confirm": falseand it trains nothing. It returns an estimate: how much material it found, roughly what it will cost, and roughly how long. Send the same call with the field dropped, or set to true, and it starts immediately. Addcost_cap_usdto any teaching call and it refuses to start if the estimate exceeds your cap. -
Watch the job
GET
/v1/jobs/{id}for the state, or GET/v1/jobs/{id}/eventsfor a live stream of it. When it finishes, the report tells you how much the learner gained on the material you gave it. -
Ask it something
POST
/v1/chat/completions, shaped like the chat call most clients already speak. When the learner is cold it answers 202 with a pending job instead of an answer. The job's field isid; pollGET /v1/chat/completions/jobs/{id}until the answer arrives. With streaming turned on you get events as the text arrives.Thinking mode works on taught learners, and it is not yet optimized for them. Send
enable_thinking: trueon a learner and the server prepends one fixed sentence to the thinking phase, asking the model to recall what it was taught before it answers; the response carries the trace inchoices[0].message.thinking. On our handbook learner this recovers 11 to 12 of 16 taught answers against 14 of 16 with thinking off, and the trace names the taught value in 13 of 16 cases. On a learner taught counterfactual facts, the trace can carry a taught value into an unrelated question. Sendthink_phase_capto change the default cap of 300 thinking tokens, or your ownthink_primerto replace the sentence. The base model honoursenable_thinkingwith no primer.
Three kinds of input
Choose by what you have. The same API key works for all three.
| You have | Call | What the learner gets |
|---|---|---|
| A document, a web page or a code repository | POST /v1/sources | With "extract_facts": true (the default), the document is learned and the facts found in it are stored as rows you can list with GET /v1/facts and taught with it. With "extract_facts": false, the document is learned as it is and no facts are extracted: use this for style, terminology or procedure. A document is not turned into question-and-answer pairs. |
| Individual facts you write yourself | POST /v1/facts POST /v1/facts/train | Each fact is stored as a row. Nothing is trained until you call /v1/facts/train, which trains the facts not yet in the model. |
| Examples of a task: an input and the output you want | /learning/v1/… | Supervised training on your rows, in sessions that produce checkpoints you can evaluate and query. See Training on your own examples. |
Documents
kind is one of file, url or github_repo. For file, send text inline in content (up to 8 MiB), or upload a file to POST /v1/sources/upload (up to 8 MiB): plain-text formats such as .txt, .md, .csv, .json, .html and source code, PDF with a text layer (up to 2,000 pages) and .docx. A legacy .doc is refused. For url, the address must be public http or https and return text, HTML, JSON, XML or YAML (a PDF link is refused, upload the file instead), up to 16 MiB. For github_repo, give the https address. For a private repository, add your own read token in deploy_token. Every source is quoted first with "confirm": false, which trains nothing and charges nothing.
curl https://api.learnerlabs.ai/v1/sources \ -H "Authorization: Bearer $LEARNER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"learner_id": "YOUR_LEARNER_ID", "kind": "file", "name": "house style", "content": "…your text…", "extract_facts": false, "confirm": false}'
Facts
POST/v1/facts takes learner_id and fact_text, one statement such as "The Zorvanex relay operates at 14.2 kHz." A fact that contradicts one already stored is not written. The answer is 409 with a notice naming the conflict. That check runs on the learner's serving capacity: while it is starting, the call answers 503 pod_cold with retry_after_s, and the fact is not written. Retry after that many seconds. Edit a fact with PATCH/v1/facts/{id}, which starts the training run that brings the model in line. POST/v1/facts/train with the learner_id starts a training job for the facts not yet in the model and answers 409 nothing_pending when there are none. It has no price quote: the charge appears on the job. The facts are in the model only when that job finishes. Poll GET /v1/jobs/{id}. Training facts currently needs at least one document taught to the same learner first: without one the call answers 409 no_stored_source.
Training on your own examples
The Learning API is at https://api.learnerlabs.ai/learning/v1. GET /learning/v1/health and GET /learning/v1/openapi.json (the full schema, with examples) need no key. Every other call takes the same API key as the rest of the API. It is enabled per account: a valid key on an account that is not enabled gets 403 learning_not_enabled, and you can ask for it through the request-access form.
Training data is JSON Lines, one object per line with exactly row_id, prompt and answer, all non-empty strings, row_id unique. The prompt is context and is not trained on. The answer, plus an end-of-sequence token, is what training fits. Nothing is inserted between the two, so put your own separator in the text, and ask later with the same prompt wording. Raw documents are refused here.
{"row_id": "a-001", "prompt": "Q: What is the capital of Ruvelia?\nA:", "answer": " Tarn"}
{"row_id": "a-002", "prompt": "Q: What river runs through Tarn?\nA:", "answer": " The Oskel"}
Each session also takes monitor panels: small fixed sets of rows the service scores during and after training to track retention. They are not a held-out test. For that, upload a separate evaluation set of {"id", "prompt", "expected"} rows. An evaluation is refused with 409 eval_overlap if any of its prompts matches a training or monitor prompt after lower-casing, collapsing whitespace and removing trailing punctuation. Once a session has used a panel, every later panel set for that learner must contain it unchanged.
Limits: up to 24,000 training rows per file, prompts up to 1,024 tokens, answers up to 1,024 tokens including the end token, 2,000,000 prompt-and-answer tokens and 1,200,000 answer tokens per file, 12 panels of up to 256 rows, 4,000 evaluation rows. Format and row counts are checked when you upload. Token limits are checked when a session starts, on the training worker before the model loads. A row over a limit refuses the session with limits_exceeded and nothing is truncated. Learning jobs are not currently deducted from your account credit, so a refused session costs you nothing. The replication repository's validate_learning_inputs.py checks your files locally for free.
# base: https://api.learnerlabs.ai/learning/v1 header: Authorization: Bearer $LEARNER_API_KEY POST /learners {"name": "my learner"} # → learner_id, initial_checkpoint_id POST /learners/{id}/datasets (application/x-ndjson, training rows) # → dataset_id POST /learners/{id}/panels {"schema": "pumod_monitor_v1", "panels": {"a": [{"id", "prompt", "answer"}]}} POST /learners/{id}/eval-datasets (application/x-ndjson, {"id", "prompt", "expected"} rows) POST /learners/{id}/evaluations {"checkpoint_id": initial_checkpoint_id, "dataset_id": …, "idempotency_key": "base-a"} POST /learners/{id}/sessions {"dataset_id": …, "panel_set_id": …, "idempotency_key": "teach-a"} # → job GET /learners/{id}/jobs/{job_id} # poll until not queued or running; the job names the new checkpoint POST /learners/{id}/evaluations {"checkpoint_id": …, "dataset_id": …, "idempotency_key": "after-a"} POST /learners/{id}/checkpoints/{checkpoint_id}/inferences {"prompts": [{"id": "p1", "prompt": "…"}], "idempotency_key": "ask-1"} GET /learners/{id}/jobs/{job_id}/answers # verbatim answers, paged
To teach a second skill, upload its rows, upload a panel set with the first skill's panel unchanged plus a new one, and start another session: it continues from the learner's latest checkpoint. Retrying any call with the same idempotency_key returns the same job and never starts a second one. Jobs are batch work: a training or inference job takes roughly 10 to 30 minutes before its first output, so poll rather than hold a connection open. The replication repository has a standard-library Python client and a step-by-step runbook.
One example, start to finish
Teach a page and ask about it. Two calls to teach (the first prices it, the second starts it) and one to ask.
# 1 · create a learner curl https://api.learnerlabs.ai/v1/learners \ -H "Authorization: Bearer $LEARNER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "product docs"}' # 2 · price the source. confirm:false trains nothing and charges nothing curl https://api.learnerlabs.ai/v1/sources \ -H "Authorization: Bearer $LEARNER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"learner_id": "YOUR_LEARNER_ID", "kind": "url", "confirm": false, "uri": "https://example.com/docs/changelog"}' # → { "confirmed": false, "quote": { "tokens": 21504, "est_usd": 0.31, # "est_seconds": 180 } } send it again with "confirm": true to start # (to teach raw text instead of a URL: "kind": "file", "content": "…your text…") # 3 · ask it, once the job has finished curl https://api.learnerlabs.ai/v1/chat/completions \ -H "Authorization: Bearer $LEARNER_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "learner-1.0:YOUR_LEARNER_ID", "messages": [{"role": "user", "content": "What changed in v4?"}], "max_tokens": 256, "temperature": 0}' # A warm learner answers in seconds to tens of seconds. A cold one hands back # 202 { "id": "…", "object": "chat.completion.pending", "retry_after_s": … } # instead — not an error. Poll with its "id" until choices arrive: curl https://api.learnerlabs.ai/v1/chat/completions/jobs/THE_PENDING_ID \ -H "Authorization: Bearer $LEARNER_API_KEY"
For agents
A hosted tool endpoint so an agent can teach a document, ask a learner and replay any of the published demonstrations without you writing a client. One line to install:
claude mcp add --transport http learner https://mcp.learnerlabs.ai/mcp \
--header "Authorization: Bearer sk-your-key"
The demonstrations page shows the same install and what to say to your agent once it is connected.
To run the ten-skill study through the API, see the API replication guide.
Endpoint reference
Keys
| Method | Path | What it does |
|---|---|---|
| POST | /v1/keys | Mint a key. Signed in as a person only |
| GET | /v1/keys | List your keys by prefix. Never the secret. Signed in as a person only |
| DELETE | /v1/keys/{id} | Revoke one. Signed in as a person only |
Listing, minting and revoking keys all need a signed-in person. A key on its own gets 403 human session required on each of the three. So an agent cannot revoke its own key if that key is compromised. A person has to.
Learners
| Method | Path | What it does |
|---|---|---|
| POST | /v1/learners | Create one |
| GET | /v1/learners | List them |
| GET | /v1/learners/{id} | One learner and what it holds |
| DELETE | /v1/learners/{id} | Delete it and everything it learned |
| GET | /v1/seeds | The prepared starting points a learner can be created from |
A prepared learner is one created from a published starting point instead of from nothing, so you can run the teach-in-sequence demonstration without teaching a document first. Pass a name from GET /v1/seeds as seed on POST /v1/learners. The copy takes a few minutes (4 minutes 9 seconds) and until it finishes the learner answers 409 preparing the requested starting point.
Sessions
| Method | Path | What it does |
|---|---|---|
| POST | /v1/sessions | Open a session for a learner |
| POST | /v1/sessions/{id}/activate | Open the sitting and get a stage to poll. No GPU is held until your first message |
| GET | /v1/sessions/{id} | Watch it come up |
Teaching
| Method | Path | What it does |
|---|---|---|
| POST | /v1/sources | Price a source with confirm false. Start it with the field dropped or true |
| POST | /v1/sources/upload | Upload a file directly |
| GET | /v1/sources | Everything this learner has been taught |
| GET | /v1/jobs/{id} | Job state |
| GET | /v1/jobs/{id}/events | Live stream of the job |
| GET | /v1/jobs/{id}/report | What it gained on the material |
Individual facts
| Method | Path | What it does |
|---|---|---|
| POST | /v1/facts | Write one fact directly, for a statement the extractor declined from a document |
| GET | /v1/facts | Everything this learner holds as individual facts, each with the row_id the edit route takes |
| PATCH | /v1/facts/{id} | Edit one by its row_id. The retrain that republishes it is dispatched for you |
| POST | /v1/facts/train | Train any still-pending facts into the served weights |
Asking
| Method | Path | What it does |
|---|---|---|
| POST | /v1/chat/completions | Ask. The familiar chat shape |
| GET | /v1/chat/completions/jobs/{id} | Collect an answer that came back as a job |
| POST | /v1/chat/stream | Ask, streamed |
Conversations
| Method | Path | What it does |
|---|---|---|
| POST | /conversations | Start a conversation with a learner |
| GET | /conversations | List them |
| POST | /conversations/{id}/messages | Send a message |
| GET | /conversations/{id}/ab | The same question to the learner and to the base model |
| PATCH | /conversations/{id} | Rename it |
| DELETE | /conversations/{id} | Delete it |
Restore points
| Method | Path | What it does |
|---|---|---|
| GET | /v1/checkpoints | Every point you can go back to |
| POST | /v1/checkpoints/{id}/restore | Roll a learner back to one |
| POST | /v1/checkpoints/{id}/fork | Branch this learner from that point. Its next teaching builds on the fork point |
| POST | /v1/checkpoints/{id}/preload | Warm it before you need it |
| GET | /v1/checkpoints/{id}/export | Take it away |
Billing and records
| Method | Path | What it does |
|---|---|---|
| GET | /v1/billing | Balance and usage |
| POST | /v1/billing/checkout | Add credit |
| GET | /v1/audit | Sessions, teaches, fact writes, keys and seeds, in order. Asks are not recorded here |
| GET | /v1/report | One learner's summary. Pass its learner_id |