code
POST /api/kyc/verify

Submits an identity for verification. The request returns immediately with 202 Accepted and a verificationId; the actual verification runs asynchronously. Receive the outcome via webhooks or by polling GET /status/:id.

This call is also what fires verification.started. That is the submission event: it fires here, before any check has run, and is how your backend learns that somebody finished the flow.

Authentication: Authorization: Bearer pk_… (required). Content type: application/json. Production note: pk_live_ keys require an approved business.

Request body

FieldTypeRequiredDescription
countrystringyesISO-3166 alpha-2 country code: a government-database market (NG, GH, KE, ZA, CI) or any country enabled for global document verification.
idTypestringyesID type identifier, e.g. bvn. See Countries & ID types.
idNumberstringno*The ID number. Required for number-based IDs; omit for pure document flows.
userDataobjectnoApplicant-provided details to cross-check: firstName, lastName, dateOfBirth.
mediaIdsobjectnomediaIds from POST /upload. Keys: documentFront, documentBack, selfie, documentFrontVideo, documentBackVideo, livenessVideo.
externalUserIdstringnoYour stable reference for the person being verified (your user id). It is returned as externalUserId, links the resulting entity, and rides every webhook about this verification. Strongly recommended.
metadataobjectyesRequest metadata (see below). Any extra keys are your customer metadata: bounded (16 KiB, four levels, 100 keys), stored apart from Myaza's own fields, and echoed in responses and on every webhook about this verification. Use opaque correlation values (a loan id, an account id), never credentials or unnecessary personal data.
metadata.requestIdstringyesYour unique idempotency key for this request.
metadata.userIdstringnoLegacy location for your user reference. Prefer the top-level externalUserId; when both are present the top-level field wins.
metadata.deviceobjectnoFree-form device metadata; forwarded to the dashboard.
metadata.endUserobjectnoSecret keys only. Your user's own connection, when you call this endpoint from your backend: ipAddress, userAgent and optionally acceptLanguage. See Calling from your backend.

* Whether idNumber and which mediaIds are needed depends on the ID type. Check its features from GET /config.

Optional header

HeaderDescription
X-SDK-VersionThe SDK version making the call; recorded with the verification for support.

Request

shell
curl "https://trust.myaza.app/api/kyc/verify" \
  -H "Authorization: Bearer $MYAZA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "country": "NG",
    "idType": "drivers-license",
    "idNumber": "ABC12345",
    "userData": { "firstName": "John", "lastName": "Doe", "dateOfBirth": "1990-01-01" },
    "mediaIds": { "documentFront": "media_01j9abc...", "selfie": "media_01j9def..." },
    "externalUserId": "user_42",
    "metadata": { "requestId": "order_1001", "loanId": "loan_20191" }
  }'

Response 202 Accepted

json
{
  "verificationId": "ver_01j9...",
  "status": "processing",
  "externalUserId": "user_42",
  "metadata": { "loanId": "loan_20191" }
}

externalUserId and metadata come back exactly as stored, so a retry of the same requestId returns the same values.

The verification is now queued. Track it via its verificationId.

processing is the same word Get verification status returns for this id a moment later. There is one status vocabulary across the whole API, so nothing changes meaning between the response that accepts a submission and the one that reports on it.

Calling from your backend

When you call this endpoint from your server with a secret key, the request reaches us from your server, not your user. We don't treat your server as your user's device: its address and user agent are kept for the record only, and no Device Intelligence signals are raised from them.

To have your user's device and network analysed (country, network type, VPN and Tor checks, blocklists), pass their connection as you received it:

json
"metadata": {
  "requestId": "order_1001",
  "endUser": {
    "ipAddress": "102.89.34.10",
    "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) ...",
    "acceptLanguage": "en-NG,en;q=0.9"
  }
}
  • Send the address your own server saw for your user, not your server's address.
  • A value we can't read (for example an IP address that isn't valid) is ignored. It never makes the request fail.
  • endUser is ignored on a publishable key. Those calls come from your user's own device, so we already see their connection.
  • Without endUser or a device fingerprint, Device Intelligence has nothing to analyse, and it is not charged.

Idempotency

metadata.requestId is your idempotency key. If you submit a request with a requestId that already exists for your organisation, the API returns the existing verification instead of creating a new one:

json
{ "verificationId": "ver_01j9...", "status": "approved" }

Always send a stable requestId per logical verification so retries (timeouts, network errors) never create duplicates or double-charge you. Reusing another organisation's requestId is not possible: a collision across orgs returns 403.

Errors

StatusBodyCause
400{ "error": "Invalid request body", "message": "…" }Validation failed (bad country, missing requestId, etc.).
401{ "error": "Invalid API key" }Auth failed.
403{ "error": "Forbidden" }The requestId belongs to another organisation.
403{ "error": "business_not_approved", … }Production key but business not approved.
429{ "error": "Too many requests, please try again later." }Rate limit exceeded.

An accepted request (202) can still end in a failed, not_found, or error status later, for example insufficient credit, an ID not found in the government database, or a face mismatch. Those outcomes arrive via status/webhook, not as HTTP errors, and each carries a reason plus a stable reasonCode. See the verification lifecycle and the failure reason codes.