Build trust into
your next request.
Your starting point for identity and business checks in Nigeria. The complete machine-readable contract is published at api.semperid.ng/docs.
Quickstart
Every check is a POST to https://api.semperid.ng with a JSON body and a Bearer key from your backend.
- Create an account in the SemperID console.
- Create a test key and choose the checks it may call. The secret is shown once; store it in your server’s secret store.
- Call any check with its sandbox test values to exercise every outcome your application must handle.
- Submit your business verification, turn on two-factor sign-in, fund your wallet and create a live key.
curl https://api.semperid.ng/v1/identity/nin/details \
--header "Authorization: Bearer $SEMPERID_API_KEY" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: signup-2291-nin" \
--data '{
"nin": "11111111110"
}'// Run on your server. Never ship your API key to a browser.
const response = await fetch(
'https://api.semperid.ng/v1/identity/nin/details',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SEMPERID_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'signup-2291-nin',
},
body: JSON.stringify({ nin: '11111111110' }),
},
);
const result = await response.json();# Run on your server. Never ship your API key to a browser.
import os
import requests
response = requests.post(
"https://api.semperid.ng/v1/identity/nin/details",
headers={
"Authorization": f"Bearer {os.environ['SEMPERID_API_KEY']}",
"Idempotency-Key": "signup-2291-nin",
},
json={"nin": "11111111110"},
timeout=30,
)
result = response.json()Authentication
Send your key in the Authorization header from your backend, never as a URL parameter or in client-side code.
Authorization: Bearer <your-api-key>
Content-Type: application/json
Idempotency-Key: <your-unique-value>Each key is scoped to the checks it may call. Rotate a key in one step from the console: the old secret stops working and its replacement is issued together. Revoking a key takes effect immediately and fires the api_key.revoked webhook.
Environments
Every key belongs to one environment. Test keys answer with the published sandbox outcomes; live keys reach the live sources.
| Key | What it needs |
|---|---|
| Test | A SemperID account. |
| Live | An approved business verification (KYB), live access for your workspace and two-factor sign-in on the account that creates it. |
Sandbox outcomes
Each check publishes six test values. The last digit of the value selects the outcome:
| Ends in | Outcome | Billed |
|---|---|---|
0 | Successful verification | Yes |
1 | No matching identity | Yes |
2 | Identity mismatch | Yes |
3 | Upstream provider unavailable | No |
4 | Upstream provider timeout | No |
5 | Upstream provider error | No |
Verification reference
Each request below uses the sandbox value for a successful verification. Every field, type and requirement is listed in the API reference.
BVN Full Details
IdentityRetrieve the identity details linked to a BVN when your approved workflow needs the registered profile.
{
"bvn": "22222222220"
}The registered details behind the BVN, such as name, date of birth, phone number and gender.
Scope identity:bvn_details · Sandbox test values 22222222220 to 22222222225
CAC
BusinessConfirm a Nigerian company by its name and RC number, then read its directors and shareholders from the CAC register.
{
"company_name": "SANDBOX TRADING LIMITED",
"registration_number": "RC1234560"
}The company with that name and number: its company ID, RC number, type, status, registration date and address. The company ID opens its directors and shareholders.
| Lookup | Endpoint | Send | Sandbox, verified |
|---|---|---|---|
| Company Lookup | POST /v1/business/cac/lookup | company_name + registration_number | SANDBOX TRADING LIMITED + RC1234560 |
| Directors | POST /v1/business/cac/directors | company_id | 3369190 |
| Shareholders | POST /v1/business/cac/shareholders | company_id | 3369190 |
Scope business:cac · Sandbox test values SANDBOX TRADING LIMITED to SANDBOX TRADING 5
Passport Lookup
IdentityCheck a Nigerian international passport and retrieve the identity and document details linked to it.
{
"passport_number": "A12345670",
"last_name": "OKAFOR",
"date_of_birth": "1990-04-17"
}The passport record: the holder’s name and date of birth, with the issue and expiry dates.
Scope identity:passport · Sandbox test values A12345670 to A12345675
NIN Full Details
IdentityRetrieve the registered identity details linked to a NIN for an approved verification flow.
{
"nin": "11111111110"
}The registered details behind the NIN, such as name, date of birth, phone number and address.
Scope identity:nin_details · Sandbox test values 11111111110 to 11111111115
Errors and outcomes
A failed request is not a negative identity result. Every error carries a request_id and a machine-readable code, so your application can tell invalid input, an authentication problem and an unavailable source apart.
{
"request_id": "req_01m4f8k2v9qyx7d3h6t1c5n0wa",
"error": {
"code": "validation_error",
"message": "nin must be an 11-digit National Identification Number."
}
}| HTTP status | What to do |
|---|---|
| 400 / 422 | Correct the request inputs. Do not retry unchanged data. |
| 401 / 403 | Check the key and the scopes it was given. |
| 402 | The wallet cannot cover the live call (insufficient_balance). The provider is not called and nothing is charged. Fund the wallet, then retry. |
| 404 | Check the endpoint path against the reference. |
| 429 | Slow down and retry with the same Idempotency-Key. |
| 5xx | Retry with the same Idempotency-Key. Not billed. |
Billing: Per resolved call. Provider failures and timeouts are not billed. Send an Idempotency-Key and a settled result is safe to retry.
Request references
{
"request_id": "req_01m4f8k2v9qyx7d3h6t1c5n0wa",
"verified": true,
"data": {
"nin": "11111111110",
"first_name": "Amara",
"middle_name": "Chidinma",
"last_name": "Okafor",
"date_of_birth": "1990-04-17",
"gender": "female",
"phone_number": "08030000012",
"nationality": "NG"
}
}Quote the request_id to support and use it to find the request in your console log, where payloads are stored masked. Keep the reference in your own records instead of the full identity response whenever it is enough for your purpose.
Wallet balance
Live calls are paid from your workspace wallet. Read it with any of your API keys to check the balance before a run of live calls, or to watch it from your own monitoring. available_kobo is what live calls can spend: the balance less what checks in progress already hold. A live call the wallet cannot cover is refused with 402 before the provider is reached.
curl https://api.semperid.ng/v1/wallet \
--header "Authorization: Bearer $SEMPERID_LIVE_KEY"{
"request_id": "req_01m4f8k2v9qyx7d3h6t1c5n0wa",
"data": {
"currency": "NGN",
"status": "ACTIVE",
"available_kobo": 1250000,
"balance_kobo": 1280000,
"reserved_kobo": 30000,
"low_balance_threshold_kobo": 500000,
"low_balance": false
}
}Set a low-balance alert on the console’s Wallet page to be emailed daily, weekly or monthly while the balance sits below an amount you choose. The same moment is published as the wallet.low_balance webhook event.
Webhooks
Register an HTTPS endpoint in the console for test or live events. Its signing secret is shown once. Send a synthetic event to prove the endpoint, inspect every delivery and replay any of them.
| Event | Sent when |
|---|---|
verification.completed | A verification resolved, whether or not it matched. |
verification.failed | A verification could not be completed by the upstream provider. |
wallet.credited | Funds were added to the wallet. |
wallet.low_balance | The wallet fell below its low-balance threshold. |
kyb.approved | Business verification was approved. |
kyb.needs_information | A reviewer asked for more information. |
api_key.revoked | An API key was revoked. |
Verifying a delivery
Each delivery carries SemperID-Event-Id, SemperID-Timestamp and SemperID-Signature headers. The signature is a hex HMAC-SHA256 of {timestamp}.{raw_body} with your signing secret. Verify against the exact bytes received and reject timestamps older than 300 seconds.
// Verify a delivery against the exact bytes received.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function isFromSemperID(rawBody, headers, secret) {
const timestamp = headers['semperid-timestamp'];
const signature = headers['semperid-signature'] ?? '';
const expected = createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
// Also reject timestamps more than 300 seconds old.
return (
signature.length === expected.length &&
timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
);
}Return a 2xx to end the chain. Anything else is retried on a fixed schedule: immediately, then at 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours.
Rate limits
Each check accepts 60 requests a minute. When you reach it, the API returns 429: back off and retry with the same Idempotency-Key rather than sending in parallel.