9ja Checkr API
JSON HTTP API for NAFDAC registration lookups. Use GET /api/verify/:nafdac with an x-api-key header. Free and API Pro tiers — Pro adds batch verify and product search. Not a government feed — disclaimer.
Overview
Lookups use the same public NAFDAC registration channel the site automates; responses may be served from our cache after the first fetch. All successful responses are JSON with an ok field.
All requests go to: https://api.9jacheckr.xyz
Free tier
Free- 300 API uses / month (single GET verify only)
- 1 API key
- GET /api/verify/:nafdac
- Lower per-window rate limits
- No dashboard metrics
Pro tier
Pro- 50,000 API uses / month (verify rows + successful search)
- Multiple API keys
- POST /api/verify/batch (up to 40 numbers)
- GET /api/products/search (indexed DB)
- Higher per-window rate limits
- Full dashboard metrics
- Commercial use
Authentication
Send your API key on every request as the x-api-keyheader (same as in the dashboard "Example request" snippet).
x-api-key: YOUR_API_KEYQuick start
Create a key under API Keys, then run (replace YOUR_API_KEY):
curl -sS "https://api.9jacheckr.xyz/api/verify/01-5713" \
-H "x-api-key: YOUR_API_KEY"encodeURIComponent on the path segment if the registration contains characters that need encoding.{
"ok": true,
"product": {
"nafdac": "01-5713",
"name": "Example product name",
"category": "Food",
"source": "nafdac-registration",
"manufacturer": "Example manufacturer",
"approvedDate": "2019-04-01T00: 00: 00.000Z",
"expiryDate": "2024-04-01T00: 00: 00.000Z",
"ingredients": []
}
}{
"ok": false,
"code": "NOT_FOUND",
"message": "Product not found for this NAFDAC number",
"nafdac": "99-9999"
}Endpoints
Verify a product
Look up one registration number. Returns ok: true and a product object when found, or 404 with NOT_FOUND when not. Counts as one monthly API use (Free or Pro).
Path
nafdac* | string | NAFDAC registration number (e.g. 01-5713, A1-5645). Pass raw or URL-encoded in the path. |
curl -sS "https://api.9jacheckr.xyz/api/verify/01-5713" \
-H "x-api-key: YOUR_API_KEY"{
"ok": true,
"product": {
"nafdac": "01-5713",
"name": "…",
"category": "Food",
"source": "nafdac-registration",
"manufacturer": "…",
"approvedDate": "2019-04-01T00: 00: 00.000Z",
"expiryDate": "2024-04-01T00: 00: 00.000Z",
"ingredients": ["…"]
}
}{
"ok": false,
"code": "NOT_FOUND",
"message": "Product not found for this NAFDAC number",
"nafdac": "99-9999"
}Batch verify
ProUp to 40 numbers per request. Each row (found or not found) counts as one unit toward the same monthly API cap as single verify. Requires API Pro — same x-api-key header and JSON body { "nafdac": string[] }.
curl -sS -X POST "https://api.9jacheckr.xyz/api/verify/batch" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{"nafdac":["01-5713","04-8127"]}'{
"ok": true,
"results": [
{
"nafdac": "01-5713",
"ok": true,
"product": {
"nafdac": "01-5713",
"name": "…",
"category": "Food",
"source": "nafdac-registration",
"manufacturer": "…",
"approvedDate": null,
"expiryDate": null,
"ingredients": []
}
},
{
"nafdac": "99-9999",
"ok": false,
"code": "NOT_FOUND",
"message": "Product not found for this NAFDAC number"
}
]
}Product search
ProAPI Pro only. Query our indexed product database (built from registrations we have seen) — not a live NAFDAC request per search. Multi-word q: every token must match somewhere (any field). Optional limit 1–50 (default 20). Each successful search response counts one monthly unit. Minimum query length: 2 characters.
Query parameters
q* | string | Search string (≥2 chars). Tokenized; all tokens must match. |
limit | number | 1–50, default 20. |
curl -sS "https://api.9jacheckr.xyz/api/products/search?q=sardine&limit=10" \
-H "x-api-key: YOUR_API_KEY"{
"ok": true,
"results": [
{
"nafdac": "01-5713",
"name": "TITUS SARDINE IN VEGETABLE OIL",
"category": "Food",
"manufacturer": "UNIMER S.A"
}
]
}Photo verify (Telegram)
Label photo OCR and extraction are not exposed on this HTTP API. They are available through Telegram Bot Pro via @NaijaCheckrBot (/upgrade).
JavaScript SDK
Official @9jacheckr/sdk wraps the same REST endpoints with typed results. Uses global fetch (Node 18+). The client always talks to https://api.9jacheckr.xyz — for a local API, use the HTTP examples above.
npm install @9jacheckr/sdkAll methods return a Promise that resolves (no throw for API/network errors). Branch on result.ok. Per-request timeout is about 25 seconds.
CheckrClient
import { CheckrClient } from '@9jacheckr/sdk';
const client = new CheckrClient({
apiKey: process.env.CHECKR_API_KEY!,
});
const result = await client.verify('01-5713');
if (result.ok) {
console.log(result.product.name, result.product.manufacturer);
} else {
console.error(result.code, result.message);
}Methods
verify(nafdac) | Promise<VerifyResult> | GET /api/verify/:nafdac — Free or Pro. Empty input returns ok: false with INVALID_NAFDAC (request not sent). |
verifyBatch(numbers) | Promise<BatchVerifyResult> | POST /api/verify/batch — API Pro. Up to 40 strings per call; each row counts toward monthly usage. Empty list → INVALID_BODY. |
searchProducts(query, { limit? }) | Promise<SearchResult> | GET /api/products/search — API Pro. query ≥ 2 characters; limit 1–50 (default 20). Each successful response counts one monthly unit. |
const batch = await client.verifyBatch(['01-5713', '04-8127']);
if (batch.ok) {
for (const row of batch.results) {
if (row.ok) console.log(row.nafdac, row.product.name);
else console.log(row.nafdac, row.code, row.message);
}
} else {
console.error(batch.code, batch.message);
}const search = await client.searchProducts('sardine', {
limit: 10,
});
if (search.ok) {
for (const hit of search.results) {
console.log(hit.nafdac, hit.name, hit.manufacturer);
}
} else {
console.error(search.code, search.message);
}Exported types include Product, VerifyResult, BatchVerifyResult, SearchResult, and row/hit types. Extra SDK-only code values include INVALID_RESPONSE, TIMEOUT, and NETWORK_ERROR.
packages/sdk (see npm package @9jacheckr/sdk).Rate limits
Two layers: a monthly usage cap per account (Free vs API Pro) and a sliding-window limiter on verify, batch, and search routes. Over the monthly cap you get 429 with PLAN_QUOTA_EXCEEDED. Over the window limit you get 429 with RATE_LIMITED.
| Plan | Monthly usage cap | ~Per 15 min / API key |
|---|---|---|
| Free | 300 units — single GET verify only (no batch/search) | 45 req / 15 min |
| Pro | 50,000 units — each verify row + each successful search | 220 req / 15 min |
Successful responses may include RateLimit-* style headers from the sliding limiter (draft standard). Monthly remaining usage is shown in your dashboard, not only in response headers.
Error codes
All errors follow a consistent shape. The HTTP status indicates the category; the code field is machine-readable.
{
"ok": false,
"code": "NOT_FOUND",
"message": "Product not found for this NAFDAC number",
"nafdac": "99-9999"
}| Status | Code | Meaning |
|---|---|---|
| 400 | INVALID_NAFDAC | Missing or empty path parameter |
| 400 | INVALID_BODY | Batch: body must include nafdac string array |
| 400 | INVALID_QUERY | Search: q too short or no valid tokens |
| 401 | UNAUTHORIZED | Missing or invalid API key |
| 403 | FEATURE_REQUIRES_PRO | Batch or search needs API Pro |
| 403 | KEY_PLAN_DISABLED | Non-primary key on Free plan |
| 404 | NOT_FOUND | No product for this NAFDAC number (verify) |
| 429 | PLAN_QUOTA_EXCEEDED | Monthly API cap reached |
| 429 | RATE_LIMITED | Sliding window limit hit |
| 500 | INTERNAL_ERROR | Unexpected server error |
Code examples
JavaScript / Node.js
const url = `https://api.9jacheckr.xyz/api/verify/` + encodeURIComponent("01-5713");
const response = await fetch(url, {
headers: { "x-api-key": "YOUR_API_KEY" },
});
const data = await response.json();
if (data.ok && data.product) {
console.log(data.product.name);
}Python
import urllib.parse
import requests
nafdac = urllib.parse.quote("01-5713", safe="")
r = requests.get(
f"https://api.9jacheckr.xyz/api/verify/{nafdac}",
headers={"x-api-key": "YOUR_API_KEY"},
timeout=30,
)
data = r.json()
if data.get("ok") and data.get("product"):
print(data["product"]["name"])TypeScript — typed helper
const API_KEY = process.env.CHECKR_API_KEY!;
const BASE = "https://api.9jacheckr.xyz";
export type Product = {
nafdac: string;
name: string;
category: string;
source: string;
manufacturer: string;
approvedDate: string | null;
expiryDate: string | null;
ingredients: string[];
};
export type VerifySuccess = { ok: true; product: Product };
export type VerifyError = {
ok: false;
code: string;
message: string;
nafdac?: string;
};
export async function verifyNafdac(
nafdac: string,
): Promise<VerifySuccess | VerifyError> {
const path = encodeURIComponent(nafdac);
const res = await fetch(`${BASE}/api/verify/${path}`, {
headers: { "x-api-key": API_KEY },
});
return res.json() as Promise<VerifySuccess | VerifyError>;
}Upgrade to Pro
Need more than 300 lookups per month, multiple keys, batch verify, or product search? Upgrade to API Pro from your dashboard. Billed at ₦10,000/month. Photo verify stays on Telegram Bot Pro only.
API Pro
₦10,000/ month
- 50,000 API uses / month
- Multiple API keys
- POST /api/verify/batch
- GET /api/products/search
- Higher sliding-window limits
- Dashboard metrics
- Commercial use