API Reference

API Documentation

Every tenant-facing endpoint on the Gyanis AI Platform — chat, streaming, curriculum taxonomy, keys, webhooks, entities, usage and billing. Standard REST over HTTPS; works with any HTTP client in any language.

Getting started

The Gyanis AI Platform is a REST API. You need one thing to start: an API key. Everything below is reachable with Authorization: Bearer gyn_sk_… — there is no SDK to install and no OAuth dance.

Base URL

Production
text
https://api.gyanis.ai/platform/v1

Every path in this reference is relative to that base URL. HTTPS is required; plain HTTP requests are refused at the edge.

Your first call

Verify a new key with GET /me. It works with any active key regardless of scopes, so it isolates “is my key working?” from “is my request body right?”. Then make a real call against chat completion.

Verify your API key
bash
curl https://api.gyanis.ai/platform/v1/me \
  -H "Authorization: Bearer gyn_sk_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345"

Getting access

New accounts start at /register. Submitting the form returns a tracking ID in the form REG-XXXXXXXX; once the application is approved you receive an invite, set a password and collect your first key from the dashboard. Both registration endpoints are public and rate-limited per IP — five submissions a minute, ten status checks a minute.

cURL — Check a registration
bash
curl "https://api.gyanis.ai/platform/v1/register/status?trackingId=REG-4F2A9C71&email=you@example.com"

Integration guide — zero to your first call in five minutes →

Authentication

Pass your API key in the Authorization header. Keys are gyn_sk_live_<32 chars> for production and gyn_sk_test_<32 chars> for development. If a proxy in front of your service strips Authorization, send the raw key in X-API-Key instead — the platform accepts either.

Either header works
http
Authorization: Bearer gyn_sk_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345

X-API-Key: gyn_sk_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345

Key scopes

A key carries a list of scopes. * is the default and grants every endpoint; anything narrower is enforced per request.

ScopeGrants access to
*All endpoints (default when no scopes are given).
chatPOST /chat
chat.streamPOST /chat/stream
keysKey management — create, list and revoke API keys.
usageGET /usage and GET /me/usage
webhooksWebhook management — create, list, delete and read deliveries.
entitiesEntity CRUD and history. The legacy alias students satisfies the same routes.
billingCheckout, purchase history and billing status.
complianceData export and deletion requests.
taxonomyTaxonomy reads — every GET /taxonomy/* endpoint plus POST /taxonomy/topics/resolve.
taxonomy.exportGET /taxonomy/export — the enterprise bulk-export add-on, which also needs account-level activation.

An unknown scope is rejected when the key is created, with 400 invalid_request listing the valid values. A key that reaches an endpoint it does not cover gets 403 endpoint_not_allowed.

Legacy alias: students is still accepted and is equivalent to entities on both the /entities and /students routes.

Security note: the /me/* self-service routes now enforce the same scopes as their top-level counterparts — keys, usage and webhooks. The earlier behaviour, where any live key could mint a new key through /me/keys, is fixed. Integrations built against it must add the scope.

Test mode

A gyn_sk_test_ key skips credit deduction and budget enforcement entirely. Everything else still runs: moderation screens the request, the request is logged, and rate limits apply. A test key is capped at 50 requests per day by default and returns 429 test_mode_limit beyond it. Taxonomy responses on a test key carry "test_mode": true in the billing object.

API keys

POST/keysscope: keys

Mint a new API key. The plaintext key is returned exactly once.

FieldTypeRequiredDescription
namestringYesDisplay name, 1–200 characters.
modestringNo"live" (default) or "test".
scopesstring[]NoScopes to grant. Defaults to ["*"]. Unknown values are rejected.
Create an API key
bash
curl -X POST https://api.gyanis.ai/platform/v1/keys \
  -H "Authorization: Bearer gyn_sk_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Mobile app — production",
    "mode": "live",
    "scopes": ["chat", "chat.stream", "taxonomy"]
  }'
Response — 201 Created
json
{
  "key": "gyn_sk_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345",
  "keyId": "6654a1b2c3d4e5f6a1b2c3d4",
  "prefix": "gyn_sk_live_aBcD",
  "name": "Mobile app — production",
  "mode": "live",
  "scopes": ["chat", "chat.stream", "taxonomy"],
  "message": "Store this key securely. It will not be shown again."
}

The full key is returned once. Nothing can retrieve it again — not the dashboard, not GET /keys, not support. Write it to your secret store in the same code path that creates it, and if you lose it, revoke and mint a new one.

EndpointScopePurpose
GET/keyskeysList keys, masked: keyId, name, prefix, mode, scopes, status, lastUsedAt, createdAt.
DELETE/keys/:keyIdkeysRevoke a key immediately. Returns { "revoked": true }.

Errors: invalid_request · endpoint_not_allowed · missing_api_key

Models

Two customer-facing model names map to two tiers. Pass one in the model field of a chat request; omit it and your tenant default applies.

ModelInput / MOutput / MCached / M
mobiusstandard$4.00$12.00$2.00
oloidpremium$6.00$22.00$3.00

mobius is tuned for cost and speed — tutoring, Q&A, content generation. oloid is the higher-capability tier for competitive-exam prep and multi-step reasoning. An unrecognised name returns 400 invalid_model; a tier your account is not entitled to returns 403 model_not_allowed. Routing is admin-configurable with automatic failover.

Model tiers, benchmarks and the routing story →

Chat completion

POST/chatscope: chat120 req/min

Send a conversation and receive one complete assistant message.

FieldTypeRequiredDescription
messagesarrayYesMessage objects, 1–100 of them.
messages[].rolestringYes"user", "assistant" or "system".
messages[].contentstringYesMessage text, 1–16,000 characters.
modelstringNo"mobius" (default) or "oloid".
max_tokensintegerNoResponse cap, 1–16,384. Default 4,096.
entity_idstringNoYour external entity ID. Enables conversation continuity, max 256 chars.
entity_typestringNoType used when auto-creating an unknown entity_id. Default "student".
subjectstringNoSubject context, e.g. "math". Max 100 chars.
gradestringNoGrade level, e.g. "8". Max 20 chars.
metadataobjectNoCustom key–value pairs, echoed back untouched.
json_modebooleanNoAsk for structured JSON output.
student_idstringNoDeprecated alias of entity_id. Use entity_id in new code.
Send a chat request
bash
curl -X POST https://api.gyanis.ai/platform/v1/chat \
  -H "Authorization: Bearer gyn_sk_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345" \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      { "role": "system", "content": "You are a helpful math tutor for 8th graders." },
      { "role": "user", "content": "Explain the Pythagorean theorem with an example." }
    ],
    "model": "mobius",
    "max_tokens": 1024,
    "entity_id": "student_abc123",
    "subject": "math",
    "grade": "8"
  }'
Response — 200 OK
json
{
  "id": "req_a1b2c3d4e5f6g7h8i9j0k1l2",
  "object": "chat.completion",
  "created": 1709834400,
  "model": "mobius",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "The Pythagorean theorem states that in a right triangle, the square of the hypotenuse equals the sum of the squares of the other two sides: a^2 + b^2 = c^2..."
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 68,
    "completion_tokens": 127,
    "total_tokens": 195
  }
}

Errors: invalid_request · message_too_long · too_many_messages · content_policy_violation · invalid_model · model_not_allowed · insufficient_credits · budget_exceeded · rpm_exceeded · model_unavailable · timeout

Token rates for both tiers and how credits are drawn →

Streaming chat (SSE)

POST/chat/streamscope: chat.stream60 req/min

Stream the same completion as Server-Sent Events, token by token.

The request body is identical to chat completion. The response is text/event-stream; token usage arrives in the final chunk alongside finish_reason.

Stream a completion
bash
curl -N -X POST https://api.gyanis.ai/platform/v1/chat/stream \
  -H "Authorization: Bearer gyn_sk_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345" \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [{ "role": "user", "content": "What is gravity?" }],
    "model": "mobius",
    "entity_id": "student_abc123"
  }'
SSE response stream
text
data: {"id":"req_a1b2c3d4e5f6","object":"chat.completion.chunk","created":1709834400,"model":"mobius","choices":[{"index":0,"delta":{"content":"Gravity"},"finish_reason":null}]}

data: {"id":"req_a1b2c3d4e5f6","object":"chat.completion.chunk","created":1709834400,"model":"mobius","choices":[{"index":0,"delta":{"content":" is"},"finish_reason":null}]}

data: {"id":"req_a1b2c3d4e5f6","object":"chat.completion.chunk","created":1709834400,"model":"mobius","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":38,"completion_tokens":195,"total_tokens":233}}

data: [DONE]
LineWhat to do with it
data: {chunk}Append delta.content to your output buffer.
data: [DONE]Stream complete — close the connection.
: pingHeartbeat comment every 15 seconds — ignore it.

Errors: endpoint_not_allowed · rpm_exceeded · model_unavailable · timeout

Taxonomy API

GET/taxonomy/*scope: taxonomy

Structured curriculum data for Indian and international boards — the full Board > Grade > Subject > Unit > Topic hierarchy plus exam tracks with per-topic weightage. All 13 endpoints are read-only: 7 basic and 4 enriched GETs, a bulk-export add-on, and one POST — /taxonomy/topics/resolve — which is a batch lookup, not a write.

EndpointRate
GET/taxonomy/boards?country=INList education boards for a country, filtered by ISO country code.Basic
GET/taxonomy/boards/:boardId/gradesList the grades a board offers, named the way that board names them.Basic
GET/taxonomy/grades/:gradeId/subjectsList the subjects taught at a grade.Basic
GET/taxonomy/subjects/:subjectId/unitsList the units (chapters) inside a subject.Basic
GET/taxonomy/units/:unitId/topicsList the topics inside a unit — the stable IDs you store on your content.Basic
GET/taxonomy/topics/:topicIdFetch one topic: name, slug, tags, difficulty, tier scope and its place in the hierarchy.Basic
GET/taxonomy/exam-tracks?country=INList exam tracks for a country — JEE/NEET-style competitive exam formats.Basic
GET/taxonomy/topics/:topicId/contextLLM-ready topic block: canonical path, learning objectives, prerequisites, keywords, difficulty, sample prompts and tags.Enriched
GET/taxonomy/search?q=&types=&country=&boardId=&limit=Search the curriculum graph and return matching boards, subjects, units and topics with their canonical paths.Enriched
GET/taxonomy/exam-tracks/:trackId?withTopics=trueExam-track detail: sections, marks, negative marking, sittings, and per-topic weightage mapped to curriculum topics.Enriched
GET/taxonomy/changes?since=&types=&limit=&cursor=Cursor-paginated change deltas including delete tombstones, over a rolling 180-day window.Enriched
POST/taxonomy/topics/resolveResolve up to 50 topic IDs in one call. Body: {"topicIds":[...]}. Billed 100 credits per topic ID.Batch
GET/taxonomy/export?boardId=Full nested tree for one board. Requires the taxonomy.export scope plus an account-level add-on.Add-on

Basic = 100 credits per request unit · Enriched = 400 credits per request unit · Batch = 100 credits per topic ID, up to 50 IDs per call · Add-on = bulk export, requires the taxonomy.export scope plus account-level activation.

List boards
bash
curl "https://api.gyanis.ai/platform/v1/taxonomy/boards?country=IN" \
  -H "Authorization: Bearer gyn_sk_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345"
Response — 200 OK (inside the free allowance)
json
{
  "object": "taxonomy.boards",
  "country": "IN",
  "boards": [
    { "id": "652f1a9c4b7d3e0012ab0155", "code": "CBSE", "name": "CBSE", "country": "IN", "offered_in": ["IN"], "aliases": ["Central Board of Secondary Education"], "order": 1, "updated_at": "2026-07-28T09:14:03.201Z" },
    { "id": "652f1a9c4b7d3e0012ab0156", "code": "ICSE", "name": "CISCE (ICSE)", "country": "IN", "offered_in": ["IN"], "aliases": ["ICSE"], "order": 2, "updated_at": "2026-07-28T09:14:03.201Z" },
    { "id": "652f1a9c4b7d3e0012ab0157", "code": "MH_SB", "name": "Maharashtra State Board", "country": "IN", "offered_in": ["IN"], "aliases": [], "order": 3, "updated_at": "2026-07-28T09:14:03.201Z" }
  ],
  "billing": {
    "request_units": 1,
    "free_units_applied": 1,
    "credits_charged": 0,
    "estimated_usd": 0,
    "test_mode": false
  }
}

Once the monthly free allowance is spent the same call returns "request_units": 1, "free_units_applied": 0, "credits_charged": 100, "estimated_usd": 0.001.

GET/taxonomy/topics/:topicId/contextscope: taxonomyEnriched

One LLM-ready topic block — drop it straight into a system prompt.

Fetch topic context
bash
curl "https://api.gyanis.ai/platform/v1/taxonomy/topics/652f1a9c4b7d3e0012ab34cd/context" \
  -H "Authorization: Bearer gyn_sk_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345"
Response — 200 OK (free allowance already spent)
json
{
  "object": "taxonomy.topic_context",
  "topic_id": "652f1a9c4b7d3e0012ab34cd",
  "context": {
    "canonical_path": "CBSE > Class 10 > Physics > Optics > Refraction of Light",
    "country": "IN",
    "board_code": "CBSE",
    "board_name": "Central Board of Secondary Education",
    "grade_code": "10",
    "grade_label": "Class 10",
    "subject_code": "PHY",
    "subject_name": "Physics",
    "unit_name": "Optics",
    "topic_name": "Refraction of Light",
    "learning_objectives": [
      "State the laws of refraction of light",
      "Apply Snell's law to solve numerical problems",
      "Explain refraction through a rectangular glass slab"
    ],
    "prerequisites": ["Reflection of Light", "Light: basic properties"],
    "prerequisite_topic_ids": ["652f1a9c4b7d3e0012ab34a1", "652f1a9c4b7d3e0012ab34b2"],
    "prerequisite_topic_names": ["Reflection of Light", "Light: basic properties"],
    "keywords": ["refractive index", "Snell's law", "optical density", "lateral displacement"],
    "difficulty": 3,
    "sample_prompts": [
      "Explain why a pencil appears bent when partially immersed in water.",
      "A ray of light passes from air into glass at 45 degrees. Find the angle of refraction if n = 1.5."
    ],
    "tags": ["optics", "numericals", "board-exam"]
  },
  "billing": {
    "request_units": 1,
    "free_units_applied": 0,
    "credits_charged": 400,
    "estimated_usd": 0.004,
    "test_mode": false
  }
}

slug and tier_scope are not on this payload — they come back from GET /taxonomy/topics/:topicId.

POST/taxonomy/topics/resolvescope: taxonomyBatch

Resolve up to 50 topic IDs in one round trip, billed per ID.

Body: { "topicIds": ["…"] } — 1 to 50 24-character topic IDs. Unknown IDs come back in missing rather than failing the call, and are still billed. IDs are de-duplicated before billing, so request_units is the count of distinct IDs you sent.

Batch-resolve topic IDs
bash
curl -X POST https://api.gyanis.ai/platform/v1/taxonomy/topics/resolve \
  -H "Authorization: Bearer gyn_sk_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345" \
  -H "Content-Type: application/json" \
  -d '{
    "topicIds": [
      "652f1a9c4b7d3e0012ab34cd",
      "652f1a9c4b7d3e0012ab34a1",
      "652f1a9c4b7d3e0012ab34b2"
    ]
  }'
Response — 200 OK (3 IDs resolved, first context shown)
json
{
  "object": "taxonomy.topic_contexts",
  "contexts": [
    {
      "topic_id": "652f1a9c4b7d3e0012ab34cd",
      "topic_name": "Refraction of Light",
      "unit_id": "652f1a9c4b7d3e0012ab2f88",
      "unit_name": "Optics",
      "subject_id": "652f1a9c4b7d3e0012ab1e77",
      "subject_name": "Physics",
      "grade_id": "652f1a9c4b7d3e0012ab0d66",
      "grade_label": "Class 10",
      "board_id": "652f1a9c4b7d3e0012ab0155",
      "board_name": "Central Board of Secondary Education",
      "board_code": "CBSE",
      "country": "IN",
      "canonical_path": "CBSE > Class 10 > Physics > Optics > Refraction of Light",
      "learning_objectives": ["State the laws of refraction of light"],
      "keywords": ["refractive index", "Snell's law"],
      "difficulty": 3
    }
  ],
  "missing": [],
  "billing": {
    "request_units": 3,
    "free_units_applied": 0,
    "credits_charged": 300,
    "estimated_usd": 0.003,
    "test_mode": false
  }
}

Billing. A request unit is one billable item: one API call, or one topic ID inside a batch resolve. Every single-call endpoint is exactly one request unit, basic and enriched alike. A basic unit costs 100 credits ($0.001) and an enriched unit 400 credits ($0.004); POST /taxonomy/topics/resolve bills 100 credits per topic ID, up to 50 IDs per call. The first 10,000 request units each calendar month are free per tenant, applied before any credits are drawn.

Staying in sync. Poll GET /taxonomy/changes?since=<ISO> for cursor-paginated deltas including delete tombstones, over a rolling 180-day window. Better, subscribe to the taxonomy.updated webhook — it carries changed_since, so you only call the changes endpoint when something has actually moved.

Errors: not_found · invalid_request · endpoint_not_allowed · product_not_enabled · insufficient_credits

What the taxonomy covers — boards, exam tracks and worked use cases →

Entities

Entities are the people and groups your product serves. Five types, linked into a hierarchy by parentId. Creating them is optional — a chat request with an unknown entity_id auto-provisions one — but creating them explicitly is what lets you attach grade, board and subjects once instead of on every request.

TypeDescription
studentIndividual learners. The only type that accumulates conversation history.
teacherEducators associated with a class or institution.
institutionSchools, colleges or training organisations.
classA group of students inside an institution.
curriculumAn educational framework a cohort follows.
POST/entitiesscope: entities

Create one entity. externalId is unique per tenant and type.

FieldTypeRequiredDescription
typestringYesOne of the five types above.
externalIdstringYesYour own identifier, 1–256 chars. This is what you pass as entity_id in chat.
namestringYesDisplay name, 1–256 chars.
emailstringNoValid email address.
parentIdstringNoThe platform id of the parent entity — the 24-character id returned by create, not its externalId.
curriculumBoardstringNoe.g. "CBSE", "ICSE". Max 100 chars.
countrystringNoISO 3166-1 alpha-2. Max 10 chars.
gradeLevelstringNoe.g. "8", "K-12". Max 20 chars.
subjectsstring[]NoSubject areas, e.g. ["math", "science"].
metadataobjectNoCustom key–value pairs.
Create an entity
bash
curl -X POST https://api.gyanis.ai/platform/v1/entities \
  -H "Authorization: Bearer gyn_sk_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "student",
    "externalId": "student-abc-123",
    "name": "Jane Doe",
    "curriculumBoard": "CBSE",
    "country": "IN",
    "gradeLevel": "10",
    "subjects": ["physics", "math"],
    "metadata": { "section": "10-A" }
  }'
Response — 201 Created
json
{
  "id": "665a1b2c3d4e5f6a7b8c9d0e",
  "type": "student",
  "external_id": "student-abc-123",
  "name": "Jane Doe",
  "email": null,
  "parent_id": null,
  "curriculum_board": "CBSE",
  "country": "IN",
  "grade_level": "10",
  "subjects": ["physics", "math"],
  "metadata": { "section": "10-A" },
  "message_count": 0,
  "last_activity": "2026-08-13T10:00:00.000Z",
  "created_at": "2026-08-13T10:00:00.000Z",
  "updated_at": "2026-08-13T10:00:00.000Z"
}

Two identifiers, one rule. Responses carry both id (the platform's 24-character identifier) and external_id (yours). The :id path segment accepts either. But parentId — on create, update and the list filter — is the platform id, not an externalId. Store the returned id when you plan to nest entities.

GET/entities/:id/historyscope: entities

Read the stored conversation for one entity, newest window first.

ParamTypeRequiredDescription
limitintegerNoMessages to return, 1–200. Default 50.
offsetintegerNoSkip the first N messages. Default 0.
Read entity history
bash
curl "https://api.gyanis.ai/platform/v1/entities/student-abc-123/history?limit=20" \
  -H "Authorization: Bearer gyn_sk_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345"
Response — 200 OK
json
{
  "entity_id": "student-abc-123",
  "entity_type": "student",
  "total_messages": 12,
  "offset": 0,
  "limit": 20,
  "messages": [
    { "role": "user", "content": "What is a variable?", "created_at": "2026-08-12T10:00:00.000Z" },
    { "role": "assistant", "content": "A variable is a symbol...", "created_at": "2026-08-12T10:00:05.000Z" }
  ],
  "has_more": false
}
EndpointScopePurpose
GET/entitiesentitiesList entities. Filters: type, parentId, search. limit 1–100 (default 50), cursor. Returns { entities, has_more, next_cursor }.
GET/entities/:identitiesFetch one entity. Same shape as the create response.
PATCH/entities/:identitiesPartial update of name, email, parentId, curriculumBoard, country, gradeLevel, subjects, metadata.
DELETE/entities/:identitiesDelete the entity and anonymise its request logs. Returns { deleted, entity_id, type }.

The legacy /students, /students/:id/history and DELETE /students/:id routes still work under the students scope alias. New integrations should use /entities.

Entity context

Pass entity_id on a chat request and the platform keeps the conversation going: the last 50 messages for that entity are replayed into the model automatically, scoped to your tenant. Omit it and the API is stateless — you send the whole history yourself.

Two turns, no history plumbing
text
Turn 1  {"messages":[{"role":"user","content":"What is a variable in math?"}],"entity_id":"student_abc123"}

Turn 2  {"messages":[{"role":"user","content":"Can you give me an example?"}],"entity_id":"student_abc123"}
        ^ the platform replays turn 1 into the model for you

The system prompt is also enriched with educational context. Each field is resolved independently, first definition wins:

FieldRequest bodyEntity recordTenant configDefault
gradebody.gradeentity.gradeLevelconfig.defaultGrade—
subjectbody.subject—config.allowedSubjects—
country—entity.countryconfig.country—
curriculumBoard—entity.curriculumBoardconfig.curriculumBoard—
language——config.language"en"

Set country and curriculumBoard once at the tenant level and every chat request uses board-aligned examples and terminology, without you sending a single extra field.

Export and deletion

EndpointScopePurpose
POST/compliance/student/exportcomplianceExport one entity’s data as JSON. Body: { "studentId": "<externalId>" }.
POST/compliance/student/deletecomplianceErase one entity’s data. Body: { "studentId": "<externalId>" }.

The student path segment is legacy; both routes operate on entities of any type. Deletion is a hard delete: the entity record is removed, tenant training data derived from it is deleted, and request logs are anonymised — messages cleared and the entity ID replaced with [deleted].

Data handling, retention and the subprocessor list →

Webhooks

Webhooks push events to your HTTPS endpoint instead of making you poll. Every delivery is signed with HMAC-SHA256 — verify the signature before you trust the body.

POST/webhooksscope: webhooks

Subscribe an HTTPS endpoint to one or more events.

FieldTypeRequiredDescription
urlstringYesPublic HTTPS URL. Private, loopback and reserved addresses are rejected.
eventsstring[]YesAt least one event name from the table below. Unknown names return 400.
Create a webhook
bash
curl -X POST https://api.gyanis.ai/platform/v1/webhooks \
  -H "Authorization: Bearer gyn_sk_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hooks.example.com/gyanis",
    "events": ["taxonomy.updated", "credits.low"]
  }'
Response — 201 Created
json
{
  "id": "6654a1b2c3d4e5f6a1b2c3e7",
  "url": "https://hooks.example.com/gyanis",
  "events": ["taxonomy.updated", "credits.low"],
  "status": "active",
  "secret": "whsec_2f8c5a1e9b7d4c3a6e0f8b2d5c7a9e1f3b6d8c0a2e4f7b9d1c3a5e8f0b2d4c6a",
  "created_at": "2026-08-13T09:14:03.201Z"
}

The signing secret is returned only on creation — GET /webhooks never repeats it. Two webhooks cannot share a URL within one tenant.

EventFires when
chat.completedA chat request completed successfully.
chat.errorA chat request failed.
credits.lowCredit balance fell below the warning threshold.
credits.depletedCredit balance reached zero.
usage.thresholdBudget usage crossed a 50%, 80%, 90% or 100% threshold.
student.createdA new student record was created.
moderation.flaggedContent was flagged by moderation.
taxonomy.updatedCurriculum taxonomy changed, debounced to roughly 60 seconds. The payload carries changed_since — call GET /taxonomy/changes?since= to fetch the delta.
Delivery headerMeaning
X-Gyanis-Signaturesha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with your webhook secret. Hash the bytes you received — verify before trusting the body.
X-Gyanis-EventThe event name, e.g. taxonomy.updated.
X-Gyanis-Delivery-IdUUID for this delivery attempt — use it to de-duplicate.
X-Gyanis-TimestampUnix timestamp of the delivery.

The signature is sha256= followed by the hex HMAC-SHA256 of the raw request body under your webhook secret. Compare it in constant time, and read the body as bytes — re-serialising the parsed JSON will not reproduce the same digest.

Verify a delivery signature
javascript
import { createHmac, timingSafeEqual } from "node:crypto";

/** @param {Buffer} rawBody  the exact bytes received, not JSON.parse output */
export function verifyGyanisWebhook(rawBody, signatureHeader, secret) {
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  const received = String(signatureHeader ?? "").replace(/^sha256=/, "");

  if (received.length !== expected.length) return false;

  return timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}

// Express: app.post("/gyanis", express.raw({ type: "application/json" }), ...)
// so req.body is a Buffer of the untouched payload.

Delivery and retries. A failed delivery is retried with exponential backoff — 30s, then 2m, 15m, 1h and 4h. That is one initial attempt plus five retries, six in total. After five consecutive failures the webhook is disabled automatically and stops receiving events until you re-enable it.

Idempotency. A retry reuses the event but not the delivery ID. De-duplicate on X-Gyanis-Delivery-Id and respond 2xx as soon as you have durably accepted the payload — a slow handler is a failed delivery. The dispatch timeout is 10 seconds.

EndpointScopePurpose
GET/webhookswebhooksList webhooks with id, url, events, status and fail_count. Never returns secrets.
DELETE/webhooks/:idwebhooksDelete a subscription. Returns { "deleted": true }.
GET/webhooks/:id/deliverieswebhooksDelivery log: event, status, http_status, attempt, latency_ms, error_message. Filters: status, limit 1–100, cursor.

Errors: invalid_request · webhook_not_found · endpoint_not_allowed

Usage and billing

Usage reporting

GET/usagescope: usage

Aggregated usage for one billing period, broken down per product.

One optional query parameter: period, formatted YYYY-MM, defaulting to the current UTC month.

Read usage for a period
bash
curl "https://api.gyanis.ai/platform/v1/usage?period=2026-08" \
  -H "Authorization: Bearer gyn_sk_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345"
Response — 200 OK, prepaid (providerBreakdown omitted)
json
{
  "object": "usage",
  "period": "2026-08",
  "totalRequests": 1247,
  "totalPromptTokens": 523400,
  "totalCompletionTokens": 312800,
  "totalTokens": 836200,
  "estimatedCostUsd": 4.82,
  "products": [
    {
      "product": "chat",
      "requests": 1180,
      "request_units": 0,
      "free_request_units": 0,
      "prompt_tokens": 523400,
      "completion_tokens": 312800,
      "total_tokens": 836200,
      "estimated_cost_usd": 4.82,
      "estimated_sell_usd": 9.6404
    },
    {
      "product": "taxonomy",
      "requests": 67,
      "request_units": 67,
      "free_request_units": 67,
      "prompt_tokens": 0,
      "completion_tokens": 0,
      "total_tokens": 0,
      "estimated_cost_usd": 0,
      "estimated_sell_usd": 0
    }
  ],
  "billingModel": "prepaid",
  "credits": 4163800,
  "budgetLimitTokens": 10000000
}

Usage is recorded per { tenant, key, period, product }, so chat and taxonomy are reported separately in products — that is where taxonomy request_units and free_request_units show up. The sell price is frozen at record time, so revenue history survives later pricing changes.

The response also carries providerBreakdown, a per-backend rollup of requests and tokens whose keys are internal routing identifiers. credits appears for prepaid tenants only, and budgetLimitTokens only when a budget cap is set. Postpaid tenants get neither.

Errors: invalid_request · endpoint_not_allowed

Credits and checkout

GET/billing/statusscope: billing

Credits, budget, current period, rates, projection and rate limits in one call.

Response — 200 OK, prepaid tenant
json
{
  "billing_model": "prepaid",
  "credits": {
    "remaining": 875000,
    "lifetime_purchased": 5000000,
    "low_balance_warning": false
  },
  "budget": { "limit_tokens": 0, "used_tokens": 500000, "utilization_pct": 0 },
  "current_period": {
    "period": "2026-08",
    "requests": 1247,
    "tokens": 500000,
    "estimated_cost_usd": 2.85,
    "estimated_sell_usd": 5.70
  },
  "pricing": {
    "tier": "standard",
    "sell_input_per_million": 4.00,
    "sell_output_per_million": 12.00,
    "sell_cached_input_per_million": 2.00
  },
  "projection": {
    "daily_avg_cost_usd": 0.95,
    "projected_cost_usd": 28.50,
    "daily_avg_tokens": 50000,
    "projected_tokens": 1500000,
    "days_until_exhausted": 17
  },
  "rate_limits": { "rpm": 60, "tpm": 100000, "daily_requests": 0 },
  "last_purchase": {
    "amount_usd": 50.00,
    "token_credits": 5000000,
    "status": "settled",
    "created_at": "2026-08-01T10:00:00.000Z"
  }
}

credits.low_balance_warning flips true below 10,000 remaining tokens. For a postpaid tenant projection.days_until_exhausted is null, budget.utilization_pct reflects real budget use, and last_purchase may be null. rate_limits echoes this tenant’s configured limits, so "daily_requests": 0 is the default and means unlimited.

POST/billing/checkoutscope: billing

Open a Stripe checkout session to buy credits. Credits land atomically on payment.

FieldTypeRequiredDescription
tokenCreditsnumberYesCredits to purchase, 100,000–100,000,000.
successUrlstringYesWhere Stripe returns after payment.
cancelUrlstringYesWhere Stripe returns if the buyer cancels.
cURL — Buy credits
bash
curl -X POST https://api.gyanis.ai/platform/v1/billing/checkout \
  -H "Authorization: Bearer gyn_sk_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345" \
  -H "Content-Type: application/json" \
  -d '{
    "tokenCredits": 5000000,
    "successUrl": "https://app.example.com/billing?state=ok",
    "cancelUrl": "https://app.example.com/billing?state=cancelled"
  }'
# → { "checkout_url": "https://checkout.stripe.com/...", "session_id": "cs_...",
#     "amount_usd": 50, "token_credits": 5000000 }
EndpointScopePurpose
GET/billing/confirm?session_id=billingIdempotently confirm a checkout after the Stripe redirect, without waiting for the webhook.
GET/billing/purchasesbillingPurchase history: amount_usd, token_credits, status, created_at, settled_at. limit 1–100 (default 20), cursor.

Errors: invalid_request · endpoint_not_allowed · account_suspended

Self-service (/me)

Every /me route mirrors a top-level one and enforces the same scope. They exist so a key can manage its own tenant without knowing a tenant ID.

EndpointScopeMirrors
GET/meany active keyTenant profile, credits and a usage summary.
GET/me/usageusageCurrent-period usage for the calling tenant. Same products breakdown as GET /usage, but without the object, billingModel, credits and budgetLimitTokens fields, and with no period parameter — it always returns the current month.
POST/me/keyskeysCreate a key. Same body and response as POST /keys.
GET/me/keyskeysList your keys.
DELETE/me/keys/:keyIdkeysRevoke a key.
POST/me/webhookswebhooksCreate a webhook. Same body as POST /webhooks.
GET/me/webhookswebhooksList your webhooks.
DELETE/me/webhooks/:idwebhooksDelete a webhook.
GET /me — Response 200 OK
json
{
  "tenant_id": "665a1b2c3d4e5f6a7b8c9d0f",
  "name": "Acme EdTech",
  "status": "active",
  "billing_model": "prepaid",
  "credits_remaining": 4163800,
  "budget_limit_tokens": 0,
  "active_keys": 3,
  "current_month": {
    "period": "2026-08",
    "requests": 1247,
    "tokens": 836200,
    "estimated_cost_usd": 4.82
  },
  "last_30_days_requests": 4180,
  "errors_last_24h": 2
}

Rates, credit packs and what a request actually costs →

Error codes

Every error, on every endpoint, has the same shape. Log request_id — it is the fastest route to an answer in support.

Error envelope
json
{
  "error": {
    "code": "insufficient_credits",
    "message": "Prepaid token credits exhausted. Purchase credits to continue.",
    "type": "billing_error",
    "request_id": "req_a1b2c3d4e5f6g7h8i9j0k1l2"
  }
}
TypeMeaning
authentication_errorThe API key is missing, invalid, revoked, expired, or the caller IP is not allowed.
scope_errorThe key lacks the scope the endpoint requires.
billing_errorAccount suspended, credits exhausted, or the budget cap reached.
rate_limit_errorA per-minute or per-day limit was exceeded.
validation_errorThe request body or a path/query parameter failed validation, or the resource does not exist.
permission_errorThe account is not entitled to the product or model tier requested.
server_errorAn upstream or internal failure — safe to retry with backoff.
CodeHTTPTypeDescription
missing_api_key401authentication_errorNo API key in the request headers.
invalid_api_key401authentication_errorThe key is malformed or does not exist.
revoked_api_key401authentication_errorThe key was explicitly revoked.
expired_api_key401authentication_errorThe key has passed its expiry date.
ip_not_allowed403authentication_errorThe caller IP is not in the tenant IP allowlist.
endpoint_not_allowed403scope_errorThe key scopes do not cover this endpoint.
account_suspended403billing_errorThe tenant account has been suspended.
product_not_enabled403permission_errorThe product is not enabled for the account — the taxonomy API itself, or the bulk-export add-on.
model_not_allowed403permission_errorThe requested model tier is not enabled for the account.
insufficient_credits402billing_errorThe prepaid balance is zero or below the amount the request requires.
budget_exceeded402billing_errorThe monthly token budget has been reached.
rpm_exceeded429rate_limit_errorRequests-per-minute limit exceeded.
tpm_exceeded429rate_limit_errorTokens-per-minute limit exceeded.
daily_limit_exceeded429rate_limit_errorThe daily request cap was reached.
test_mode_limit429rate_limit_errorThe test-mode daily request cap was reached — use a live key.
invalid_request400validation_errorValidation failed — missing fields, wrong types, or a bad parameter.
invalid_model400validation_errorUnknown model name.
message_too_long400validation_errorA single message exceeds the 16,000 character limit.
too_many_messages400validation_errorThe messages array exceeds the 100-message limit.
content_policy_violation400validation_errorThe message was flagged by content moderation.
not_found404validation_errorThe resource does not exist — an unknown topic, unit, subject, grade, board or exam-track ID.
webhook_not_found404validation_errorNo webhook with that ID.
internal_error500server_errorAn unexpected internal failure.
model_unavailable503server_errorThe model provider is temporarily unavailable.
service_circuit_open503server_errorThe provider circuit breaker is open after five consecutive failures; it blocks for 30 seconds, then probes.
timeout504server_errorThe model request timed out.

A revoked or expired key currently authenticates as invalid_api_key — treat all three 401 codes as one branch in your client. Retry 429, 503 and 504 with exponential backoff; never retry a 4xx validation failure unchanged.

Operational answers — retry etiquette, what to do on a 402 →

Rate limits

Limits are enforced per API key on a sliding window. Every default below is configurable per tenant.

LimitDefaultErrorNotes
RPM60rpm_exceededRequests per minute, sliding window, per key.
TPM100,000tpm_exceededTokens per minute, per key.
Daily requestsUnlimiteddaily_limit_exceededRequests per day. 0 means unlimited, which is the default.
POST /chat120 / minrpm_exceededEndpoint throttle, applied on top of the per-key RPM.
POST /chat/stream60 / minrpm_exceededStreaming costs a connection, so the throttle is half the chat one.
Test-mode keys50 / daytest_mode_limitDaily cap on requests made with a gyn_sk_test_ key.

Reliability and security

Distributed enforcement. Rate limiting can run against Redis so several API instances share one counter; it falls back to in-memory counting if Redis is unreachable.

IP allowlisting. A tenant can restrict API access to a list of addresses or CIDR ranges. An empty list means allow-all, which is the default; a call from outside the list gets 403 ip_not_allowed.

Circuit breaker. Each model provider sits behind a breaker. After five consecutive failures it opens, blocks for 30 seconds and returns 503 service_circuit_open, then half-opens and probes: success closes it, failure re-opens it. Back off and retry rather than hammering through it.

EndpointScopePurpose
GET/healthnonePublic, unauthenticated. Returns status "healthy" or "degraded" with per-dependency checks; 503 when degraded.

Response headers

HeaderDescription
X-Request-IdUnique request identifier (req_<24 chars>). Quote it in support requests.
X-RateLimit-Limit-RequestsMaximum requests per minute for this key.
X-RateLimit-Remaining-RequestsRequests remaining in the current window.
X-RateLimit-Reset-RequestsSeconds until the rate-limit window resets.

Ready to integrate?

The integration guide walks the same endpoints in order, from your first key to a grounded tutor in production.