GraphMind APIv1
Get an API key →

Two APIs, one graph

Build turns text you already have — a PDF you OCR'd, a lecture you transcribed, a page you scraped — into a trail: a graph of concepts, each one backed by a verbatim quote from your own material. Teach walks a learner through that trail: you send one message, and get back a tutor's reply streamed token by token, together with the mastery that answer earned and the graph it moved.

Extraction is yours. Structure, evidence-gating, the knowledge graph and the tutor are ours.

Getting started

Every request goes to https://api.graphmind.in and carries an API key. Mint one in the org console — the secret is shown once, at creation.

build a trail, then teach it
BASE=https://api.graphmind.in
KEY=gm_live_…

# 1. build a trail out of your own text
curl -sX POST $BASE/v1/trails \
  -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"title":"Reinforcement Learning — Lecture 4",
       "documents":[{"name":"lecture-4.pdf","segments":[
         {"locator":"page 1","text":"…"},
         {"locator":"page 2","text":"…"}]}]}'
# → {"trailId":"reinforcement-learning-lecture-4-a3f9","status":"building", …}

# 2. wait for it (or use a webhook)
curl -s $BASE/v1/trails/reinforcement-learning-lecture-4-a3f9 -H "Authorization: Bearer $KEY"
# → {"status":"ready", "outline":{…}}

# 3. tutor one of your learners on it — streamed
curl -N -X POST $BASE/v1/learners/alice/chat \
  -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"trailId":"reinforcement-learning-lecture-4-a3f9",
       "message":"I think the value function is just the reward?"}'
A build takes minutes; a tutor turn takes seconds. Poll GET /v1/trails/{id} or set a webhook, and only send learners at a trail once it reports ready.

Authentication

Authorization: Bearer gm_live_…

Keys are minted per organization in the console. We store only a SHA-256 of the secret, so a key cannot be recovered after it is shown — lose it and mint another. Keys are revoked, never deleted, so the audit trail survives.

X-API-Key: gm_live_… is accepted as an alternative for clients that mangle Authorization across a redirect.

A key carries your whole organization's authority: it can build trails, read every learner's progress and delete learners. It belongs on your server, never in a browser, a mobile app or anything else a user can open.

Build a trail

You send text; we send back structure. Every concept we extract must be backed by a quote that is literally present in what you submitted — that is a string containment check, not a prompt instruction — so a trail is made of your material rather than of what a model recalls about the topic.

POST/v1/trails

Starts a build and returns immediately. The build runs asynchronously.

request
{
  "title": "Reinforcement Learning — Lecture 4",
  "guide": "Focus on the maths, not the history",
  "difficulty": 2,
  "depth": 1,
  "category": "ai",
  "externalId": "lecture-4-2026",
  "documents": [
    {
      "name": "lecture-4.pdf",
      "segments": [
        { "locator": "page 1", "text": "…" },
        { "locator": "page 2", "text": "…" }
      ]
    },
    {
      "name": "lecture-4.mp4",
      "segments": [{ "locator": "00:14:32", "text": "…" }]
    }
  ]
}
field
required
notes
title
yes
becomes the trail's subject
documents[]
yes
up to 50 per request
documents[].name
no
shown in citations; defaults to document-N
…segments[].text
yes
the extracted text
…segments[].locator
no
free-form position label — page 7, 00:14:32, slide 3, §2.1
documents[].text
no
shorthand for one unlocated segment; ignored when segments is present
guide
no
steering text for the distiller
difficulty
no
0–4, how demanding the mastery targets are
depth
no
0–3, how many facets each concept is broken into
category
no
your own shelf label, echoed back on reads
externalId
no
your own id — makes the call idempotent
locator is the field that matters most. Every concept carries a verbatim quote, and the locator is how that quote is addressed back to your source. Send a good one and a learner is told which minute of the lecture an idea came from; omit it and citations fall back to the filename.
202 Accepted
{ "trailId": "reinforcement-learning-lecture-4-a3f9",
  "status": "building", "externalId": "lecture-4-2026",
  "chars": 48210, "quotaUsed": 148210, "quota": 5000000 }

Idempotency. Supplying externalId reserves it before any work begins, so a retry after a client timeout returns the original build rather than starting — and billing — a second one. The retry is answered with 200 and "idempotent": true.

POST/v1/trails/estimate

The same body, priced but not built. No model is called, so it is free and instant.

200 OK
{ "chars": 48210, "segments": 42, "extractionCalls": 10,
  "estimatedChunks": 25, "quotaUsed": 100000, "quota": 5000000,
  "quotaRemaining": 4900000, "withinQuota": true }
GET/v1/trails/{id}
200 OK — mid-build
{ "id": "…", "status": "building",
  "phase": "reading", "message": "Reading lecture-4.pdf, pages 1-6…",
  "done": 3, "total": 9 }

Poll this from the moment POST returns — the id is valid immediately, and the trail reports building straight away rather than appearing only once it has finished.

status goes buildingready | error. Typical phases: queuedextractingreadingdistilling embeddingdone. When ready, the response also carries an outline: modules → concepts → prerequisites, each concept with its quote and locator.

GET/v1/trails

Everything your organization owns, including drafts and rejected trails.

DELETE/v1/trails/{id}

Permanent. Learner mastery recorded against the trail's concepts goes with it.

POST/v1/trails/{id}/review
{ "action": "approve" | "reject", "note": "optional" }

Builds are auto-approved today — a trail is published to your organization the moment it is ready. This endpoint records a verdict either way. Rejecting parks the trail back in draft, which every listing excludes: it stops being offered without being destroyed, and approve puts it back.

POSTyour callback URL

Set an HTTPS callback in the console. When a build finishes we POST:

{ "trailId": "…", "externalId": "…", "status": "ready" | "error",
  "subject": "…", "error": "", "at": 1756400000000 }

Best-effort, with a 10-second deadline: a slow endpoint on your side never delays or fails a build. Treat GET /v1/trails/{id} as the source of truth.

Teach a learner

This is the half that runs the model. You send a learner's message; we pick what to teach next from their mastery graph, ground the reply in your own material, stream it back, silently grade what the learner demonstrated, and update the graph. One request per turn.

Learner identity

Your learners never sign in here and have no account with us. You assert a learnerId in the path — whatever you already call them: a UUID, an email, a row id — and we namespace it to your organization. Two organizations both having a learner called alice is fine; the graphs never touch.

Up to 128 characters, no control characters, no leading or trailing whitespace. It is the join key for everything below, so use a stable id — change it and you have created a new learner with no history.

POST/v1/learners/{learnerId}/opener

The tutor's first line of a session, and the thing that makes a learner start talking. It is a separate call from /chat because the prompt genuinely differs: a first-timer is welcomed to the trail, a returning learner is picked back up mid-thread. Costs one turn.

request
{ "trailId": "reinforcement-learning-lecture-4-a3f9", "stream": true }
POST/v1/learners/{learnerId}/chat
request
{
  "trailId": "reinforcement-learning-lecture-4-a3f9",
  "message": "I think the value function is just the reward?",
  "stream": true
}
field
required
notes
trailId
yes
must be a trail your organization owns and that has finished building
message
yes
the learner's message, up to 8,000 characters
stream
no
defaults to true. false returns one JSON object instead
history
no
optional. Omit it and we keep the transcript for you, keyed on (learner, trail). Send it and you own the transcript — we will not write ours, so pick one and stay with it
We do not accept a provider or model field, and never will on this endpoint. Choosing the model, writing the tutor prompt and grading the answer are the product. If you want to run your own model, see bring your own model.

The event stream

With "stream": true the response is text/event-stream. Events are named, and each carries one JSON object. They always arrive in this order:

text/event-stream
event: ready
data: {"trailId":"…","learnerId":"alice","plan":{"focus":"Value function","depth":"explain",
       "facet":"relation to reward","nextUp":["Bellman equation"],"mastered":["Reward"],
       "sources":[{"cite":"lecture-4.pdf, page 7","text":"The value function is the expected…"}]}}

event: delta
data: {"text":"Close, but not quite — "}

event: delta
data: {"text":"the reward is what you get right now…"}

event: reply
data: {"text":"Close, but not quite — the reward is what you get right now… Why might a state
       with a small immediate reward still have a high value?"}

event: mastery
data: {"concepts":[{"name":"Value function","mastery":0.31,"confidence":0.42,"status":"shaky",
       "quality":"partial","difficulty":"explain","misconception":"value equals immediate reward"}],
       "connections":[],"progress":0.18,"coverage":0.22,"nextUp":["Bellman equation"]}

event: done
data: {"ok":true,"ms":2140,"usage":{"turnsUsed":417,"turnQuota":20000,"month":"2026_08"}}
event
required
notes
ready
no
fires before the model does. Carries the turn's plan — the focus concept, the depth being probed, and the passages of your material the tutor is teaching from. Render the citation here, not after
delta
no
one fragment of the reply. Append them in order
reply
no
the complete reply, once. Use it rather than your own concatenation if you only need the final text
mastery
no
what the learner demonstrated, after the graph write. mastery is a probability 0–1, progress and coverage are the trail-wide figures
done
no
terminator, with timing and your quota position
error
no
something failed mid-stream. Followed by done with "ok": false
An error after the stream opens arrives in the body, not the status line. By the time a model call can fail the response is already 200. Handle the error event; do not treat 200 as success. A turn that fails before any token is sent is refunded and does not count against your quota.
Not every hop preserves a stream. Some proxies and serverless gateways buffer text/event-stream and release it all at once. The events and their order are identical either way, so a correct client works regardless — but if your deltas arrive in one burst, look at what sits between you and us before you look at us.
stream: false — the same turn as one object
{
  "trailId": "…", "learnerId": "alice",
  "reply": "Close, but not quite — …",
  "plan": { "focus": "Value function", "depth": "explain", "sources": [ … ] },
  "mastery": { "concepts": [ … ], "progress": 0.18, "coverage": 0.22, "nextUp": [ … ] },
  "usage": { "turnsUsed": 417, "turnQuota": 20000, "month": "2026_08" }
}

What the tutor would do on the next turn, without running it: the focus concept, the depth to probe it at, the facet not yet demonstrated, and the source passages behind it. Free — no model is called. Use it to show a learner what is coming, or to drive your own model (see below).

GET/v1/learners/{learnerId}/progress?trailId=
200 OK
{ "learnerId": "alice", "trailId": "…", "subject": "Reinforcement Learning — Lecture 4",
  "progress": 0.18, "coverage": 0.22, "rank": 3, "ofLearners": 40,
  "nextUp": ["Bellman equation", "Discount factor"],
  "concepts": [
    { "concept": "Value function", "key": "value-function", "mastery": 0.31,
      "target": 0.75, "classAvg": 0.44, "status": "shaky",
      "frontier": true, "atTarget": false }
  ] }

progress is mastery against target across the trail; coverage is how much of it has been touched at all. classAvg is across your organization's learners on that trail, and no one else's.

GET/v1/learners/{learnerId}/graph?trailId=

The trail's concept graph with this learner's state painted onto it: mastery and target per node, which facets are covered, which concept is the frontier, plus prerequisite and similarity edges. One call, because structure and state fetched separately disagree the moment a turn lands between them.

200 OK
{ "subject": "…",
  "nodes": [{ "key": "value-function", "name": "Value function", "module": "Foundations",
              "mastery": 0.31, "target": 0.75, "classAvg": 0.44, "status": "shaky",
              "frontier": true, "atTarget": false, "inCourse": true,
              "facets": [{ "name": "relation to reward", "covered": false }],
              "nextFacet": "relation to reward" }],
  "edges": [{ "source": "reward", "target": "value-function",
              "type": "prereq", "weight": 1, "directed": true }] }
GET/v1/learners/{learnerId}/history?trailId=

The transcript we keep for that learner and trail. Empty if you send your own history on every turn.

GET/v1/learners
200 OK
{ "learners": [
  { "learnerId": "alice", "trails": ["reinforcement-learning-lecture-4-a3f9"],
    "concepts": 12, "lastSeenAt": 1756400000000 }
] }
DELETE/v1/learners/{learnerId}

Erases a learner: their mastery, their enrolments, their transcript, the node itself. Irreversible, and deliberately so — when you are asked to delete a person's data this has to actually delete it. Trail content is untouched.

Usage & limits

GET/v1/usage
200 OK
{ "month": "2026_08",
  "ingest":   { "chars": 148210, "quota": 5000000,  "remaining": 4851790 },
  "tutoring": { "turns": 417,    "quota": 20000,    "remaining": 19583 } }

Two meters, because they measure two costs. Characters buy the one-off build of a trail — one extraction call per 5,000 characters, plus an embedding per 2,000-character chunk. Turns buy every tutored exchange on it, forever after. An organization that imports a small library and teaches it to ten thousand people spends almost nothing on the first and everything on the second.

limit
required
notes
documents / request
no
50
segments / request
no
5,000
characters / request
no
1,000,000 (minimum 400)
message length
no
8,000 characters per tutor turn
requests / minute
no
60 on /v1/trails, 600 on /v1/learners — separate buckets, per key
concurrent builds
no
5 per organization
characters / month
no
5,000,000 by default
turns / month
no
20,000 by default

Every limit is enforced synchronously, so an over-limit request fails at submit with a 4xx you can act on — never as an async job that dies minutes later having already spent your quota. Need more than the defaults? Ask.

You are not billed for work that produced nothing. Characters are charged at submit, because that is the only way the check can be fair between concurrent callers — but a build that fails, for any reason, hands them back. A submission the builder rejects as too thin costs you nothing. The same holds for a tutor turn whose model call fails before any token reaches the learner.

Errors

status
required
notes
400
no
malformed body, or input that cannot make a trail (too thin, no text)
401
no
missing, invalid or revoked key
404
no
no such trail in your organization. We return 404 rather than 403 so trail ids in other organizations cannot be probed
409
no
a learner was sent at a trail that is still building, or whose build failed — check status and reason
429
no
rate limit, concurrent-build limit, or a monthly quota is exhausted
502
no
the tutor model is unavailable. The turn is refunded — retry
503
no
backend starting up, or a provider is down. Retryable
every error has the same shape
{ "error": "monthly character quota exhausted",
  "used": 4990000, "quota": 5000000, "requested": 48210 }

A 429 from the rate limiter also sets Retry-After and repeats it as retryAfter in the body.

Bring your own model

Everything above is mode 1: we run the tutor. There is a second mode for teams that want to keep the model call — and the learner's words — on their own side. In it we supply the steering, the graph and the mastery write, and you supply the intelligence: your model, your prompt, your infrastructure.

The primitive already exists here: GET /v1/learners/{id}/next returns exactly the context a tutoring model needs — which concept to teach, at what depth, which facet has not been demonstrated, which of your own passages to teach from, and what the learner has already mastered and must not be asked again. Feed that to your model today and you have most of mode 2 already.

What is still to come is the other half of the loop: an endpoint that accepts your model's assessment of a turn and applies it to the graph, plus the system prompt, skill file and MCP server that let a general-purpose assistant drive all of this without you writing an integration at all. Tell us if that is the shape you need — it changes what we build first.

GraphMind API reference — build trails and tutor learners over HTTP