Token Management for Content APIs
Use this page when you are implementing the manual Client Credentials flow and need the correct token lifecycle behavior.
How to cache access tokens, when to re-request them, how to handle 401 Unauthorized, and how to prevent multiple concurrent requests from fetching the same token repeatedly.
Content API Token Lifecycle
For Quran Foundation Content APIs:
- Access tokens are valid for
3600seconds. - Client Credentials does not use
refresh_token. - When the token is close to expiry, request a new token from the same
/oauth2/tokenendpoint. - A good default is to re-request the token about
30seconds before expiry.
Recommended Cache Behavior
Store the token together with its expiry and reuse it until it is almost expired.
Language-agnostic flow:
if cached token exists and expires more than 30 seconds from now:
return cached token
if another request is already fetching a new token:
wait for that in-flight request
request a new token from /oauth2/token
store access_token and expires_at
return the new token
This keeps your integration fast and avoids unnecessary token requests.
Expand for Python and Node.js cache examples
Python Example (requests + lock)
import os
import threading
import time
import requests
from requests.auth import HTTPBasicAuth
class QfTokenCache:
def __init__(self):
self._lock = threading.Lock()
self._token = None
self._expires_at = 0
env = os.getenv("QF_ENV", "prelive")
if env not in ("prelive", "production"):
raise ValueError(
f"Invalid QF_ENV value: {env!r}. Expected 'prelive' or 'production'."
)
auth_base_by_env = {
"production": "https://oauth2.quran.foundation",
"prelive": "https://prelive-oauth2.quran.foundation",
}
api_base_by_env = {
"production": "https://apis.quran.foundation",
"prelive": "https://apis-prelive.quran.foundation",
}
self.auth_base_url = auth_base_by_env[env]
self.api_base_url = api_base_by_env[env]
def clear(self):
with self._lock:
self._token = None
self._expires_at = 0
def get_access_token(self):
now = time.time()
if self._token and now < self._expires_at - 30:
return self._token
with self._lock:
now = time.time()
if self._token and now < self._expires_at - 30:
return self._token
response = requests.post(
f"{self.auth_base_url}/oauth2/token",
auth=HTTPBasicAuth(
os.environ["QF_CLIENT_ID"],
os.environ["QF_CLIENT_SECRET"],
),
headers={"Content-Type": "application/x-www-form-urlencoded"},
data={
"grant_type": "client_credentials",
"scope": "content",
},
timeout=30,
)
response.raise_for_status()
token = response.json()
self._token = token["access_token"]
self._expires_at = time.time() + token["expires_in"]
return self._token
Node.js Example (fetch + shared promise)
This example assumes Node 18+ or another runtime with a global fetch implementation.
const env = process.env.QF_ENV ?? "prelive";
if (!["prelive", "production"].includes(env)) {
throw new Error("QF_ENV must be 'prelive' or 'production'");
}
const authBaseByEnv = {
production: "https://oauth2.quran.foundation",
prelive: "https://prelive-oauth2.quran.foundation",
};
const apiBaseByEnv = {
production: "https://apis.quran.foundation",
prelive: "https://apis-prelive.quran.foundation",
};
const authBaseUrl = authBaseByEnv[env];
const apiBaseUrl = apiBaseByEnv[env];
let cachedToken = null;
let expiresAt = 0;
let inflightTokenPromise = null;
async function fetchToken() {
const basicAuth = Buffer.from(
`${process.env.QF_CLIENT_ID}:${process.env.QF_CLIENT_SECRET}`,
).toString("base64");
const response = await fetch(`${authBaseUrl}/oauth2/token`, {
method: "POST",
headers: {
Authorization: `Basic ${basicAuth}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "client_credentials",
scope: "content",
}),
});
if (!response.ok) {
throw new Error(`Token request failed: ${response.status}`);
}
const token = await response.json();
cachedToken = token.access_token;
expiresAt = Date.now() + token.expires_in * 1000;
return cachedToken;
}
async function getAccessToken() {
if (cachedToken && Date.now() < expiresAt - 30_000) {
return cachedToken;
}
if (!inflightTokenPromise) {
inflightTokenPromise = fetchToken().finally(() => {
inflightTokenPromise = null;
});
}
return inflightTokenPromise;
}
function clearToken() {
cachedToken = null;
expiresAt = 0;
}
Retry Once on 401
If a Content API request returns 401 Unauthorized, treat the token as expired or invalid:
- Clear the cached token.
- Request a fresh token from
/oauth2/token. - Retry the failed API request once.
- If it still fails, surface the error instead of looping.
This is a re-request, not a refresh. Client Credentials has no refresh token in this flow.
Expand for Python and Node.js 401 retry examples
Python Example (401 retry once)
cache = QfTokenCache()
def get_json(path):
token = cache.get_access_token()
response = requests.get(
f"{cache.api_base_url}{path}",
headers={
"x-auth-token": token,
"x-client-id": os.environ["QF_CLIENT_ID"],
},
timeout=30,
)
if response.status_code == 401:
cache.clear()
token = cache.get_access_token()
response = requests.get(
f"{cache.api_base_url}{path}",
headers={
"x-auth-token": token,
"x-client-id": os.environ["QF_CLIENT_ID"],
},
timeout=30,
)
response.raise_for_status()
return response.json()
Node.js Example (401 retry once)
async function getJson(path) {
let token = await getAccessToken();
let response = await fetch(`${apiBaseUrl}${path}`, {
headers: {
"x-auth-token": token,
"x-client-id": process.env.QF_CLIENT_ID,
},
});
if (response.status === 401) {
clearToken();
token = await getAccessToken();
response = await fetch(`${apiBaseUrl}${path}`, {
headers: {
"x-auth-token": token,
"x-client-id": process.env.QF_CLIENT_ID,
},
});
}
if (!response.ok) {
throw new Error(`Content API request failed: ${response.status}`);
}
return response.json();
}
Stampede Prevention
Without coordination, many requests arriving at the same time can all decide the token is expired and all request a new token at once.
Prevent that with a single in-flight token request:
- JavaScript or TypeScript: keep one shared pending promise.
- Python: use a lock around the token fetch.
- Other runtimes: use whichever synchronization primitive ensures only one token request happens at a time.
The important behavior is that one request fetches the new token while the others wait for it.
What Not to Do
- Do not request a new token for every API call.
- Do not store a token forever without checking
expires_in. - Do not retry
401in a loop. - Do not mix caches between
preliveandproduction. - Do not log
client_secretoraccess_token.
AI Handoff Prompt
Use this prompt when you want an AI coding tool to add safe token lifecycle handling to an existing backend integration:
Expand AI handoff prompt
Implement Quran Foundation Content API token management for a backend Client Credentials integration.
Requirements
- Cache access_token together with its expiry time.
- Re-request a token about 30 seconds before expiry.
- Do not implement refresh_token logic.
- Ensure only one token request is in flight at a time.
- On a 401 from the Content API, clear the cached token, re-request once, and retry once.
- Do not retry in a loop.
Documentation to follow
- Token management: https://api-docs.quran.foundation/docs/quickstart/token-management
- Manual authentication: https://api-docs.quran.foundation/docs/quickstart/manual-authentication
- First API call: https://api-docs.quran.foundation/docs/quickstart/first-api-call
Next Step
Continue with first API call to send the required headers and verify the integration with GET /content/api/v4/chapters.