Verify a person with one hosted flow.
Create a secure identity-verification link, send it to your customer or tenant, and receive a structured decision through the API and signed webhooks.
Quickstart
Complete one end-to-end Sandbox verification before writing production logic. REST API delivery is authenticated JSON and signed webhook events; it does not create a downloadable client PDF report. For a reusable link, dashboard case and PDF report without coding, use No-Code Integration in the Client Dashboard.
Register, verify your email, and sign in to the Client Dashboard.
Open API Integration, choose Generate TEST Keys, and store the secret securely.
Call the create endpoint from your server using the TEST key and a unique external reference.
Open hostedUrl and choose Pass, Review required, or Fail.
Retrieve by ID or external reference. If configured, verify that your webhook received the same result.
Authentication
Every API request requires your environment’s secret key in the X-API-Key header. TEST keys create simulated, zero-cost requests; LIVE keys create billable real verifications.
What does “Authorize 🔒” do?
The Authorize padlock exists only in the interactive API reference. Entering a TEST key there makes the page add X-API-Key automatically when you select “Try it out.” It does not enable your account, create a key, or affect calls made from your own code. If you use curl, Postman, or your backend, send the header yourself.
X-API-Key: sk_test_your_secret_key
Choose a product
| Product code | Identity & face | Proof of address | References | Use when |
|---|---|---|---|---|
KYC_STANDARD | Yes | Yes | No | You need core identity and address evidence. |
KYC_ENHANCED | Yes | Yes | Yes | You also need referee evidence and responses. |
KYT | Yes | Yes | Tenant-focused | You are screening a prospective tenant. |
Additional government ID is conditional. It is requested when initial identity, name, facial, or confidence checks need remediation—not for every subject.
Create a verification
/api/v1/hosted-verificationsCreates one hosted journey. The API key selects TEST or LIVE mode. TEST costs zero and allows up to 100 requests per month.
curl --request POST \
--url https://api-cp.onmero.com/api/v1/hosted-verifications \
--header "Content-Type: application/json" \
--header "X-API-Key: sk_test_your_secret_key" \
--header "Idempotency-Key: hv_customer_4f29a8c7" \
--data '{
"productCode": "KYC_STANDARD",
"externalReference": "customer_4f29a8c7",
"subject": {
"firstName": "Ada",
"middleName": "Ifeoma",
"lastName": "Okafor",
"email": "ada@example.com",
"phone": "+2348012345678"
},
"delivery": { "email": false },
"redirectUrl": "https://app.example.com/verification/complete",
"metadata": { "customerId": "cus_123" }
}'Request fields
| Field | Required | Meaning |
|---|---|---|
productCode | Yes | The evidence workflow to run. |
externalReference | Yes | Your unique case ID. Use it to reconcile webhooks and retrieve by reference. |
subject.firstNamesubject.middleNamesubject.lastName | First + last | Real legal names, prefilled and compared with the verified identity record. |
subject.email | Yes | Subject email and optional ONMERO delivery destination. |
subject.phone | No | Phone number, preferably with country code. |
delivery.email | No | When true, ONMERO emails the link. Otherwise deliver hostedUrl yourself. |
redirectUrl | No | HTTPS return page after the journey. A return is not proof of success. |
metadata | No | Non-sensitive reconciliation values. Never include identity numbers, documents or secrets. |
Idempotency-Key | Recommended | Reuse only for an identical network retry. After a 402 payment failure, use a new key and external reference. |
TEST response
{
"success": true,
"data": {
"id": "hv_123",
"externalReference": "customer_4f29a8c7",
"environment": "TEST",
"productCode": "KYC_STANDARD",
"status": "CREATED",
"hostedUrl": "https://cp.onmero.com/hosted-verification/...",
"verificationUrl": "https://cp.onmero.com/hosted-verification/...",
"qrCodeUrl": "https://api-cp.onmero.com/api/v1/.../qr.svg",
"expiresAt": "2026-10-28T12:00:00.000Z",
"billing": { "currency": "NGN", "amountCharged": 0, "pricingSource": "SANDBOX" }
}
}LIVE response
{
"success": true,
"data": {
"id": "hv_live_123",
"externalReference": "customer_live_001",
"environment": "LIVE",
"status": "CREATED",
"hostedUrl": "https://kyc.onmero.com/verify?token=...",
"verificationUrl": "https://kyc.onmero.com/verify?token=...",
"expiresAt": "2026-11-02T12:00:00.000Z",
"billing": { "currency": "NGN", "amountCharged": 5000, "pricingSource": "API_SERVICE_PRICE", "walletTransactionId": "txn_123" }
}
}Send the hosted journey
hostedUrl. It is the canonical subject-facing bearer URL in both environments. TEST opens the free simulator; LIVE opens the real billable verification journey. The subject does not need an ONMERO account or control-panel login. Keep the URL out of logs and analytics.Send hostedUrl by your own email, SMS, or application.
Returns the customer browser to your application. A request value overrides the saved TEST or LIVE default.
Receives signed server-to-server events. It must always be publicly reachable HTTPS.
Local development
http://localhost:3000/onmero/identity-verification/return or http://127.0.0.1:3000/onmero/identity-verification/return. For webhooks—or to test a public return URL—use an HTTPS ngrok or Cloudflare Tunnel address. LIVE redirects and every webhook require public HTTPS.TEST_REDIRECT_URL=http://localhost:3000/onmero/identity-verification/return PUBLIC_REDIRECT_URL=https://your-tunnel.ngrok-free.app/onmero/identity-verification/return WEBHOOK_URL=https://your-tunnel.ngrok-free.app/onmero/webhooks/events
status === "COMPLETED" and result.decision === "VERIFIED" using the authenticated status endpoint or a webhook whose signature you verified.Read the result
/api/v1/hosted-verifications/{id}/api/v1/hosted-verifications/by-reference/{externalReference}Example: straight-through verification
No additional identity document was needed. Liveness is shown as an outcome rather than a numeric score; the numeric value belongs to the facial comparison.
{
"status": "COMPLETED",
"result": {
"schemaVersion": "2026-10-21",
"decision": "VERIFIED",
"identity": {
"overallScore": 94,
"name": {
"firstName": { "matched": true, "score": 100 },
"lastName": { "matched": true, "score": 100 }
},
"facial": {
"matched": true,
"score": 96
},
"liveness": {
"required": false,
"completed": false,
"status": "NOT_REQUIRED"
},
"additionalIdentityDocument": {
"requested": false,
"received": false,
"ocrCompleted": false,
"evidence": {
"frontReceived": false,
"backReceived": false,
"ocrDocumentReceived": false
}
}
},
"proofOfAddress": { "status": "ACCEPTED", "received": true },
"references": [],
"reportId": "report_123"
}
}Example: additional ID and address review
The initial confidence or facial result required remediation. An additional government ID was received and OCR-processed, while proof of address still requires authorised review.
{
"status": "REVIEW_REQUIRED",
"result": {
"decision": "REVIEW_REQUIRED",
"identity": {
"overallScore": 68,
"facial": { "matched": false, "score": 72 },
"liveness": {
"required": true,
"completed": true,
"status": "COMPLETED"
},
"additionalIdentityDocument": {
"requested": true,
"received": true,
"ocrCompleted": true,
"evidence": {
"frontReceived": true,
"backReceived": true,
"ocrDocumentReceived": true
}
}
},
"proofOfAddress": {
"status": "REVIEW_REQUIRED",
"documentType": "UTILITY_BILL",
"received": true,
"confirmedAt": null,
"reviewReason": "The address evidence requires staff review."
},
"references": [],
"reportId": null
}
}Example: Enhanced verification with a reference
KYC Enhanced and KYT can return referee statements, recommendations, identity checks, live-presence outcome and referee face-match score.
{
"status": "COMPLETED",
"result": {
"decision": "VERIFIED",
"proofOfAddress": {
"status": "ACCEPTED",
"documentType": "BANK_STATEMENT",
"received": true,
"confirmedAt": "2026-10-21T12:00:00.000Z",
"reviewReason": null
},
"references": [{
"type": "CHARACTER",
"status": "ACCEPTED",
"relationship": "Professional referee",
"response": "I have known the subject for five years.",
"recommendation": "RECOMMENDED",
"identityNameMatched": true,
"ninVerified": true,
"facial": {
"livePresenceStatus": "CONFIRMED",
"facialComparisonStatus": "CONFIRMED",
"faceMatchScore": 93
}
}],
"reportId": "report_123"
}
}Conditional result sections
| Section | What it tells you |
|---|---|
identity.name | Per-name match flags and scores for supplied legal names. |
identity.facial | Facial comparison match and score. This is the numeric biometric comparison. |
identity.liveness | Whether liveness was required, completed, or not required. The current verification model does not issue a separate numeric liveness score. |
additionalIdentityDocument | Whether remediation ID was requested, received, OCR-processed, and which evidence sides were received. |
proofOfAddress | Submission, review status, document type, confirmation, and review reason. |
references | Enhanced/KYT referee status, response, recommendation, identity result, and available facial/liveness evidence. |
Handle webhooks
Configure an HTTPS URL in API Integration. ONMERO sends verification.created, verification.updated, verification.review_required, verification.completed, and verification.failed.
Do this before a framework parses or changes the JSON.
Calculate HMAC-SHA256 over timestamp.rawBody with the API secret and compare it safely with x-onmero-signature.
Use x-onmero-timestamp for replay protection and the payload ID for idempotency.
Queue slow work, then retrieve the verification from the API before granting access.
Test scenarios
Use a TEST request and choose each Sandbox result at least once.
| Scenario | Expected status | What to verify |
|---|---|---|
| Pass | COMPLETED | Your integration accepts the final result and reads identity/address/reference sections. |
| Review required | REVIEW_REQUIRED | No automatic approval; queue the case for review and handle additional evidence. |
| Fail | FAILED | No access is granted and the user receives an appropriate next step. |
| Duplicate network retry | Original response | The same idempotency key and identical payload do not create a duplicate. |
| Invalid/absent key | 401 | The secret is read from secure server configuration and the error is logged safely. |
| Invalid webhook signature | Your endpoint rejects it | No state is changed from an untrusted payload. |
Errors
| HTTP | Meaning | Action |
|---|---|---|
| 400 | Invalid product, subject, URL, or payload. | Correct the request; do not retry unchanged. |
| 401 | Missing, invalid, inactive, or revoked key. | Check the environment and rotate the key if needed. |
| 402 | LIVE wallet balance is insufficient; no journey is activated. | Fund the wallet, then create a new request with a new externalReference and Idempotency-Key. |
| 404 | The verification does not belong to this API client. | Check the ID/reference and environment. |
| 409 | The external reference or idempotency key conflicts. | Retrieve the original or use a new reference for a genuinely new case. |
| 429 | Too many requests. | Back off with jitter; do not load-test the Sandbox. |
Insufficient-funds response
{
"statusCode": 402,
"error": "Payment Required",
"code": "INSUFFICIENT_WALLET_BALANCE",
"message": "Insufficient wallet balance."
}redirectUrl as a verified result, automatically retry a 402, or place identity data in metadata.Go live
Submit the API Access service request and wait for approval.
Fund the wallet, generate LIVE keys, register the production webhook URL, and store secrets in your server’s secret manager.
Confirm link delivery, signature verification, final retrieval, decision handling, logging, and support procedures.
Watch webhook delivery logs and rotate a key immediately if it may have been exposed.
Sample integration code
This Node.js Quick Start creates a hosted verification from your server. Use a TEST key first. The dashboard also provides copyable examples for Python, PHP, Java, .NET, Go, Ruby, Rust, Kotlin and React backend-call flows.
X-API-Key in React, browser JavaScript, a mobile application or a public repository.import crypto from "node:crypto";
const externalReference = `customer_${crypto.randomUUID()}`;
const response = await fetch("https://api-cp.onmero.com/api/v1/hosted-verifications", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.ONMERO_API_KEY,
"Idempotency-Key": `create_${externalReference}`,
},
body: JSON.stringify({
productCode: "KYC_ENHANCED",
externalReference,
subject: { firstName: "Ada", lastName: "Okafor", email: "ada@example.com" },
redirectUrl: "http://localhost:3000/onmero/identity-verification/return",
delivery: { email: false },
}),
});
const body = await response.json();
if (response.status === 402) throw new Error("Fund the LIVE wallet, then use a new reference.");
if (response.status !== 201) throw new Error(body.message ?? "Unable to create verification.");
const data = body.data ?? body;
// Save data.id, then redirect the browser to data.hostedUrl. Never log that URL.Production-ready code
Production integrations should add timeouts, structured error handling, trusted-host validation, idempotency, minimal logging, authenticated status retrieval and verified webhook signatures.
const BASE_URL = "https://api-cp.onmero.com";
const TRUSTED_HOSTS = new Set(["cp.onmero.com", "kyc.onmero.com"]);
export async function getVerification(id) {
const response = await fetch(
`${BASE_URL}/api/v1/hosted-verifications/${encodeURIComponent(id)}`,
{ headers: { "X-API-Key": process.env.ONMERO_API_KEY },
signal: AbortSignal.timeout(20_000) },
);
if (!response.ok) throw new Error("Unable to retrieve verification status.");
const body = await response.json();
const data = body.data ?? body;
return {
...data,
verified: data.status === "COMPLETED" && data.result?.decision === "VERIFIED",
};
}
// Before redirecting, require HTTPS and an ONMERO trusted hostname.
// Verify webhook signatures over the raw request body.
// A redirect back to your portal is never proof of successful verification.COMPLETED with decision VERIFIED.