Best Practices
Recommended patterns for building reliable, high-performance applications with the PaperStack API.
Best Practices
Caching
PaperStack API responses include Cache-Control headers:
| Resource | Cache Duration | Header |
|---|---|---|
| Question data (JSON) | 24 hours | public, max-age=86400 |
| Diagram images | 7 days | public, max-age=604800, immutable |
| Discovery endpoints | 1 hour | public, 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 == expectedRate 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 decoratorWhen 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_dataNote: 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.