Council for the Regulation of Engineering in Nigeria
Verification API developer guide · v1
Back to portal
COREN Integrated Systems · API v1

Verification API guide

A self-guided handbook for connecting any authorized external system to practitioner and firm verification. Follow the steps, copy the examples, then try a sandbox call.

OpenAPI spec
Step 1

What this API does

Authorized government and enterprise systems can confirm whether a COREN registration number belongs to a real practitioner or firm, and whether that record is currently allowed to practice.

Practitioner Verification Name, cadre, field, status, license, qualifications, passport
Firm Verification Name, type, category, size, fields, year established, status, license
The API uses the same registry rules as the public verification page. It does not invent a second source of truth, and it never returns emails, phone numbers, addresses, passwords, or payment history.
Step 2

Quick start

Work through these five actions once. After that, every verification call follows the same pattern.

  1. Ask COREN to register your organization as an API consumer, then create a sandbox application and a production application.
  2. Generate credentials on the application. Copy the secret immediately. It is shown only once.
  3. Start in sandbox. Use https://portal.coren.gov.ng/api/sandbox/v1 and keys that begin with pk_test_ / sk_test_.
  4. Sign every request with HMAC-SHA256. Unsigned or replayed requests are rejected.
  5. Send the license number as reference — the practitioner number or firm number you want to verify.
Step 3

Environments and base URLs

Sandbox and production are separate. A key from one environment is rejected by the other.

EnvironmentBase URLKey prefixUse for
Sandbox https://portal.coren.gov.ng/api/sandbox/v1 pk_test_ / sk_test_ Integration testing
Production https://portal.coren.gov.ng/api/v1 pk_live_ / sk_live_ Live verification

Current version: v1. Future versions will be introduced as /api/v2 without removing v1.

Step 4

API credentials

Each application receives a public key (identifier) and a secret key (signing key).

Public key:  pk_test_ab12cd34ef56gh78ij90klmnopqr
Secret key:  sk_test_zw9y8x7w6v5u4t3s2r1q0ponmlkjihgfedcba98
Treat the secret like a password. Do not commit it, log it, or put it in browser history, tickets, or chat. If it leaks, rotate it in API Management immediately.
  • Rotating a key issues a new pair and retires the old one.
  • Revoking a key stops it immediately.
  • Expired keys return expired_credentials.
Step 5

Authentication

Every request must identify the application and prove possession of the secret.

Required headers

HeaderRequiredMeaning
X-Api-KeyYesPublic key
X-Api-SignatureYeshex HMAC-SHA256 of the canonical string
X-Api-TimestampYesUnix seconds, within ±300 seconds
X-Api-NonceYesUnique value. Reuse is rejected as a replay
X-Idempotency-KeyOptionalCaller-defined request id
Content-TypeYesapplication/json

Canonical string

Join these five lines with a single newline (\n), then HMAC-SHA256 them with the secret key:

timestamp
nonce
METHOD
/path
sha256(raw_json_body)

Worked example for practitioner verification:

1710000000
nonce-8f1c2a
POST
/api/v1/verify/practitioner
e3b0c44298fc1c149afbf4c8996fb924...   ← SHA-256 of the exact JSON body
Sign the raw body bytes you actually send. If you pretty-print JSON for humans, the signature will not match. The path includes the environment prefix, for example /api/sandbox/v1/verify/firm.
Step 6

Practitioner verification

POST /api/v1/verify/practitioner · scope practitioners.verify

Request

{
  "reference": "R12345"
}
FieldRequiredNotes
referenceYesPractitioner license number (COREN number).

Successful response

{
  "success": true,
  "message": "Practitioner verified successfully.",
  "data": {
    "reference": "R12345",
    "registration_number": "R12345",
    "full_name": "Aisha Bello",
    "gender": "Female",
    "cadre": "Engineer",
    "engineering_field": "Civil Engineering",
    "registration_status": "Active",
    "registration_status_code": "enabled",
    "is_active": true,
    "license_status": {
      "is_valid": true,
      "is_licensed_to_practice": true,
      "label": "Licensed to practice",
      "is_expired": false
    },
    "passport_url": "https://portal.coren.gov.ng/storage/passports/R12345.jpg",
    "qualifications": [
      {
        "qualification": "B.Eng",
        "institution": "Ahmadu Bello University",
        "certificate": "Bachelor of Engineering",
        "start_date": "2010",
        "end_date": "2015"
      }
    ]
  },
  "meta": {
    "request_id": "9f2c1a6e-4b77-4d21-9c0a-1e8f0b2d3c4a",
    "timestamp": "2026-09-05T11:10:00+01:00"
  }
}
Step 7

Firm verification

POST /api/v1/verify/firm · scope firms.verify

Request

{
  "reference": "F12345"
}

Successful response

{
  "success": true,
  "message": "Firm verified successfully.",
  "data": {
    "reference": "F12345",
    "registration_number": "F12345",
    "full_name": "Greenfield Engineering Ltd",
    "firm_type": "Limited Liability",
    "category": "Consulting",
    "size": "Medium",
    "engineering_fields": ["Civil Engineering", "Structural Engineering"],
    "year_established": 2008,
    "registration_status": "Active",
    "registration_status_code": "active",
    "is_active": true,
    "license_status": {
      "is_valid": true,
      "is_licensed_to_practice": true,
      "label": "Licensed to practice",
      "is_expired": false
    }
  },
  "meta": {
    "request_id": "b71d0e22-88aa-4f01-91c3-2a44e19d8c10",
    "timestamp": "2026-09-05T11:12:00+01:00"
  }
}
Step 8

How to read the response

Your application should decide presentation. Do not expect COREN to return colours.

FieldMeaningSuggested UI
registration_status Account standing from the COREN portal (Active, Suspended, Deregistered, Deceased, Inactive). Show the label as-is.
is_active The record may currently renew / practice from an account-status perspective. Gate the main “eligible” badge.
license_status.is_licensed_to_practice Wallet-based license rule used on the public verification page. Expired when balance is missing or negative. Green if true, red if false.
license_status.is_valid License is current and the account is allowed to practice. Use this for a single pass/fail decision.
meta.request_id Unique id for this call. Quote it when contacting COREN support. Store with your case record.
Step 9

Errors

Failed calls still return JSON. Read error_code, message, and meta.request_id.

{
  "success": false,
  "message": "No practitioner was found with the provided reference.",
  "data": { "error_code": "not_found" },
  "error_code": "not_found",
  "meta": {
    "request_id": "c0ffee00-1111-2222-3333-444455556666",
    "timestamp": "2026-09-05T11:15:00+01:00"
  }
}
HTTPerror_codeWhat to do
401missing_credentialsSend X-Api-Key.
401invalid_credentialsCheck the public key or secret.
401expired_credentials / revoked_credentialsGenerate or rotate a new key.
401invalid_signatureRebuild the canonical string from the exact body and path.
401invalid_timestampSync server time. Window is ±300 seconds.
401replay_detectedUse a new nonce on every request.
403suspended_consumer / suspended_applicationAsk COREN to reactivate access.
403environment_mismatchUse sandbox keys only on sandbox URLs.
403insufficient_scopeAsk COREN to assign the product/scope.
403ip_not_allowedCall from an allowlisted address.
404not_foundThe registration number is not on the register.
422invalid_requestSend a reference (license number).
429rate_limit_exceededWait for Retry-After seconds.
500internal_errorRetry later and share the request id.
Step 10

Rate limits and IP restrictions

Default limits apply unless COREN sets custom values on the application.

SandboxProduction
Per minute3060
Per hour5002,000
Per day2,00010,000
Per month20,000200,000

When limited, the response is HTTP 429 and includes Retry-After, X-RateLimit-Limit, and X-RateLimit-Remaining.

If an IP allowlist is enabled, only listed addresses or CIDR ranges can call the API. Ask COREN to add your egress IPs before go-live.

Step 11

Integration playbook

Any approved consumer follows the same contract. The API is not tailored to a single organization or product.

  1. Complete your own authorization step first (payment, eligibility check, or internal approval). Your system is the record for that step.
  2. Only then call COREN with HMAC credentials.
  3. Send reference as the practitioner or firm license number.
  4. Render registration_status and license_status in your own interface.
  5. Store meta.request_id with your case record.
reference is the license number to look up. It does not authorize the call. Credentials, signature, timestamp, and nonce are always required. Repeating the same reference is safe: verification is read-only and idempotent.
Step 12

Code examples

These samples sign a sandbox practitioner request. Replace the keys and license number in reference.

BODY='{"reference":"R12345"}'
TS=$(date +%s)
NONCE=$(uuidgen)
PATH_URL="/api/sandbox/v1/verify/practitioner"
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $2}')
CANON=$(printf '%s\n%s\n%s\n%s\n%s' "$TS" "$NONCE" "POST" "$PATH_URL" "$BODY_HASH")
SIG=$(printf '%s' "$CANON" | openssl dgst -sha256 -hmac "$SECRET_KEY" -hex | awk '{print $2}')

curl -sS -X POST "https://portal.coren.gov.ng/api/sandbox/v1/verify/practitioner" \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: $PUBLIC_KEY" \
  -H "X-Api-Signature: $SIG" \
  -H "X-Api-Timestamp: $TS" \
  -H "X-Api-Nonce: $NONCE" \
  --data "$BODY"
<?php
$body = json_encode([
    'reference' => 'R12345',
], JSON_UNESCAPED_SLASHES);
$path = '/api/sandbox/v1/verify/practitioner';
$timestamp = (string) time();
$nonce = bin2hex(random_bytes(16));
$canonical = implode("\n", [$timestamp, $nonce, 'POST', $path, hash('sha256', $body)]);
$signature = hash_hmac('sha256', $canonical, $secretKey);

$ch = curl_init('https://portal.coren.gov.ng/api/sandbox/v1/verify/practitioner');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'X-Api-Key: '.$publicKey,
        'X-Api-Signature: '.$signature,
        'X-Api-Timestamp: '.$timestamp,
        'X-Api-Nonce: '.$nonce,
    ],
    CURLOPT_POSTFIELDS => $body,
    CURLOPT_RETURNTRANSFER => true,
]);
echo curl_exec($ch);
import crypto from 'crypto';

const body = JSON.stringify({
  reference: 'R12345',
});
const path = '/api/sandbox/v1/verify/practitioner';
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomUUID();
const canonical = [timestamp, nonce, 'POST', path, crypto.createHash('sha256').update(body).digest('hex')].join('\n');
const signature = crypto.createHmac('sha256', secretKey).update(canonical).digest('hex');

const res = await fetch('https://portal.coren.gov.ng/api/sandbox/v1/verify/practitioner', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Api-Key': publicKey,
    'X-Api-Signature': signature,
    'X-Api-Timestamp': timestamp,
    'X-Api-Nonce': nonce,
  },
  body,
});
console.log(await res.json());
import hashlib, hmac, json, time, uuid, urllib.request

body = json.dumps({
    "reference": "R12345",
}, separators=(",", ":"))
path = "/api/sandbox/v1/verify/practitioner"
timestamp = str(int(time.time()))
nonce = str(uuid.uuid4())
body_hash = hashlib.sha256(body.encode()).hexdigest()
canonical = "\n".join([timestamp, nonce, "POST", path, body_hash])
signature = hmac.new(secret_key.encode(), canonical.encode(), hashlib.sha256).hexdigest()

req = urllib.request.Request(
    "https://portal.coren.gov.ng/api/sandbox/v1/verify/practitioner",
    data=body.encode(),
    headers={
        "Content-Type": "application/json",
        "X-Api-Key": public_key,
        "X-Api-Signature": signature,
        "X-Api-Timestamp": timestamp,
        "X-Api-Nonce": nonce,
    },
    method="POST",
)
print(urllib.request.urlopen(req).read().decode())
Step 13

Try it in the browser

This console signs the request in your browser and calls the API directly. Keys stay in this page and are not saved on the server.

Use sandbox keys first. Do not paste production secrets into a shared computer.
The response will appear here.
Step 14

Versioning and changelog

External URLs include the version. v1 remains available when v2 is introduced. Deprecated versions will be announced before retirement.

VersionDateNotes
v1 5 September 2026 Lookup field is reference (practitioner or firm license number). HMAC authentication, sandbox/production split, request logging, and admin portal.

Machine-readable contract: openapi-v1.yaml

Step 15

FAQ

Can I send the secret as a Bearer token instead of HMAC?

Production applications require HMAC. You may also send X-Api-Secret or Authorization: Bearer sk_..., but the signature, timestamp, and nonce are still required when HMAC is enabled.

Why is my signature invalid?

Almost always the body or path used for signing differs from the request. Sign the exact JSON string and the full path including /api/v1 or /api/sandbox/v1.

Does sandbox use dummy practitioners?

Sandbox is a separate credential environment. Verification still reads the live register (read-only) so you can test with real numbers without production keys.

Will more APIs be added?

Yes. License, certificate, and other products can be added without changing how consumers, applications, or credentials work.