Jump to Section
SPYRAL Research API
Programmatic access to longitudinal student performance data, competency trends, and AI challenge statistics from India's NEP 2020 adaptive learning platform.
https://tryspyral.com/api/researchAll responses are JSON. All requests require an API key.
The Research API is designed for educational researchers, government bodies, and B2B partners who need structured access to anonymised learning analytics at scale. PII is masked by default — student IDs are replaced with stable anonymised tokens.
Authentication
All Research API endpoints require an API key with the rsk_ prefix. Pass it in one of two ways:
# Recommended — header never appears in server logs
curl -H "X-Api-Key: rsk_your_api_key_here" \
https://tryspyral.com/api/research/schools
# Query param — convenient for quick testing only
# Avoid in production: key may appear in access logs
curl https://tryspyral.com/api/research/schools?api_key=rsk_your_api_key_here
Quick Start
Make your first API call in under 60 seconds. Replace rsk_YOUR_KEY with your actual key.
# 1. List schools available under your key
curl -H "X-Api-Key: rsk_YOUR_KEY" \
https://tryspyral.com/api/research/schools
# 2. Pull SPI timeline for a school (last 30 days)
curl -H "X-Api-Key: rsk_YOUR_KEY" \
"https://tryspyral.com/api/research/spi-timeline?schoolId=SCH001&from=2025-01-01&limit=100"
// spyral-client.js — copy-paste ready
const API_KEY = 'rsk_YOUR_KEY';
const BASE_URL = 'https://tryspyral.com/api/research';
async function spyral(endpoint, params = {}) {
const url = new URL(BASE_URL + endpoint);
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
const res = await fetch(url, {
headers: { 'X-Api-Key': API_KEY }
});
if (!res.ok) throw new Error(`API error ${res.status}`);
return res.json();
}
// Usage
spyral('/schools').then(d => console.log(d.data.schools));
spyral('/spi-timeline', { schoolId: 'SCH001', limit: '100' })
.then(d => console.log(d.data.records));
# spyral_client.py — copy-paste ready
import requests
API_KEY = "rsk_YOUR_KEY"
BASE_URL = "https://tryspyral.com/api/research"
HEADERS = {"X-Api-Key": API_KEY}
def spyral(endpoint, **params):
r = requests.get(BASE_URL + endpoint, headers=HEADERS, params=params)
r.raise_for_status()
return r.json()
# Usage
schools = spyral("/schools")
print(schools["data"]["schools"])
timeline = spyral("/spi-timeline", schoolId="SCH001", limit=100)
print(timeline["data"]["records"])
API Endpoints
Multi-year SPI trajectory snapshots for a school cohort. Returns data points per student showing performance evolution over time.
Parameters
| Name | Type | Description |
|---|---|---|
| schoolId * | string | School identifier (required) |
| classNum | integer | Filter by class number (e.g. 8, 9, 10) |
| from | ISO date | Start date filter — 2024-01-01 |
| to | ISO date | End date filter |
| page | integer | Page number (default: 1) |
| limit | integer | Records per page (default: 100, max: 500) |
curl -H "X-Api-Key: rsk_YOUR_KEY" \
"https://tryspyral.com/api/research/longitudinal?schoolId=SCH001&classNum=8&from=2024-01-01&limit=100"
const data = await spyral('/longitudinal', {
schoolId: 'SCH001',
classNum: 8,
from: '2024-01-01',
limit: 100
});
data = spyral("/longitudinal",
schoolId="SCH001", classNum=8,
from_date="2024-01-01", limit=100)
{
"success": true,
"data": {
"records": [
{
"id": "anon_dGVzdA",
"studentRef": "anon_c3R1ZA", // stable anon ID
"class": 8,
"section": "A",
"trackingStartDate": "2024-06-01T00:00:00.000Z",
"dataPoints": [
{ "timestamp": "2024-06-15T...", "spi": 72.4 },
{ "timestamp": "2024-07-15T...", "spi": 75.1 }
],
"trends": { "slope": 0.23, "direction": "improving" }
}
],
"total": 248, "page": 1, "pages": 3
}
}
SPI (Student Performance Index) snapshots over time for a school cohort. Includes Kalman uncertainty estimates and learning state classifications.
Parameters
| Name | Type | Description |
|---|---|---|
| schoolId * | string | School identifier (required) |
| classNum | integer | Filter by class number |
| from / to | ISO date | Date range filter |
| page / limit | integer | Pagination (max limit: 500) |
curl -H "X-Api-Key: rsk_YOUR_KEY" \
"https://tryspyral.com/api/research/spi-timeline?schoolId=SCH001&from=2024-01-01&to=2024-12-31&limit=500"
const data = await spyral('/spi-timeline', {
schoolId: 'SCH001',
from: '2024-01-01', to: '2024-12-31', limit: 500
});
data = spyral("/spi-timeline",
schoolId="SCH001",
**{"from": "2024-01-01", "to": "2024-12-31"},
limit=500)
{
"data": {
"records": [
{
"studentRef": "anon_c3R1ZA",
"class": 9,
"spi": 78.3,
"grade": "B+",
"learningState": "consolidating",
"kalmanUncertainty": 0.042,
"totalChallenges": 34,
"calculatedAt": "2024-09-15T10:22:00.000Z",
"source": "challenge_evaluated"
}
]
}
}
Aggregated Bayesian belief scores per competency across a school cohort. Returns average mastery level per NEP 2020 competency plus Kalman ability distribution.
Parameters
| Name | Type | Description |
|---|---|---|
| schoolId * | string | School identifier |
| classNum | integer | Filter by class number |
curl -H "X-Api-Key: rsk_YOUR_KEY" \
"https://tryspyral.com/api/research/competency-trends?schoolId=SCH001&classNum=10"
const data = await spyral('/competency-trends', {
schoolId: 'SCH001', classNum: 10
});
data = spyral("/competency-trends", schoolId="SCH001", classNum=10)
{
"data": {
"studentCount": 142,
"competencies": {
"critical_thinking": { "averageMastery": 0.712, "studentsCovered": 138 },
"problem_solving": { "averageMastery": 0.689, "studentsCovered": 140 },
"communication": { "averageMastery": 0.754, "studentsCovered": 135 }
},
"kalman": { "averageAbility": 0.681, "sampleSize": 138 }
}
}
Aggregated challenge performance statistics broken down by simulation type. Includes pass rates, average scores, and average completion time.
curl -H "X-Api-Key: rsk_YOUR_KEY" \
"https://tryspyral.com/api/research/challenge-stats?schoolId=SCH001&from=2024-01-01"
const data = await spyral('/challenge-stats', {
schoolId: 'SCH001', from: '2024-01-01'
});
data = spyral("/challenge-stats", schoolId="SCH001",
**{"from": "2024-01-01"})
{
"data": {
"bySimulation": [
{
"simulationType": "math_reasoning",
"totalAttempts": 1240,
"passRate": 74.2,
"averageScore": 68.5,
"averageTimeMin": 18.3
}
],
"overall": { "total": 4820, "passRate": 71.8, "avgScore": 67.2 }
}
}
List schools available under your API key. School names and cities are anonymised by default. Full metadata requires the canViewRawData permission.
curl -H "X-Api-Key: rsk_YOUR_KEY" \
https://tryspyral.com/api/research/schools
const data = await spyral('/schools');
data = spyral("/schools")
{
"data": {
"schools": [
{
"id": "SCH001", // real ID if canViewRawData
"name": "School-anon_dGVz", // anonymised by default
"state": "Maharashtra",
"studentCount": 480,
"teacherCount": 32,
"plan": "premium"
}
],
"total": 14
}
}
canExportFullDataset permission on your API key.
Paginated full dataset export. Choose collection: spi, longitudinal, or challenges. Maximum page size is capped by your key's maxExportSize setting.
Parameters
| Name | Type | Description |
|---|---|---|
| collection | string | spi | longitudinal | challenges (default: spi) |
| schoolId | string | Filter by school (optional) |
| from / to | ISO date | Date range |
| page / limit | integer | Pagination — capped by maxExportSize on your key |
curl -H "X-Api-Key: rsk_YOUR_KEY" \
"https://tryspyral.com/api/research/export?collection=spi&schoolId=SCH001&page=1&limit=500"
// Paginate through all SPI records
async function exportAll(schoolId) {
let page = 1, all = [];
while (true) {
const { data } = await spyral('/export', {
collection: 'spi', schoolId, page, limit: 500
});
all.push(...data.records);
if (page >= data.pages) break;
page++;
}
return all;
}
def export_all(school_id, collection="spi"):
records, page = [], 1
while True:
r = spyral("/export",
collection=collection,
schoolId=school_id,
page=page, limit=500)
records.extend(r["data"]["records"])
if page >= r["data"]["pages"]: break
page += 1
return records
Response Format
Every response follows the same envelope structure:
{
"success": true, // false on error
"data": { ... } // payload on success
}
// Paginated endpoints include:
"total": 1240, // total records matching query
"page": 1,
"pages": 3, // ceil(total / limit)
"limit": 500
"anon_...") by default. Tokens are consistent across API calls — the same student always gets the same token. If your key has canViewRawData, real IDs are returned.
Error Codes
| HTTP | Code | Meaning |
|---|---|---|
| 401 | API_KEY_REQUIRED | No key provided |
| 401 | INVALID_API_KEY | Key not found |
| 401 | API_KEY_REVOKED | Key has been revoked |
| 401 | API_KEY_EXPIRED | Key past its expiry date |
| 403 | PERMISSION_DENIED | Key lacks required permission (e.g. canExportFullDataset) |
| 400 | — | Missing required parameter (message explains which) |
| 429 | — | Rate limit exceeded — back off and retry |
| 500 | — | Internal error — contact info@tryspyral.com |
Rate Limits
| Tier | Calls / day | Max page size |
|---|---|---|
| Basic | 1,000 | 100 |
| Premium | 10,000 | 500 |
| Enterprise | Unlimited | 500 (custom on request) |
When you exceed the rate limit the API returns HTTP 429. Add exponential back-off to your retry logic.
API Plans
Research API access is included in the Enterprise plan. Specific data-export permissions must be granted by the SPYRAL team when your key is provisioned.
Basic
- Up to 500 students
- AI challenges
- SPI tracking
- NEP 2020 reports
Premium
- Up to 2,000 students
- Advanced analytics
- Batch reports
- Export APIs
Enterprise
- Unlimited students
- Research API access
- Raw data export
- Dedicated support
Request API Access
API keys are provisioned by the SPYRAL team. Submit the form below — we'll set up your key within one business day.