NextSchoolNextSchool Data API

Token Expired

Handle expired access tokens by detecting errors and refreshing automatically

Handling Expired Access Tokens

Access tokens expire after 15 minutes. When you make a request with an expired token, the API returns an UNAUTHENTICATED error:

{
  "errors": [
    {
      "message": "Authentication required",
      "extensions": { "code": "UNAUTHENTICATED" }
    }
  ]
}

The solution is to detect the error, refresh the token, and retry the request.

#!/bin/bash
API_URL="https://data.nextschool.io/"

# Assume these are set from a previous login
# ACCESS_TOKEN="eyJhbGciOiJIUzI1NiIs..."
# REFRESH_TOKEN="a1b2c3d4-e5f6-7890-abcd-ef1234567890"

QUERY='{ students(limit: 5) { id fullName classroomName } }'

# Step 1: Make a request with the (possibly expired) access token
RESPONSE=$(curl -s -X POST "$API_URL" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -d "{\"query\": \"$QUERY\"}")

# Step 2: Check if we got UNAUTHENTICATED
CODE=$(echo "$RESPONSE" | jq -r '.errors[0].extensions.code // empty')

if [ "$CODE" = "UNAUTHENTICATED" ]; then
  echo "Token expired, refreshing..."

  # Step 3: Refresh the access token
  REFRESH_RESPONSE=$(curl -s -X POST "$API_URL" \
    -H "Content-Type: application/json" \
    -d "{
      \"query\": \"mutation { refreshToken(refreshToken: \\\"$REFRESH_TOKEN\\\") { accessToken expiresIn } }\"
    }")

  ACCESS_TOKEN=$(echo "$REFRESH_RESPONSE" | jq -r '.data.refreshToken.accessToken')

  if [ "$ACCESS_TOKEN" = "null" ] || [ -z "$ACCESS_TOKEN" ]; then
    echo "Refresh token also expired. Please login again."
    exit 1
  fi

  echo "Token refreshed successfully."

  # Step 4: Retry the original request with the new token
  RESPONSE=$(curl -s -X POST "$API_URL" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $ACCESS_TOKEN" \
    -d "{\"query\": \"$QUERY\"}")
fi

echo "$RESPONSE" | jq '.data'
const API_URL = 'https://data.nextschool.io/';

let accessToken = '...';   // from login
let refreshToken = '...';  // from login

async function fetchWithRefresh(query, variables = {}) {
  // Step 1: Try the request
  let res = await fetch(API_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${accessToken}`
    },
    body: JSON.stringify({ query, variables })
  });
  let result = await res.json();

  // Step 2: Check for expired token
  if (result.errors?.[0]?.extensions?.code === 'UNAUTHENTICATED') {
    console.log('Token expired, refreshing...');

    // Step 3: Refresh the token
    const refreshRes = await fetch(API_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        query: `mutation { refreshToken(refreshToken: "${refreshToken}") {
          accessToken expiresIn
        }}`
      })
    });
    const refreshResult = await refreshRes.json();

    if (refreshResult.errors) {
      throw new Error('Refresh token expired. Please login again.');
    }

    accessToken = refreshResult.data.refreshToken.accessToken;
    console.log('Token refreshed successfully.');

    // Step 4: Retry with the new token
    res = await fetch(API_URL, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${accessToken}`
      },
      body: JSON.stringify({ query, variables })
    });
    result = await res.json();
  }

  return result.data;
}

// Usage
const students = await fetchWithRefresh(
  '{ students(limit: 5) { id fullName classroomName } }'
);
console.log(students);
import requests

API_URL = 'https://data.nextschool.io/'

access_token = '...'   # from login
refresh_token = '...'  # from login

def fetch_with_refresh(query: str, variables: dict = None):
    global access_token

    # Step 1: Try the request
    resp = requests.post(API_URL, headers={
        'Content-Type': 'application/json',
        'Authorization': f'Bearer {access_token}',
    }, json={'query': query, 'variables': variables or {}})
    result = resp.json()

    # Step 2: Check for expired token
    errors = result.get('errors', [])
    if errors and errors[0].get('extensions', {}).get('code') == 'UNAUTHENTICATED':
        print('Token expired, refreshing...')

        # Step 3: Refresh the token
        refresh_resp = requests.post(API_URL, json={
            'query': f'''mutation {{
                refreshToken(refreshToken: "{refresh_token}") {{
                    accessToken expiresIn
                }}
            }}'''
        })
        refresh_result = refresh_resp.json()

        if 'errors' in refresh_result:
            raise Exception('Refresh token expired. Please login again.')

        access_token = refresh_result['data']['refreshToken']['accessToken']
        print('Token refreshed successfully.')

        # Step 4: Retry with the new token
        resp = requests.post(API_URL, headers={
            'Content-Type': 'application/json',
            'Authorization': f'Bearer {access_token}',
        }, json={'query': query, 'variables': variables or {}})
        result = resp.json()

    return result['data']

# Usage
students = fetch_with_refresh(
    '{ students(limit: 5) { id fullName classroomName } }'
)
print(students)
<?php
$apiUrl = 'https://data.nextschool.io/';
$accessToken = '...';   // from login
$refreshToken = '...';  // from login

function fetchWithRefresh(string $query, array $variables = []): array {
    global $apiUrl, $accessToken, $refreshToken;

    // Step 1: Try the request
    $result = graphqlRequest($query, $accessToken);

    // Step 2: Check for expired token
    $code = $result['errors'][0]['extensions']['code'] ?? null;
    if ($code === 'UNAUTHENTICATED') {
        echo "Token expired, refreshing...\n";

        // Step 3: Refresh the token
        $refreshResult = graphqlRequest(
            "mutation { refreshToken(refreshToken: \"$refreshToken\") { accessToken expiresIn } }"
        );

        if (isset($refreshResult['errors'])) {
            throw new \RuntimeException('Refresh token expired. Please login again.');
        }

        $accessToken = $refreshResult['data']['refreshToken']['accessToken'];
        echo "Token refreshed successfully.\n";

        // Step 4: Retry with the new token
        $result = graphqlRequest($query, $accessToken);
    }

    return $result['data'];
}

function graphqlRequest(string $query, ?string $token = null): array {
    global $apiUrl;
    $ch = curl_init($apiUrl);
    $headers = ['Content-Type: application/json'];
    if ($token) {
        $headers[] = "Authorization: Bearer $token";
    }
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_POSTFIELDS => json_encode(['query' => $query]),
        CURLOPT_RETURNTRANSFER => true,
    ]);
    $data = json_decode(curl_exec($ch), true);
    curl_close($ch);
    return $data;
}

// Usage
$students = fetchWithRefresh('{ students(limit: 5) { id fullName classroomName } }');
print_r($students);
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
)

const apiURL = "https://data.nextschool.io/"

var accessToken = "..."  // from login
var refreshToken = "..." // from login

func fetchWithRefresh(query string) (map[string]any, error) {
	// Step 1: Try the request
	result, err := graphqlRequest(query, accessToken)
	if err != nil {
		return nil, err
	}

	// Step 2: Check for expired token
	if errors, ok := result["errors"].([]any); ok && len(errors) > 0 {
		errMap := errors[0].(map[string]any)
		if ext, ok := errMap["extensions"].(map[string]any); ok && ext["code"] == "UNAUTHENTICATED" {
			fmt.Println("Token expired, refreshing...")

			// Step 3: Refresh the token
			refreshQuery := fmt.Sprintf(`mutation {
				refreshToken(refreshToken: "%s") { accessToken expiresIn }
			}`, refreshToken)
			refreshResult, err := graphqlRequest(refreshQuery, "")
			if err != nil {
				return nil, err
			}
			if _, hasErr := refreshResult["errors"]; hasErr {
				return nil, fmt.Errorf("refresh token expired, please login again")
			}

			rt := refreshResult["data"].(map[string]any)["refreshToken"].(map[string]any)
			accessToken = rt["accessToken"].(string)
			fmt.Println("Token refreshed successfully.")

			// Step 4: Retry with the new token
			result, err = graphqlRequest(query, accessToken)
			if err != nil {
				return nil, err
			}
		}
	}

	return result["data"].(map[string]any), nil
}

func graphqlRequest(query, token string) (map[string]any, error) {
	body, _ := json.Marshal(map[string]string{"query": query})
	req, _ := http.NewRequest("POST", apiURL, bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var result map[string]any
	json.NewDecoder(resp.Body).Decode(&result)
	return result, nil
}

func main() {
	students, err := fetchWithRefresh(`{ students(limit: 5) { id fullName classroomName } }`)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println(students)
}
use reqwest::Client;
use serde_json::{json, Value};

const API_URL: &str = "https://data.nextschool.io/";

struct TokenStore {
    access_token: String,
    refresh_token: String,
}

async fn fetch_with_refresh(
    client: &Client,
    tokens: &mut TokenStore,
    query: &str,
) -> Result<Value, Box<dyn std::error::Error>> {
    // Step 1: Try the request
    let mut result = graphql_request(client, query, &tokens.access_token).await?;

    // Step 2: Check for expired token
    if let Some(errors) = result["errors"].as_array() {
        if let Some(first) = errors.first() {
            let code = first["extensions"]["code"].as_str().unwrap_or("");
            if code == "UNAUTHENTICATED" {
                println!("Token expired, refreshing...");

                // Step 3: Refresh the token
                let refresh_query = format!(
                    r#"mutation {{ refreshToken(refreshToken: "{}") {{ accessToken expiresIn }} }}"#,
                    tokens.refresh_token
                );
                let refresh_result = graphql_request(client, &refresh_query, "").await?;

                if refresh_result["errors"].is_array() {
                    return Err("Refresh token expired. Please login again.".into());
                }

                tokens.access_token = refresh_result["data"]["refreshToken"]["accessToken"]
                    .as_str()
                    .unwrap()
                    .to_string();
                println!("Token refreshed successfully.");

                // Step 4: Retry with the new token
                result = graphql_request(client, query, &tokens.access_token).await?;
            }
        }
    }

    Ok(result["data"].clone())
}

async fn graphql_request(
    client: &Client,
    query: &str,
    token: &str,
) -> Result<Value, Box<dyn std::error::Error>> {
    let mut req = client.post(API_URL)
        .header("Content-Type", "application/json");
    if !token.is_empty() {
        req = req.header("Authorization", format!("Bearer {}", token));
    }
    let resp: Value = req
        .json(&json!({ "query": query }))
        .send().await?
        .json().await?;
    Ok(resp)
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new();
    let mut tokens = TokenStore {
        access_token: "...".into(),   // from login
        refresh_token: "...".into(),  // from login
    };

    let students = fetch_with_refresh(
        &client,
        &mut tokens,
        r#"{ students(limit: 5) { id fullName classroomName } }"#,
    ).await?;

    println!("{:#}", students);
    Ok(())
}

For a complete API client class that handles token refresh automatically, see the Full Client example.

On this page