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.

Base URL: https://tryspyral.com/api/research
All 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
Keep your key secret. Do not commit it to version control or expose it in client-side code. Treat it like a password.

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

GET /api/research/longitudinal

Multi-year SPI trajectory snapshots for a school cohort. Returns data points per student showing performance evolution over time.

Parameters

NameTypeDescription
schoolId *stringSchool identifier (required)
classNumintegerFilter by class number (e.g. 8, 9, 10)
fromISO dateStart date filter — 2024-01-01
toISO dateEnd date filter
pageintegerPage number (default: 1)
limitintegerRecords 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
  }
}
GET /api/research/spi-timeline

SPI (Student Performance Index) snapshots over time for a school cohort. Includes Kalman uncertainty estimates and learning state classifications.

Parameters

NameTypeDescription
schoolId *stringSchool identifier (required)
classNumintegerFilter by class number
from / toISO dateDate range filter
page / limitintegerPagination (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"
      }
    ]
  }
}
GET /api/research/challenge-stats

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 }
  }
}
GET /api/research/schools

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
  }
}
GET /api/research/export
Requires 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

NameTypeDescription
collectionstringspi | longitudinal | challenges (default: spi)
schoolIdstringFilter by school (optional)
from / toISO dateDate range
page / limitintegerPagination — 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
PII policy. Student IDs are replaced with stable anonymised tokens ("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

HTTPCodeMeaning
401API_KEY_REQUIREDNo key provided
401INVALID_API_KEYKey not found
401API_KEY_REVOKEDKey has been revoked
401API_KEY_EXPIREDKey past its expiry date
403PERMISSION_DENIEDKey lacks required permission (e.g. canExportFullDataset)
400Missing required parameter (message explains which)
429Rate limit exceeded — back off and retry
500Internal error — contact info@tryspyral.com

Rate Limits

TierCalls / dayMax page size
Basic1,000100
Premium10,000500
EnterpriseUnlimited500 (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

₹2,999 /month
  • Up to 500 students
  • AI challenges
  • SPI tracking
  • NEP 2020 reports

Premium

₹9,999 /month
  • Up to 2,000 students
  • Advanced analytics
  • Batch reports
  • Export APIs

Enterprise

Custom
  • 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.

Request received! We'll be in touch within one business day with your API key.