This guide provides practical examples for common API integration scenarios. For the complete API specification, see the API Reference.
Authentication
All API requests require authentication via Bearer token.
Obtaining a Token
Tokens are generated from your user settings:
- Navigate to Settings > API Tokens
- Click Generate New Token
- Copy and securely store the token
Using the Token
Include the token in the Authorization header:
curl -H "Authorization: Bearer YOUR_API_TOKEN" \
https://your-domain.com/api/v1/curricula
const response = await fetch('/api/v1/curricula', {
headers: {
'Authorization': 'Bearer YOUR_API_TOKEN',
'Content-Type': 'application/json'
}
});
Curricula Endpoints
List All Curricula
# Basic list
curl -H "Authorization: Bearer TOKEN" \
https://your-domain.com/api/v1/curricula
# With pagination
curl -H "Authorization: Bearer TOKEN" \
"https://your-domain.com/api/v1/curricula?page=1&per_page=20"
# Only base curricula (no variants)
curl -H "Authorization: Bearer TOKEN" \
"https://your-domain.com/api/v1/curricula?root_only=true"
# Only curricula with published content
curl -H "Authorization: Bearer TOKEN" \
"https://your-domain.com/api/v1/curricula?status=published"
Response:
json
{
"data": [
{
"id": "123",
"type": "curriculum",
"attributes": {
"name": "Grade 3 Mathematics",
"description": "Complete Grade 3 math curriculum",
"structure_name": "K-5 Math Structure",
"node_count": 245,
"is_variant": false,
"parent_id": null,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-03-20T14:45:00Z"
}
}
],
"meta": {
"total_count": 15,
"page": 1,
"per_page": 20
}
}
Get Curriculum with Root Nodes
Returns curriculum details plus root-level content nodes (lazy-loading pattern):
curl -H "Authorization: Bearer TOKEN" \
https://your-domain.com/api/v1/curricula/123
Response:
json
{
"data": {
"id": "123",
"type": "curriculum",
"attributes": {
"name": "Grade 3 Mathematics",
"description": "Complete Grade 3 math curriculum",
"structure_name": "K-5 Math Structure",
"node_count": 245,
"is_variant": false,
"parent_id": null,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-03-20T14:45:00Z"
},
"relationships": {
"content_nodes": {
"data": [
{
"id": "1001",
"type": "content_node",
"attributes": {
"title": "Unit 1: Addition and Subtraction",
"node_type": {
"name": "unit",
"display_name": "Unit",
"icon": "folder"
},
"ordinal_label": "1",
"type_display": "Unit 1",
"workflow_status": "published",
"position": 0,
"depth": 0,
"has_children": true,
"attributes": {
"title": "Unit 1: Addition and Subtraction",
"description": "Foundational operations..."
}
}
}
]
}
}
},
"meta": {
"root_count": 8,
"total_node_count": 245
}
}
Get Curriculum Variants
List all variants (direct and nested) for a curriculum:
curl -H "Authorization: Bearer TOKEN" \
https://your-domain.com/api/v1/curricula/123/variants
Response:
json
{
"data": [
{
"id": "456",
"type": "curriculum_variant",
"attributes": {
"name": "Grade 3 Math - Texas",
"description": "TEKS-aligned variant",
"parent_id": "123",
"depth": 1,
"node_count": 12,
"override_count": 45,
"added_count": 12
}
}
],
"meta": {
"total_count": 3,
"curriculum_id": "123",
"curriculum_name": "Grade 3 Mathematics"
}
}
Get Locale Progress
Check translation progress for each enabled locale:
curl -H "Authorization: Bearer TOKEN" \
https://your-domain.com/api/v1/curricula/123/locales
Response:
json
{
"data": [
{
"code": "en",
"name": "English",
"is_primary": true,
"translation_progress": 100,
"published_progress": 85
},
{
"code": "es",
"name": "Spanish",
"is_primary": false,
"translation_progress": 72,
"published_progress": 68
}
],
"meta": {
"primary_locale": "en",
"total_locales": 2,
"curriculum_id": "123",
"curriculum_name": "Grade 3 Mathematics"
}
}
Get Resolved Curriculum
Returns fully resolved content with inheritance and translations applied:
# Base curriculum in English
curl -H "Authorization: Bearer TOKEN" \
https://your-domain.com/api/v1/curricula/123/resolved
# Specific variant
curl -H "Authorization: Bearer TOKEN" \
"https://your-domain.com/api/v1/curricula/123/resolved?variant=Texas"
# Spanish translation
curl -H "Authorization: Bearer TOKEN" \
"https://your-domain.com/api/v1/curricula/123/resolved?locale=es"
# Variant with Spanish translation
curl -H "Authorization: Bearer TOKEN" \
"https://your-domain.com/api/v1/curricula/123/resolved?variant=Texas&locale=es"
Response:
json
{
"data": {
"id": "123",
"type": "resolved_curriculum",
"attributes": {
"name": "Grade 3 Mathematics",
"variant": "Texas",
"locale": "es",
"resolved_at": "2024-03-20T14:45:00Z"
},
"relationships": {
"content_nodes": {
"data": [
{
"id": "1001",
"type": "resolved_content_node",
"attributes": {
"title": "Unidad 1: Suma y Resta",
"position": 0,
"node_type": {
"name": "unit",
"display_name": "Unit"
},
"workflow_status": "published",
"attributes": {
"title": "Unidad 1: Suma y Resta",
"description": "Operaciones fundamentales..."
},
"inheritance_metadata": {
"is_inherited": false,
"source_curriculum": "Grade 3 Math - Texas",
"override_level": 1
},
"translation_metadata": {
"is_translated": true,
"translation_status": "translated",
"source_locale": "en"
}
},
"children": [...]
}
]
}
}
},
"meta": {
"variant_chain": ["Grade 3 Mathematics", "Grade 3 Math - Texas"],
"locale": "es",
"node_count": 245,
"max_depth": 4
}
}
Content Node Endpoints
Get Node with Children
Returns a content node plus its direct children (one level):
curl -H "Authorization: Bearer TOKEN" \
https://your-domain.com/api/v1/content_nodes/1001
Response:
json
{
"data": {
"id": "1001",
"type": "content_node",
"attributes": {
"title": "Unit 1: Addition and Subtraction",
"node_type": {
"name": "unit",
"display_name": "Unit",
"icon": "folder"
},
"workflow_status": "published",
"position": 0,
"depth": 0,
"has_children": true,
"attributes": {
"title": "Unit 1: Addition and Subtraction",
"description": "Learn addition and subtraction strategies"
}
},
"relationships": {
"curriculum": {
"data": {"id": "123", "type": "curriculum"}
},
"parent": {
"data": null
},
"children": {
"data": [
{
"id": "1002",
"type": "content_node",
"attributes": {
"title": "Lesson 1: Adding Within 20",
"has_children": true,
...
}
}
]
}
}
},
"meta": {
"children_count": 5,
"ancestry_depth": 0
}
}
Get Resolved Node
Returns a single node with inheritance and translation resolved:
# English
curl -H "Authorization: Bearer TOKEN" \
https://your-domain.com/api/v1/content_nodes/1001/resolved
# With variant context
curl -H "Authorization: Bearer TOKEN" \
"https://your-domain.com/api/v1/content_nodes/1001/resolved?variant=Texas"
# With translation
curl -H "Authorization: Bearer TOKEN" \
"https://your-domain.com/api/v1/content_nodes/1001/resolved?locale=es"
Lazy-Loading Tree Pattern
The API uses lazy-loading for efficient tree traversal. Each node includes has_children to indicate if deeper content exists.
JavaScript Implementation
class CurriculumTree {
constructor(apiToken, baseUrl = '/api/v1') {
this.token = apiToken;
this.baseUrl = baseUrl;
this.cache = new Map();
}
async fetch(endpoint) {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}
// Load curriculum with root nodes
async loadCurriculum(curriculumId) {
const { data } = await this.fetch(`/curricula/${curriculumId}`);
return {
curriculum: data.attributes,
rootNodes: data.relationships.content_nodes.data
};
}
// Load children of a node
async loadChildren(nodeId) {
// Check cache first
if (this.cache.has(nodeId)) {
return this.cache.get(nodeId);
}
const { data } = await this.fetch(`/content_nodes/${nodeId}`);
const children = data.relationships.children.data;
// Cache the result
this.cache.set(nodeId, children);
return children;
}
// Recursive tree loading with depth limit
async loadTree(nodeId, maxDepth = 3, currentDepth = 0) {
if (currentDepth >= maxDepth) {
return null;
}
const children = await this.loadChildren(nodeId);
return Promise.all(
children.map(async (child) => {
if (child.attributes.has_children && currentDepth < maxDepth - 1) {
child.children = await this.loadTree(
child.id,
maxDepth,
currentDepth + 1
);
}
return child;
})
);
}
}
// Usage
const tree = new CurriculumTree('YOUR_TOKEN');
const { curriculum, rootNodes } = await tree.loadCurriculum('123');
// Load children on demand (e.g., when user expands a node)
const children = await tree.loadChildren('1001');
React Integration Example
function ContentNode({ node, onExpand }) {
const [children, setChildren] = useState([]);
const [expanded, setExpanded] = useState(false);
const [loading, setLoading] = useState(false);
const handleExpand = async () => {
if (!node.attributes.has_children) return;
if (!expanded && children.length === 0) {
setLoading(true);
try {
const loadedChildren = await onExpand(node.id);
setChildren(loadedChildren);
} finally {
setLoading(false);
}
}
setExpanded(!expanded);
};
return (
<div className="node">
<div className="node-header" onClick={handleExpand}>
{node.attributes.has_children && (
<span className="expand-icon">
{loading ? '...' : expanded ? 'â–¼' : 'â–¶'}
</span>
)}
<span className="node-title">{node.attributes.title}</span>
<span className="node-type">{node.attributes.node_type.display_name}</span>
</div>
{expanded && children.length > 0 && (
<div className="node-children">
{children.map(child => (
<ContentNode
key={child.id}
node={child}
onExpand={onExpand}
/>
))}
</div>
)}
</div>
);
}
Caching with ETags
The API supports HTTP caching for efficient polling and reduced bandwidth.
Implementation Pattern
class CachedApiClient {
constructor(token) {
this.token = token;
this.etags = new Map();
}
async fetch(endpoint) {
const headers = {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
};
// Include ETag if we have one
const cachedEtag = this.etags.get(endpoint);
if (cachedEtag) {
headers['If-None-Match'] = cachedEtag;
}
const response = await fetch(endpoint, { headers });
// 304 Not Modified - content unchanged
if (response.status === 304) {
return { unchanged: true, data: null };
}
// Store new ETag for future requests
const newEtag = response.headers.get('ETag');
if (newEtag) {
this.etags.set(endpoint, newEtag);
}
const data = await response.json();
return { unchanged: false, data };
}
}
// Usage - efficient polling
const client = new CachedApiClient('YOUR_TOKEN');
async function pollForUpdates(curriculumId) {
const endpoint = `/api/v1/curricula/${curriculumId}/resolved`;
const { unchanged, data } = await client.fetch(endpoint);
if (unchanged) {
console.log('No changes since last fetch');
return null;
}
console.log('Content updated');
return data;
}
// Poll every 5 minutes
setInterval(() => pollForUpdates('123'), 5 * 60 * 1000);
Error Handling
HTTP Status Codes
| Status | Meaning | Action |
|---|---|---|
| 200 | Success | Process response |
| 304 | Not Modified | Use cached data |
| 400 | Bad Request | Check request parameters |
| 401 | Unauthorized | Refresh/check token |
| 403 | Forbidden | Check permissions |
| 404 | Not Found | Resource doesn't exist |
| 429 | Rate Limited | Implement backoff |
| 500 | Server Error | Retry with backoff |
Error Response Format
{
"error": {
"code": "unauthorized",
"message": "Invalid or expired API token",
"details": {}
}
}
Robust Error Handling
class ApiClient {
async fetchWithRetry(endpoint, options = {}) {
const maxRetries = options.retries || 3;
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await this.fetch(endpoint);
return response;
} catch (error) {
lastError = error;
// Don't retry client errors (4xx)
if (error.status >= 400 && error.status < 500) {
throw error;
}
// Exponential backoff for server errors
const delay = Math.pow(2, attempt) * 1000;
await new Promise(r => setTimeout(r, delay));
}
}
throw lastError;
}
handleError(error) {
switch (error.code) {
case 'unauthorized':
// Redirect to login or refresh token
this.refreshToken();
break;
case 'forbidden':
// Show access denied message
this.showAccessDenied();
break;
case 'rate_limit_exceeded':
// Queue request for later
this.queueForRetry();
break;
default:
// Log and show generic error
console.error('API error:', error);
this.showGenericError();
}
}
}
Rate Limiting
The API implements rate limiting to ensure fair usage:
| Limit Type | Value |
|---|---|
| Requests per minute | 60 |
| Requests per hour | 1000 |
Handling Rate Limits
async function fetchWithRateLimit(endpoint) {
const response = await fetch(endpoint, { headers });
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
const waitMs = (parseInt(retryAfter) || 60) * 1000;
console.log(`Rate limited. Waiting ${waitMs}ms`);
await new Promise(r => setTimeout(r, waitMs));
return fetchWithRateLimit(endpoint);
}
return response;
}
Integration Patterns
Full Curriculum Export
async function exportCurriculum(curriculumId, options = {}) {
const locale = options.locale || 'en';
const variant = options.variant;
// Build query string
let query = `locale=${locale}`;
if (variant) query += `&variant=${encodeURIComponent(variant)}`;
const response = await fetch(
`/api/v1/curricula/${curriculumId}/resolved?${query}`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
const { data, meta } = await response.json();
return {
curriculum: data.attributes,
nodes: flattenTree(data.relationships.content_nodes.data),
meta
};
}
function flattenTree(nodes, result = []) {
for (const node of nodes) {
result.push({
id: node.id,
...node.attributes
});
if (node.children) {
flattenTree(node.children, result);
}
}
return result;
}
Syncing to External System
class CurriculumSync {
constructor(apiToken, externalSystem) {
this.api = new CachedApiClient(apiToken);
this.external = externalSystem;
this.lastSyncTime = null;
}
async sync(curriculumId) {
const endpoint = `/api/v1/curricula/${curriculumId}/resolved`;
const { unchanged, data } = await this.api.fetch(endpoint);
if (unchanged) {
console.log('No changes to sync');
return { synced: 0 };
}
const nodes = data.relationships.content_nodes.data;
let synced = 0;
for (const node of this.flattenNodes(nodes)) {
await this.external.upsert({
externalId: `curriculum_${curriculumId}_node_${node.id}`,
title: node.attributes.title,
content: node.attributes.attributes,
type: node.attributes.node_type.name,
status: node.attributes.workflow_status
});
synced++;
}
this.lastSyncTime = new Date();
return { synced };
}
flattenNodes(nodes, result = []) {
for (const node of nodes) {
result.push(node);
if (node.children) {
this.flattenNodes(node.children, result);
}
}
return result;
}
}
Common Questions
"How do I get all content at once?"
Use the /resolved endpoint which returns the complete tree:
curl -H "Authorization: Bearer TOKEN" \
https://your-domain.com/api/v1/curricula/123/resolved
For very large curricula, consider using pagination or the lazy-loading pattern.
"What's the difference between show and resolved?"
| Endpoint | Returns | Use Case |
|---|---|---|
GET /curricula/:id |
Root nodes only | Building lazy-loading UI |
GET /curricula/:id/resolved |
Full tree with inheritance/translations | Export, sync, full render |
"How do I get content in a specific language?"
Add the locale parameter:
curl "https://your-domain.com/api/v1/curricula/123/resolved?locale=es"
"How do I get variant-specific content?"
Add the variant parameter with the variant name:
curl "https://your-domain.com/api/v1/curricula/123/resolved?variant=Texas"
Related Documentation:
- API Reference - Complete endpoint specification
- Authentication - Token management
- Webhooks - Event notifications