Skip to content
JEE Main & JEE Advanced datasets — Coming Soon

Best Practices

Recommended patterns for building reliable, high-performance applications with the PaperStack API.

Best Practices

Caching

PaperStack API responses include Cache-Control headers:

ResourceCache DurationHeader
Question data (JSON)24 hourspublic, max-age=86400
Diagram images7 dayspublic, max-age=604800, immutable
Discovery endpoints1 hourpublic, max-age=3600

Client-Side Caching

For production applications, cache responses locally to reduce API calls and improve latency:

import requests
from functools import lru_cache

@lru_cache(maxsize=128)
def get_paper(exam, year, shift):
    headers = {"Authorization": "Bearer ps_xxxxx"}
    url = f"https://api.paperstack.qzz.io/{exam}/{year}/{shift}/paper.json"
    return requests.get(url, headers=headers).json()
const cache = new Map();

async function getPaper(exam, year, shift, apiKey) {
  const key = `${exam}/${year}/${shift}`;
  if (cache.has(key)) return cache.get(key);
  const res = await fetch(`https://api.paperstack.qzz.io/${key}/paper.json`, {
    headers: { Authorization: `Bearer ${apiKey}` },
  });
  const data = await res.json();
  cache.set(key, data);
  setTimeout(() => cache.delete(key), 86400000); // 24h TTL
  return data;
}

Data Integrity (Checksums)

Every paper response includes a checksum field (SHA-256). Verify data integrity on critical paths:

import hashlib

def verify_checksum(data):
    expected = data["checksum"]
    actual = hashlib.sha256(
        json.dumps(data, sort_keys=True).encode()
    ).hexdigest()
    return actual == expected

Rate Limiting

Each plan has a requests-per-second limit. Respect it by implementing client-side throttling:

import time
from functools import wraps

def rate_limit(max_per_second):
    min_interval = 1.0 / max_per_second
    last_call = [0.0]
    
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            elapsed = time.time() - last_call[0]
            if elapsed < min_interval:
                time.sleep(min_interval - elapsed)
            result = func(*args, **kwargs)
            last_call[0] = time.time()
            return result
        return wrapper
    return decorator

When you hit a rate limit, the API returns 429 Too Many Requests. Implement exponential backoff:

import time

def fetch_with_retry(url, headers, max_retries=3):
    for attempt in range(max_retries):
        resp = requests.get(url, headers=headers)
        if resp.status_code == 429:
            wait = (2 ** attempt) * 1  # 1s, 2s, 4s
            time.sleep(wait)
            continue
        resp.raise_for_status()
        return resp.json()

Error Handling

Always check the HTTP status code before parsing the response body:

resp = requests.get(url, headers=headers)
if resp.status_code == 200:
    return resp.json()
elif resp.status_code == 401:
    # Key invalid or expired — prompt user to regenerate
    raise Exception("API key invalid. Create a new one in your dashboard.")
elif resp.status_code == 429:
    # Rate limited — back off
    raise Exception("Rate limit exceeded. Slow down requests.")
elif resp.status_code == 404:
    # Paper/shift not found — check your discovery query
    return None
else:
    resp.raise_for_status()

Bulk Downloads

To download all papers for an exam, first discover available shifts, then fetch each one:

import requests

def download_all_papers(exam, api_key, max_concurrent=5):
    headers = {"Authorization": f"Bearer {api_key}"}
    base = "https://api.paperstack.qzz.io"
    
    # Discover
    years = requests.get(f"{base}/{exam}", headers=headers).json()["years"]
    
    all_data = []
    for year in years[:3]:  # Limit to recent years
        shifts = requests.get(f"{base}/{exam}/{year}").json()["shifts"]
        for shift_info in shifts:
            shift = shift_info["shift"]
            resp = requests.get(
                f"{base}/{exam}/{year}/{shift}/paper.json",
                headers=headers
            )
            if resp.status_code == 200:
                all_data.append(resp.json())
    return all_data

Note: Respect rate limits during bulk operations. Space requests at least 1/rate_limit seconds apart.

Diagram Handling

Questions with hasDiagram: true may include a diagrams array with image references:

{
  "hasDiagram": true,
  "diagrams": [
    { "file": "diagrams/q013-img-7.jpeg", "label": "img-7.jpeg" }
  ]
}

Fetch diagrams from the same base path as the question data:

diagram_url = f"{base}/{exam}/{year}/{shift}/{diagram['file']}"

Diagram URLs are immutable and cached for 7 days. They're safe to hotlink.

Was this page helpful?

On this page