Developers · Partner integration
Inter-Agency Verification API
The authenticated, signed REST API for partner agencies — WAEC, NECO, JAMB, federal ministries, and approved employers — to verify a Nigerian teacher’s registration, certificate, and license status, with webhook callbacks for status changes.
Before you start
This API is restricted to partner agencies onboarded by TRCN — examination bodies (WAEC, NECO, JAMB), federal ministries, and approved employers. Provisioning is handled by TRCN; this guide assumes you have already received:
- A partner ID (your audience, e.g. waec)
- An API key (and, for federal partners, an mTLS client certificate)
- A set of scopes and allowed purposes that gate which endpoints you may call and why
- A webhook signing secret (whsec_…) if you subscribed to status-change callbacks
Treat the API key and signing secret as production secrets. If either is exposed, contact TRCN to rotate it immediately.
Quick reference
Base URL
https://api.trcn.gov.ng/v1/inter-agency
Authentication
Bearer API key, x-trcn-api-key, or mTLS
Response format
Signed JSON envelope (RS256 JWS)
Rate limit
Per-partner, per-minute + per-day
Status
JWKS
/v1/inter-agency/.well-known/jwks.json
Authentication
Every request (except the public /health and /.well-known/jwks.json) must authenticate. Three methods are accepted, checked in this order:
- mTLS client certificate — federal partners present a certificate at the TLS layer (the recommended posture for this data class). Resolved by certificate fingerprint.
- Bearer API key —
Authorization: Bearer <key> - Header API key —
x-trcn-api-key: <key>
A request with no valid credential returns 401 Unauthorized. Even with mTLS, continue to verify the JWS signature on every response — defence in depth.
Scopes and purpose-of-use
Each endpoint requires a scope on your partner record. The eligibility and batch endpoints additionally require a purpose, and that purpose must be in your allowed purposes. Calling outside your grants returns 403 Forbidden — request additional scopes/purposes from TRCN rather than working around them.
Scopes → endpoints
| Scope | Endpoint |
|---|---|
| eligibility | POST /eligibility-check |
| lookup_by_reg_no | GET /lookup/by-reg-no/{regNo} |
| lookup_by_tid | GET /lookup/by-tid/{tId} |
| lookup_by_nin | POST /lookup/by-nin |
| batch | POST /batch |
Purpose-of-use values
Pass one of these as purpose on eligibility and batch checks. You will be granted only the purposes relevant to your agency.
WAEC_INVIGILATOR_ELIGIBILITYWAEC_EXAMINER_CREDENTIAL_CHECKNECO_INVIGILATOR_ELIGIBILITYNECO_EXAMINER_CREDENTIAL_CHECKJAMB_CBT_STAFF_VERIFICATIONJAMB_INVIGILATOR_ELIGIBILITYJAMB_ENDORSEMENT_ELIGIBILITYSCHOOL_EMPLOYMENT_VERIFICATIONEMPLOYER_BACKGROUND_CHECKGENERAL_PUBLIC_VERIFICATIONQuick start
A first eligibility check. Set TRCN_API_KEY in your environment, then:
curl -X POST "https://api.trcn.gov.ng/v1/inter-agency/eligibility-check" \
-H "Authorization: Bearer $TRCN_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 2f1c9a7e-checker-001" \
-d '{
"identifier": { "regNo": "TRCN/ABJ/2024/123456" },
"purpose": "WAEC_INVIGILATOR_ELIGIBILITY",
"context": { "examBatch": "WASSCE-2026-MAY", "correlationId": "req-8842" }
}'The response is a signed envelope (see below). Two response headers echo the identifiers for logging: X-Verification-Id and X-Signature-Kid.
Endpoints
/v1/inter-agency/eligibility-checkResolves a teacher and evaluates eligibility against the supplied purpose. Body:
identifier— one ofregNo,tId, orninpurpose— a purpose-of-use value (required)context— optionalexamBatch,employerName,correlationId
Supports Idempotency-Key.
/v1/inter-agency/lookup/by-reg-no/{regNo}Status lookup by TRCN registration number (URL-encode the slashes).
curl "https://api.trcn.gov.ng/v1/inter-agency/lookup/by-reg-no/TRCN%2FABJ%2F2024%2F123456" \
-H "x-trcn-api-key: $TRCN_API_KEY"/v1/inter-agency/lookup/by-tid/{tId}Status lookup by numeric Teacher ID. Non-numeric tId returns 400.
/v1/inter-agency/lookup/by-ninStatus lookup by 11-digit NIN. The NIN is sent in the request body, never the URL, so it never lands in access logs.
# NIN goes in the body, NEVER the URL (NIMC Act s.26 / NDPA s.30)
curl -X POST "https://api.trcn.gov.ng/v1/inter-agency/lookup/by-nin" \
-H "Authorization: Bearer $TRCN_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "nin": "12345678901" }'/v1/inter-agency/batchUp to 100 eligibility checks in one call. Returns { count, results } where each result is its own signed envelope. Supports Idempotency-Key.
curl -X POST "https://api.trcn.gov.ng/v1/inter-agency/batch" \
-H "Authorization: Bearer $TRCN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [
{ "identifier": { "tId": 1234567 }, "purpose": "NECO_INVIGILATOR_ELIGIBILITY" },
{ "identifier": { "regNo": "TRCN/LAG/2023/998877" }, "purpose": "NECO_INVIGILATOR_ELIGIBILITY" }
]
}'/v1/inter-agency/.well-known/jwks.json/v1/inter-agency/healthNo authentication. The JWKS document holds the public keys for verifying response signatures; /health returns the issuer and active key id.
The signed response envelope
Every authenticated response is a JSON envelope. The signature field is a compact RS256 JWS (JWT) signed by TRCN over the same result, with claims iss, aud (your partner ID), sub (the verification ID), and iat. This gives you non-repudiation: you can prove to a third party that TRCN attested a status at a point in time.
{
"verificationId": "ver_7f3a1c9e8b2d4f60",
"verifiedAt": "2026-06-07T10:42:11.880Z",
"issuer": "https://api.trcn.gov.ng",
"audience": "waec",
"result": {
"found": true,
"source": "new_system",
"status": "VALID",
"eligible": true,
"reasons": [],
"teacher": {
"name": "Adeyemi John Olumide",
"tId": 1234567,
"state": "Lagos",
"category": "Professional"
},
"license": {
"status": "VALID",
"issueDate": "2024-01-15",
"expiryDate": "2027-01-14",
"daysRemaining": 587
},
"certificate": {
"status": "VERIFIED",
"regNo": "TRCN/ABJ/2024/123456",
"issueDate": "2024-01-15"
}
},
"signature": "eyJhbGciOiJSUzI1NiIsImtpZCI6InRyY24taWEtMjAyNi0wNiJ9.eyJzdWIiО...",
"signatureKid": "trcn-ia-2026-06"
}Verify the signature
Fetch the JWKS (cache it; respect kid rotation), verify the JWS with the key whose kid matches the JOSE header, and confirm iss and aud. Trust the verified result claim, not the unsigned envelope copy.
Node.js
// npm i jose
import { createRemoteJWKSet, jwtVerify } from 'jose';
const JWKS = createRemoteJWKSet(
new URL('https://api.trcn.gov.ng/v1/inter-agency/.well-known/jwks.json'),
);
// envelope = the JSON body you received
const { payload } = await jwtVerify(envelope.signature, JWKS, {
issuer: 'https://api.trcn.gov.ng',
audience: 'YOUR_PARTNER_ID', // must equal envelope.audience
});
// payload.result is what TRCN cryptographically attested.
// Trust payload.result over envelope.result if they ever differ.
if (payload.result.status !== 'VALID') {
// not currently licensed
}Python
# pip install "pyjwt[crypto]"
import jwt
from jwt import PyJWKClient
JWKS_URL = "https://api.trcn.gov.ng/v1/inter-agency/.well-known/jwks.json"
jwks = PyJWKClient(JWKS_URL)
signing_key = jwks.get_signing_key_from_jwt(envelope["signature"])
claims = jwt.decode(
envelope["signature"],
signing_key.key,
algorithms=["RS256"],
issuer="https://api.trcn.gov.ng",
audience="YOUR_PARTNER_ID", # must equal envelope["audience"]
)
result = claims["result"] # the attested resultResult fields
| Field | Meaning |
|---|---|
| found | Whether a teacher matched. When false, other fields are null. |
| status | VALID · EXPIRED · REVOKED · NOT_FOUND. Treat anything but VALID as not currently licensed. |
| eligible | Boolean verdict for the supplied purpose; reasons[] explains a false. |
| source | new_system (certified) or legacy. |
| teacher | name, tId, state, category. |
| license | status, issueDate, expiryDate, daysRemaining. |
| certificate | status (VERIFIED / CLAIMED / LEGACY_RECORD / NOT_FOUND), regNo, issueDate. Use regNo as the stable identifier. |
| profile | Full profile (qualifications, employment) for new-system lookups. Raw NIN is never returned — only ninVerified. |
Rate limits
Limits are per-partner: a per-minute and a per-day quota set on your record. Every response carries the current window state, and a breach returns 429 with a retryAfterSeconds hint. Back off and retry; do not hammer.
X-RateLimit-Limit— your per-minute quotaX-RateLimit-Remaining— calls left in the windowX-RateLimit-Reset— Unix epoch (seconds) when the window resets
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1749291791
{
"statusCode": 429,
"message": "Per-minute quota exceeded for partner waec",
"retryAfterSeconds": 23
}Idempotency
For POST /eligibility-check and POST /batch, send an Idempotency-Key header (a unique value you generate per logical request). A retry with the same key and same body replays the original response instead of re-running the check — safe for network retries. Reusing a key with a different body is rejected.
Errors
| Status | When |
|---|---|
| 400 | Malformed request — missing identifier or purpose, NIN not 11 digits, non-numeric tId, empty or >100-item batch. |
| 401 | No valid credential (mTLS cert, Bearer key, or x-trcn-api-key). |
| 403 | Authenticated, but missing the scope for the endpoint or the purpose is not in your allowed purposes. |
| 429 | Per-minute or per-day quota exceeded. Honour retryAfterSeconds. |
| 5xx | Transient — retry with exponential back-off (start 1s, max 3 retries). |
Note: a successful lookup that simply finds no teacher is still 200 with result.found = false — not a 404.
Webhooks (status-change callbacks)
If you subscribed, TRCN POSTs a signed event to your endpoint whenever a verified teacher’s status changes — so you don’t have to poll. Subscriptions are provisioned by TRCN; ask to subscribe and you’ll receive a signing secret (whsec_…).
Event types
license.verifiedlicense.expiredlicense.revokedlicense.reinstatedcertificate.verifiedcertificate.revokedteacher.deactivatedDelivery payload
The body carries no raw PII — only the public regNo/tId identifiers and status strings. Headers include X-TRCN-Signature, X-TRCN-Event-Id, X-TRCN-Event-Type, and X-TRCN-Delivery-Id.
POST https://your-endpoint.example.gov.ng/trcn/webhooks
Content-Type: application/json
X-TRCN-Signature: t=1749291731,v1=3a9f...c12e
X-TRCN-Event-Id: evt_5b2c8d1a9e4f
X-TRCN-Event-Type: license.revoked
X-TRCN-Delivery-Id: dlv_91aa77c0
User-Agent: TRCN-Webhooks/1.0
{
"eventId": "evt_5b2c8d1a9e4f",
"eventType": "license.revoked",
"occurredAt": "2026-06-07T10:42:11.880Z",
"data": {
"tId": 1234567,
"regNo": "TRCN/ABJ/2024/123456",
"licenseStatus": "REVOKED",
"previousStatus": "VALID"
}
}Verify the signature
X-TRCN-Signature is a Stripe-style HMAC-SHA256 of `${t}.${rawBody}` using your signing secret. Compute it over the raw request body (before JSON parsing), compare in constant time, and reject events older than 5 minutes (replay protection). Acknowledge with a 2xx as soon as you have persisted the event; process asynchronously. Failed deliveries are retried with exponential back-off and jitter, then dead-lettered.
import crypto from 'crypto';
const REPLAY_WINDOW_SECONDS = 300; // 5 minutes
// IMPORTANT: verify against the RAW request body, before JSON parsing.
function verifyTrcnWebhook(rawBody, signatureHeader, signingSecret) {
const parts = Object.fromEntries(
signatureHeader.split(',').map((p) => p.split('=')),
);
const t = Number(parts.t);
const provided = parts.v1;
if (!Number.isFinite(t) || !provided) return false;
// reject replays
if (Math.abs(Date.now() / 1000 - t) > REPLAY_WINDOW_SECONDS) return false;
const expected = crypto
.createHmac('sha256', signingSecret) // your whsec_... secret
.update(`${t}.${rawBody}`)
.digest('hex');
const a = Buffer.from(provided);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Acknowledge fast (2xx) once stored; process asynchronously.OpenAPI specification
The full contract is published as OpenAPI 3.1 — generate typed clients, drive contract tests, or import into Postman / Insomnia / Bruno.
Partner credentials, additional scopes/purposes, mTLS certificate enrolment, and webhook subscriptions are provisioned by the TRCN integrations team. Looking for the free, no-auth public lookup instead? See the Teacher Verification API.
Contact the integrations team