The DETECK API
Everything the DETECK website does with serials is also available to your own systems: a mobile app, pharmacy software, a manufacturer's ERP. The API speaks JSON over HTTPS at /api/v1. The machine-readable description is at /api/v1/openapi.json (OpenAPI 3.1).
Keys and limits
- Checking serials needs no key: up to 300 requests an hour from one IP address. A key raises the limit.
- Manufacturers create keys in the workspace (API and webhooks). Pharmacies, hospitals and distributors create keys in their portal. Partner apps get a key from DETECK.
- Send the key in a header: Authorization: Bearer dtk_live_…. Each key has its own limit per minute (120 by default). Too many requests get 429 with a Retry-After header.
- Keys are shown once. If one leaks, revoke it straight away and create a new one.
Errors
Every error has the same shape, with an HTTP status: { "error": { "code": "missing_scope", "message": "…" } }. The message is written for people; the code is stable for programs.
Check a serial
curl -X POST https://deteck.ng/api/v1/checks \
-H "Content-Type: application/json" \
-d '{"serial": "7K4M9QX2TB8D"}' Or send a scanned DataMatrix code as { "code": "(01)…(21)…" }. The answer:
{
"id": "Xb3…",
"result": {
"code": "VALID",
"flags": [],
"serialDisplay": "…TB8D",
"manufacturer": { "name": "Northbridge Pharma Ltd", "state": "Ogun", "town": "Ota" },
"product": { "brandName": "Curavyn", "strength": "500 mg", "nafdacRegNo": "A4-0000", … },
"batch": { "lot": "NB-2611", "expiryDate": "2028-04-30", "status": "active", … },
"journeyAvailable": true
},
"summary": { "title": "VALID", "message": "This serial matches Northbridge Pharma Ltd's records for Curavyn 500 mg." },
"links": { "journey": "/api/v1/checks/Xb3…/journey" }
} summary is DETECK's own wording for people. Ask for another language with Accept-Language or ?lang=: pcm (Nigerian Pidgin), ha (Hausa), yo (Yorùbá) or ig (Igbo).
result.code is always one of these. Show the flags too: an expired or mismatched pack is still VALID with a flag.
| code | Meaning |
|---|---|
| VALID | The serial is in a live manufacturer's records. Flags: EXPIRED, LOT_MISMATCH, EXPIRY_MISMATCH |
| RECALLED | The batch was recalled or withdrawn (see batch.status and batch.statusReason) |
| UNRECOGNISED | No live record of this serial |
| NOT_COVERED | A scanned product whose manufacturer isn't on DETECK yet |
| CANT_CONFIRM | The manufacturer or the drug is paused on DETECK |
| NEEDS_BATCH | The serial exists on more than one product; check by scanned code instead |
Never describe a VALID result as "genuine" or "safe" to people: it means the serial matches the manufacturer's records. The summary field carries DETECK's own wording.
Manufacturers: send data
Send drugs (products), batches and serials (batches_serials) or supply chain records (distribution) as rows. They are checked exactly like an uploaded spreadsheet. With "commit": false the rows are only checked.
curl -X POST https://deteck.ng/api/v1/manufacturer/imports \
-H "Authorization: Bearer dtk_live_ab12cd34_…" \
-H "Content-Type: application/json" \
-d '{
"kind": "batches_serials",
"commit": true,
"rows": [
{ "gtin": "09506000990019", "lot": "NB-2611", "manufactureDate": "2026-03-01",
"expiryDate": "2028-04-30", "serial": "7K4M9QX2TB8D" }
]
}' The answer is 202 with an import id. Read GET /api/v1/manufacturer/imports/{id} until its status is committed (or ready when only checking). It lists the counts and the first 100 problem rows with reasons. Sending exactly the same rows again returns the first import, so you can retry without importing twice. Up to 50,000 rows a request. Recall or withdraw a batch with POST /api/v1/manufacturer/batch-status.
Pharmacies and distributors: check a delivery
POST /api/v1/business/checks with { "codes": [...] } checks up to 500 at once. To record what you received:
curl -X POST https://deteck.ng/api/v1/business/receipts \
-H "Authorization: Bearer dtk_live_…" \
-H "Content-Type: application/json" \
-d '{ "codes": ["7K4M9QX2TB8D", "H2R5W8C1N6PZ"], "receivedOn": "2026-10-26",
"supplier": "Lakeside Medical Supply" }' Webhooks
Manufacturers can ask DETECK to call their own HTTPS address when something happens: an import finishes, a go-live decision, a new drug reviewed, a batch recalled, a serial checked unusually often, a suspected fake reported, or a pharmacy confirming receipt. Each delivery is a POST with a JSON body: { "id", "event", "createdAt", "data" }, and these headers: x-deteck-event, x-deteck-delivery, x-deteck-timestamp and x-deteck-signature. Answer with any 2xx status; anything else is retried for about 2 hours with growing gaps.
Check every delivery's signature with your signing secret:
import { createHmac, timingSafeEqual } from 'node:crypto';
// In your webhook handler, with the raw request body as a string:
function fromDeteck(body, headers, secret) {
const timestamp = headers['x-deteck-timestamp'];
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; // older than 5 minutes
const expected = 'v1=' + createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex');
const given = headers['x-deteck-signature'] ?? '';
return expected.length === given.length && timingSafeEqual(Buffer.from(expected), Buffer.from(given));
}