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
https://api.gyanis.ai/platform/v1Every 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.
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 "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.
Authorization: Bearer gyn_sk_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345
X-API-Key: gyn_sk_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345Key scopes
A key carries a list of scopes. * is the default and grants every endpoint; anything narrower is enforced per request.
| Scope | Grants access to |
|---|---|
| * | All endpoints (default when no scopes are given). |
| chat | POST /chat |
| chat.stream | POST /chat/stream |
| keys | Key management — create, list and revoke API keys. |
| usage | GET /usage and GET /me/usage |
| webhooks | Webhook management — create, list, delete and read deliveries. |
| entities | Entity CRUD and history. The legacy alias students satisfies the same routes. |
| billing | Checkout, purchase history and billing status. |
| compliance | Data export and deletion requests. |
| taxonomy | Taxonomy reads — every GET /taxonomy/* endpoint plus POST /taxonomy/topics/resolve. |
| taxonomy.export | GET /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
/keysscope: keysMint a new API key. The plaintext key is returned exactly once.
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Display name, 1–200 characters. |
| mode | string | No | "live" (default) or "test". |
| scopes | string[] | No | Scopes to grant. Defaults to ["*"]. Unknown values are rejected. |
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"]
}'{
"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.
| Endpoint | Scope | Purpose |
|---|---|---|
GET/keys | keys | List keys, masked: keyId, name, prefix, mode, scopes, status, lastUsedAt, createdAt. |
DELETE/keys/:keyId | keys | Revoke 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.
| Model | Input / M | Output / M | Cached / 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.
Chat completion
/chatscope: chat120 req/minSend a conversation and receive one complete assistant message.
| Field | Type | Required | Description |
|---|---|---|---|
| messages | array | Yes | Message objects, 1–100 of them. |
| messages[].role | string | Yes | "user", "assistant" or "system". |
| messages[].content | string | Yes | Message text, 1–16,000 characters. |
| model | string | No | "mobius" (default) or "oloid". |
| max_tokens | integer | No | Response cap, 1–16,384. Default 4,096. |
| entity_id | string | No | Your external entity ID. Enables conversation continuity, max 256 chars. |
| entity_type | string | No | Type used when auto-creating an unknown entity_id. Default "student". |
| subject | string | No | Subject context, e.g. "math". Max 100 chars. |
| grade | string | No | Grade level, e.g. "8". Max 20 chars. |
| metadata | object | No | Custom key–value pairs, echoed back untouched. |
| json_mode | boolean | No | Ask for structured JSON output. |
| student_id | string | No | Deprecated alias of entity_id. Use entity_id in new code. |
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"
}'{
"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
Streaming chat (SSE)
/chat/streamscope: chat.stream60 req/minStream 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.
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"
}'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]| Line | What to do with it |
|---|---|
| data: {chunk} | Append delta.content to your output buffer. |
| data: [DONE] | Stream complete — close the connection. |
| : ping | Heartbeat comment every 15 seconds — ignore it. |
Errors: endpoint_not_allowed · rpm_exceeded · model_unavailable · timeout
Taxonomy API
/taxonomy/*scope: taxonomyStructured 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.
| Endpoint | Rate |
|---|---|
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.
curl "https://api.gyanis.ai/platform/v1/taxonomy/boards?country=IN" \
-H "Authorization: Bearer gyn_sk_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345"{
"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.
/taxonomy/topics/:topicId/contextscope: taxonomyEnrichedOne LLM-ready topic block — drop it straight into a system prompt.
curl "https://api.gyanis.ai/platform/v1/taxonomy/topics/652f1a9c4b7d3e0012ab34cd/context" \
-H "Authorization: Bearer gyn_sk_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345"{
"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.
/taxonomy/topics/resolvescope: taxonomyBatchResolve 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.
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"
]
}'{
"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.
| Type | Description |
|---|---|
| student | Individual learners. The only type that accumulates conversation history. |
| teacher | Educators associated with a class or institution. |
| institution | Schools, colleges or training organisations. |
| class | A group of students inside an institution. |
| curriculum | An educational framework a cohort follows. |
/entitiesscope: entitiesCreate one entity. externalId is unique per tenant and type.
| Field | Type | Required | Description |
|---|---|---|---|
| type | string | Yes | One of the five types above. |
| externalId | string | Yes | Your own identifier, 1–256 chars. This is what you pass as entity_id in chat. |
| name | string | Yes | Display name, 1–256 chars. |
| string | No | Valid email address. | |
| parentId | string | No | The platform id of the parent entity — the 24-character id returned by create, not its externalId. |
| curriculumBoard | string | No | e.g. "CBSE", "ICSE". Max 100 chars. |
| country | string | No | ISO 3166-1 alpha-2. Max 10 chars. |
| gradeLevel | string | No | e.g. "8", "K-12". Max 20 chars. |
| subjects | string[] | No | Subject areas, e.g. ["math", "science"]. |
| metadata | object | No | Custom key–value pairs. |
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" }
}'{
"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.
/entities/:id/historyscope: entitiesRead the stored conversation for one entity, newest window first.
| Param | Type | Required | Description |
|---|---|---|---|
| limit | integer | No | Messages to return, 1–200. Default 50. |
| offset | integer | No | Skip the first N messages. Default 0. |
curl "https://api.gyanis.ai/platform/v1/entities/student-abc-123/history?limit=20" \
-H "Authorization: Bearer gyn_sk_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345"{
"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
}| Endpoint | Scope | Purpose |
|---|---|---|
GET/entities | entities | List entities. Filters: type, parentId, search. limit 1–100 (default 50), cursor. Returns { entities, has_more, next_cursor }. |
GET/entities/:id | entities | Fetch one entity. Same shape as the create response. |
PATCH/entities/:id | entities | Partial update of name, email, parentId, curriculumBoard, country, gradeLevel, subjects, metadata. |
DELETE/entities/:id | entities | Delete 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.
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 youThe system prompt is also enriched with educational context. Each field is resolved independently, first definition wins:
| Field | Request body | Entity record | Tenant config | Default |
|---|---|---|---|---|
| grade | body.grade | entity.gradeLevel | config.defaultGrade | — |
| subject | body.subject | — | config.allowedSubjects | — |
| country | — | entity.country | config.country | — |
| curriculumBoard | — | entity.curriculumBoard | config.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
| Endpoint | Scope | Purpose |
|---|---|---|
POST/compliance/student/export | compliance | Export one entity’s data as JSON. Body: { "studentId": "<externalId>" }. |
POST/compliance/student/delete | compliance | Erase 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].
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.
/webhooksscope: webhooksSubscribe an HTTPS endpoint to one or more events.
| Field | Type | Required | Description |
|---|---|---|---|
| url | string | Yes | Public HTTPS URL. Private, loopback and reserved addresses are rejected. |
| events | string[] | Yes | At least one event name from the table below. Unknown names return 400. |
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"]
}'{
"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.
| Event | Fires when |
|---|---|
| chat.completed | A chat request completed successfully. |
| chat.error | A chat request failed. |
| credits.low | Credit balance fell below the warning threshold. |
| credits.depleted | Credit balance reached zero. |
| usage.threshold | Budget usage crossed a 50%, 80%, 90% or 100% threshold. |
| student.created | A new student record was created. |
| moderation.flagged | Content was flagged by moderation. |
| taxonomy.updated | Curriculum taxonomy changed, debounced to roughly 60 seconds. The payload carries changed_since — call GET /taxonomy/changes?since= to fetch the delta. |
| Delivery header | Meaning |
|---|---|
| X-Gyanis-Signature | sha256= 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-Event | The event name, e.g. taxonomy.updated. |
| X-Gyanis-Delivery-Id | UUID for this delivery attempt — use it to de-duplicate. |
| X-Gyanis-Timestamp | Unix 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.
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.
| Endpoint | Scope | Purpose |
|---|---|---|
GET/webhooks | webhooks | List webhooks with id, url, events, status and fail_count. Never returns secrets. |
DELETE/webhooks/:id | webhooks | Delete a subscription. Returns { "deleted": true }. |
GET/webhooks/:id/deliveries | webhooks | Delivery 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
/usagescope: usageAggregated usage for one billing period, broken down per product.
One optional query parameter: period, formatted YYYY-MM, defaulting to the current UTC month.
curl "https://api.gyanis.ai/platform/v1/usage?period=2026-08" \
-H "Authorization: Bearer gyn_sk_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345"{
"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
/billing/statusscope: billingCredits, budget, current period, rates, projection and rate limits in one call.
{
"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.
/billing/checkoutscope: billingOpen a Stripe checkout session to buy credits. Credits land atomically on payment.
| Field | Type | Required | Description |
|---|---|---|---|
| tokenCredits | number | Yes | Credits to purchase, 100,000–100,000,000. |
| successUrl | string | Yes | Where Stripe returns after payment. |
| cancelUrl | string | Yes | Where Stripe returns if the buyer cancels. |
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 }| Endpoint | Scope | Purpose |
|---|---|---|
GET/billing/confirm?session_id= | billing | Idempotently confirm a checkout after the Stripe redirect, without waiting for the webhook. |
GET/billing/purchases | billing | Purchase 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.
| Endpoint | Scope | Mirrors |
|---|---|---|
GET/me | any active key | Tenant profile, credits and a usage summary. |
GET/me/usage | usage | Current-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/keys | keys | Create a key. Same body and response as POST /keys. |
GET/me/keys | keys | List your keys. |
DELETE/me/keys/:keyId | keys | Revoke a key. |
POST/me/webhooks | webhooks | Create a webhook. Same body as POST /webhooks. |
GET/me/webhooks | webhooks | List your webhooks. |
DELETE/me/webhooks/:id | webhooks | Delete a webhook. |
{
"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
}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": {
"code": "insufficient_credits",
"message": "Prepaid token credits exhausted. Purchase credits to continue.",
"type": "billing_error",
"request_id": "req_a1b2c3d4e5f6g7h8i9j0k1l2"
}
}| Type | Meaning |
|---|---|
| authentication_error | The API key is missing, invalid, revoked, expired, or the caller IP is not allowed. |
| scope_error | The key lacks the scope the endpoint requires. |
| billing_error | Account suspended, credits exhausted, or the budget cap reached. |
| rate_limit_error | A per-minute or per-day limit was exceeded. |
| validation_error | The request body or a path/query parameter failed validation, or the resource does not exist. |
| permission_error | The account is not entitled to the product or model tier requested. |
| server_error | An upstream or internal failure — safe to retry with backoff. |
| Code | HTTP | Type | Description |
|---|---|---|---|
| missing_api_key | 401 | authentication_error | No API key in the request headers. |
| invalid_api_key | 401 | authentication_error | The key is malformed or does not exist. |
| revoked_api_key | 401 | authentication_error | The key was explicitly revoked. |
| expired_api_key | 401 | authentication_error | The key has passed its expiry date. |
| ip_not_allowed | 403 | authentication_error | The caller IP is not in the tenant IP allowlist. |
| endpoint_not_allowed | 403 | scope_error | The key scopes do not cover this endpoint. |
| account_suspended | 403 | billing_error | The tenant account has been suspended. |
| product_not_enabled | 403 | permission_error | The product is not enabled for the account — the taxonomy API itself, or the bulk-export add-on. |
| model_not_allowed | 403 | permission_error | The requested model tier is not enabled for the account. |
| insufficient_credits | 402 | billing_error | The prepaid balance is zero or below the amount the request requires. |
| budget_exceeded | 402 | billing_error | The monthly token budget has been reached. |
| rpm_exceeded | 429 | rate_limit_error | Requests-per-minute limit exceeded. |
| tpm_exceeded | 429 | rate_limit_error | Tokens-per-minute limit exceeded. |
| daily_limit_exceeded | 429 | rate_limit_error | The daily request cap was reached. |
| test_mode_limit | 429 | rate_limit_error | The test-mode daily request cap was reached — use a live key. |
| invalid_request | 400 | validation_error | Validation failed — missing fields, wrong types, or a bad parameter. |
| invalid_model | 400 | validation_error | Unknown model name. |
| message_too_long | 400 | validation_error | A single message exceeds the 16,000 character limit. |
| too_many_messages | 400 | validation_error | The messages array exceeds the 100-message limit. |
| content_policy_violation | 400 | validation_error | The message was flagged by content moderation. |
| not_found | 404 | validation_error | The resource does not exist — an unknown topic, unit, subject, grade, board or exam-track ID. |
| webhook_not_found | 404 | validation_error | No webhook with that ID. |
| internal_error | 500 | server_error | An unexpected internal failure. |
| model_unavailable | 503 | server_error | The model provider is temporarily unavailable. |
| service_circuit_open | 503 | server_error | The provider circuit breaker is open after five consecutive failures; it blocks for 30 seconds, then probes. |
| timeout | 504 | server_error | The 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.
| Limit | Default | Error | Notes |
|---|---|---|---|
| RPM | 60 | rpm_exceeded | Requests per minute, sliding window, per key. |
| TPM | 100,000 | tpm_exceeded | Tokens per minute, per key. |
| Daily requests | Unlimited | daily_limit_exceeded | Requests per day. 0 means unlimited, which is the default. |
| POST /chat | 120 / min | rpm_exceeded | Endpoint throttle, applied on top of the per-key RPM. |
| POST /chat/stream | 60 / min | rpm_exceeded | Streaming costs a connection, so the throttle is half the chat one. |
| Test-mode keys | 50 / day | test_mode_limit | Daily 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.
| Endpoint | Scope | Purpose |
|---|---|---|
GET/health | none | Public, unauthenticated. Returns status "healthy" or "degraded" with per-dependency checks; 503 when degraded. |
Response headers
| Header | Description |
|---|---|
| X-Request-Id | Unique request identifier (req_<24 chars>). Quote it in support requests. |
| X-RateLimit-Limit-Requests | Maximum requests per minute for this key. |
| X-RateLimit-Remaining-Requests | Requests remaining in the current window. |
| X-RateLimit-Reset-Requests | Seconds until the rate-limit window resets. |