API Examples & Patterns

Practical examples and integration patterns for the CurryCMS API.

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:

  1. Navigate to Settings > API Tokens
  2. Click Generate New Token
  3. 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

Was this page helpful? |