ONMERO Developers
Verification API · v1

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.

New integration? Start in TEST mode. It is free, uses simulated outcomes, and never performs a real identity check.

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.

Create your client account

Register, verify your email, and sign in to the Client Dashboard.

Generate a TEST secret key

Open API Integration, choose Generate TEST Keys, and store the secret securely.

Create a hosted verification

Call the create endpoint from your server using the TEST key and a unique external reference.

Simulate the subject outcome

Open hostedUrl and choose Pass, Review required, or Fail.

Confirm the result

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.

Keep secret keys server-side. Never put them in browser JavaScript, mobile applications, public repositories, screenshots, or support messages.

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 codeIdentity & faceProof of addressReferencesUse when
KYC_STANDARDYesYesNoYou need core identity and address evidence.
KYC_ENHANCEDYesYesYesYou also need referee evidence and responses.
KYTYesYesTenant-focusedYou 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

POST /api/v1/hosted-verifications

Creates 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

FieldRequiredMeaning
productCodeYesThe evidence workflow to run.
externalReferenceYesYour unique case ID. Use it to reconcile webhooks and retrieve by reference.
subject.firstName
subject.middleName
subject.lastName
First + lastReal legal names, prefilled and compared with the verified identity record.
subject.emailYesSubject email and optional ONMERO delivery destination.
subject.phoneNoPhone number, preferably with country code.
delivery.emailNoWhen true, ONMERO emails the link. Otherwise deliver hostedUrl yourself.
redirectUrlNoHTTPS return page after the journey. A return is not proof of success.
metadataNoNon-sensitive reconciliation values. Never include identity numbers, documents or secrets.
Idempotency-KeyRecommendedReuse 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

Use 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.
Direct link

Send hostedUrl by your own email, SMS, or application.

Redirect URL

Returns the customer browser to your application. A request value overrides the saved TEST or LIVE default.

Webhook URL

Receives signed server-to-server events. It must always be publicly reachable HTTPS.

Local development

TEST browser redirects can use loopback HTTP. Use 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
A browser return is not verification proof. Confirm status === "COMPLETED" and result.decision === "VERIFIED" using the authenticated status endpoint or a webhook whose signature you verified.

Read the result

GET /api/v1/hosted-verifications/{id}
GET /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

SectionWhat it tells you
identity.namePer-name match flags and scores for supplied legal names.
identity.facialFacial comparison match and score. This is the numeric biometric comparison.
identity.livenessWhether liveness was required, completed, or not required. The current verification model does not issue a separate numeric liveness score.
additionalIdentityDocumentWhether remediation ID was requested, received, OCR-processed, and which evidence sides were received.
proofOfAddressSubmission, review status, document type, confirmation, and review reason.
referencesEnhanced/KYT referee status, response, recommendation, identity result, and available facial/liveness evidence.
Missing is not passed. A null or absent section means the evidence is not available or does not apply. Base access decisions on the final status and the checks required by your policy.

Handle webhooks

Configure an HTTPS URL in API Integration. ONMERO sends verification.created, verification.updated, verification.review_required, verification.completed, and verification.failed.

Read the raw request body

Do this before a framework parses or changes the JSON.

Verify the signature

Calculate HMAC-SHA256 over timestamp.rawBody with the API secret and compare it safely with x-onmero-signature.

Reject stale timestamps and deduplicate

Use x-onmero-timestamp for replay protection and the payload ID for idempotency.

Return 2xx quickly

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.

ScenarioExpected statusWhat to verify
PassCOMPLETEDYour integration accepts the final result and reads identity/address/reference sections.
Review requiredREVIEW_REQUIREDNo automatic approval; queue the case for review and handle additional evidence.
FailFAILEDNo access is granted and the user receives an appropriate next step.
Duplicate network retryOriginal responseThe same idempotency key and identical payload do not create a duplicate.
Invalid/absent key401The secret is read from secure server configuration and the error is logged safely.
Invalid webhook signatureYour endpoint rejects itNo state is changed from an untrusted payload.

Errors

HTTPMeaningAction
400Invalid product, subject, URL, or payload.Correct the request; do not retry unchanged.
401Missing, invalid, inactive, or revoked key.Check the environment and rotate the key if needed.
402LIVE wallet balance is insufficient; no journey is activated.Fund the wallet, then create a new request with a new externalReference and Idempotency-Key.
404The verification does not belong to this API client.Check the ID/reference and environment.
409The external reference or idempotency key conflicts.Retrieve the original or use a new reference for a genuinely new case.
429Too 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."
}
Common mistakes: never expose secret keys in browser code, send a URL from a failed response, treat redirectUrl as a verified result, automatically retry a 402, or place identity data in metadata.

Go live

Request LIVE API access

Submit the API Access service request and wait for approval.

Configure production

Fund the wallet, generate LIVE keys, register the production webhook URL, and store secrets in your server’s secret manager.

Run a controlled real verification

Confirm link delivery, signature verification, final retrieval, decision handling, logging, and support procedures.

Monitor and rotate

Watch webhook delivery logs and rotate a key immediately if it may have been exposed.

Open interactive API reference →

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.

Server-side only. Never expose 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.
Final decision rule: unlock protected features only after authenticated retrieval or a verified webhook confirms COMPLETED with decision VERIFIED.

Open all language examples →