Identity Hub API

The Identity Hub is the server-to-server API you use to register the people and businesses you want to monitor as entities, and to read back their risk state. Once an entity exists you can stream its events for scoring.

Base URL & authentication

All Identity Hub endpoints live under a single base URL, the same for sandbox and production:

code
https://trust.myaza.app/api/identity

Every endpoint requires a secret (sk_) key as a Bearer token: these are backend-only, server-to-server calls that read and write identity data, so a publishable (pk_) key is rejected with 403 secret_key_required.

shell
curl "https://trust.myaza.app/api/identity/entities/user_42" \
  -H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

The key identifies your organisation and its environment; sk_test_ keys operate on sandbox data, sk_live_ on production. See Authentication.

Register or update an entity

POST /entities: create or update an entity, keyed by your own externalUserId. The call is idempotent: sending the same externalUserId again updates the existing entity rather than creating a duplicate.

Request

json
{
  "externalUserId": "user_42",
  "metadata": { "loanId": "loan_20191", "accountId": "acc_7732" },
  "type": "INDIVIDUAL",
  "kycProvenance": "MYAZA_VERIFIED",
  "kycSource": null,
  "profile": {
    "fullName": "John Doe",
    "dateOfBirth": "1990-01-01",
    "nationality": "NG",
    "idType": "bvn",
    "idNumber": "12345678901",
    "address": { "city": "Lagos", "country": "NG" },
    "declaredMonthlyVolume": 500000,
    "declaredTxnTypes": ["transfer", "deposit"],
    "walletAddresses": [
      { "address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", "chain": "bitcoin", "label": "withdrawal" }
    ]
  }
}
FieldRequiredNotes
externalUserIdYesYour own stable reference for the user. The entity's primary key within your org + environment.
metadataNoYour own correlation fields for this entity (a loan id, an account id). Bounded (16 KiB, four levels, 100 keys), returned on every read and carried on every webhook about the entity. Sending it again replaces the stored object.
typeNoINDIVIDUAL (default) or BUSINESS.
kycProvenanceNoHow the entity was KYC'd: MYAZA_VERIFIED, EXTERNAL_VERIFIED, or UNVERIFIED (default).
kycSourceNoFree-text label of the external verification source, when kycProvenance is EXTERNAL_VERIFIED.
profileNoDeclared attributes, all optional. nationality and declared fields feed monitoring rules (e.g. cross-border).
profile.walletAddressesNoUp to 20 crypto wallet addresses ({ address, chain?, label? }, address 20–128 chars). Omit the key to leave the stored set unchanged; send an array (including []) to replace it. Attaching a wallet enrols the entity in WALLET screening; changing the set re-screens promptly. Wallets are risk attributes, never identity keys.

Response

201 Created for a new entity, 200 OK for an update:

json
{
  "entityId": "ent_01j9abc123",
  "identityId": "idn_01j9xyz456",
  "externalUserId": "user_42",
  "metadata": { "loanId": "loan_20191", "accountId": "acc_7732" },
  "kycProvenance": "MYAZA_VERIFIED",
  "created": true,
  "screening": { "status": "QUEUED", "types": ["SANCTIONS", "PEP"] }
}
  • metadata echoes your customer metadata exactly as stored.
  • identityId is the entity's link to a global identity: a person or company shared across organisations and environments. It's null until resolution links one (see below).
  • screening reflects screening enrolment. It's the string "INACTIVE" when screening is not configured for your organisation, never a fake QUEUED.

KYC provenance & identity resolution

When you declare an entity verified (MYAZA_VERIFIED or EXTERNAL_VERIFIED) and supply an ID (profile.idType + profile.idNumber), the Hub runs resolution: it links the entity to a global identity, matching an existing one where the ID is already known or creating a new one. UNVERIFIED entities skip resolution and stay unlinked.

A linked identity carries its own trustState (UNVERIFIED, VERIFIED, FLAGGED), risk tier, and the set of verified identifiers (e.g. BVN and NIN) the person has accumulated across your org.

Look up an entity

GET /entities/:externalUserId: fetch an entity by your own reference, scoped to the key's org and environment.

json
{
  "entity": {
    "entityId": "ent_01j9abc123",
    "externalUserId": "user_42",
    "metadata": { "loanId": "loan_20191", "accountId": "acc_7732" },
    "identityId": "idn_01j9xyz456",
    "type": "INDIVIDUAL",
    "status": "ACTIVE",
    "disposition": "APPROVED",
    "kycProvenance": "MYAZA_VERIFIED",
    "kycSource": null,
    "kycVerifiedAt": "2026-04-27T12:00:00.000Z",
    "riskTier": "LOW",
    "riskScore": 12,
    "createdAt": "2026-04-27T12:00:00.000Z",
    "profile": { "fullName": "John Doe", "nationality": "NG" }
  },
  "identity": {
    "id": "idn_01j9xyz456",
    "type": "INDIVIDUAL",
    "trustState": "VERIFIED",
    "riskTier": "LOW",
    "riskScore": 12
  },
  "identifiers": [
    { "idType": "bvn", "idNumber": "12345678901" }
  ],
  "verifications": []
}

identity and identifiers are populated only when the entity is linked. verifications is the entity's full KYC history (newest first). A missing entity returns 404 { "error": "entity_not_found" }.

Update an entity

PATCH /entities/:externalUserId: change some of what you hold about an entity without resending the rest. Only the keys you send are touched; an absent key leaves the stored value alone, and null clears a nullable field. (POST /entities remains the full upsert, which replaces the whole profile.)

json
{
  "metadata": { "loanId": "loan_20191", "tier": "gold" },
  "profile": { "nationality": "GH", "declaredMonthlyVolume": 750000 }
}

The body accepts the same metadata, kycSource, kycProvenance (EXTERNAL_VERIFIED or UNVERIFIED only) and profile fields as the create call. externalUserId and type cannot change: the reference is the entity's key, and a person does not become a business.

Identity facts on a Myaza-verified entity are locked. When kycProvenance is MYAZA_VERIFIED, its fullName, dateOfBirth, nationality, idType and idNumber came from a verification we ran, and a patch touching them is refused with 400 field_not_editable naming the fields. Run a new verification to change them (verify again). Everything you declare yourself, contacts, address, declared volume, wallets, your own metadata, stays editable.

Changing idType or idNumber on an externally-verified entity re-runs identity resolution, exactly as the create call does.

An address you send is filed as declared evidence in the entity's address book, as email and phone are in its contact book. It may be one addressLine, or structured parts (line1, line2, city, state, country as ISO-2, postcode), which are kept and composed into the line. It becomes the headline address only while nothing a verification found outranks it: an address read from a government record or a proof-of-address document keeps that place. declaredMonthlyVolume takes an optional declaredVolumeCurrency (an ISO-4217 code such as NGN).

Response 200 OK

json
{
  "entityId": "ent_01j9abc123",
  "externalUserId": "user_42",
  "identityId": "idn_01j9xyz456",
  "changedFields": ["metadata", "nationality", "declaredMonthlyVolume"],
  "screening": null
}

changedFields names what actually moved (empty when nothing did). screening is set only when the wallet set changed and WALLET screening was re-queued. Your endpoints receive entity.updated carrying the same field names, never the values.

Delete an entity

DELETE /entities/:externalUserId: remove the entity from your organisation. The body may carry a reason (up to 500 characters) for your audit log.

Deletion is soft. The entity leaves your dashboard and every Identity Hub read, its ongoing screening is paused (history, adjudications and cleared matches are kept), and its verifications stay where they are. The global identity it resolved to is untouched: that record is shared and belongs to the person. Myaza keeps the entity, marked as deleted, and can restore it on request; there is no restore endpoint.

Registering the same externalUserId again brings it back. A new POST /entities, a new verification for that user, or activity for it through the events API revives the deleted entity in place rather than creating a second one, because the reference is the key.

shell
curl -X DELETE "https://trust.myaza.app/api/identity/entities/user_42" \
  -H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Account closed at the customer\u2019s request" }'

Response 200 OK

json
{
  "entityId": "ent_01j9abc123",
  "externalUserId": "user_42",
  "deleted": true,
  "deletedAt": "2026-09-05T10:12:00.000Z",
  "screeningPaused": true
}

Your endpoints receive entity.deleted. A deleted entity answers 404 entity_not_found on every other endpoint, the same as one that never existed.

Bulk import

For backfilling an existing user base, POST /entities/import accepts 1–1000 entities in one call. It's durable and asynchronous: the request is persisted and acknowledged with 202 immediately, then a worker processes each item in the background (nothing is lost on a restart).

Request

json
{
  "entities": [
    { "externalUserId": "user_1", "type": "INDIVIDUAL", "kycProvenance": "EXTERNAL_VERIFIED", "kycSource": "acme-kyc" },
    { "externalUserId": "user_2", "type": "BUSINESS" }
  ]
}

Each element uses the same shape as a single entity registration.

Response

json
{ "jobId": "job_01j9def789", "status": "PROCESSING", "total": 2 }

Poll import progress

GET /entities/import/:jobId:

json
{
  "jobId": "job_01j9def789",
  "status": "COMPLETED",
  "totalCount": 2,
  "succeededCount": 2,
  "failedCount": 0,
  "pendingCount": 0,
  "createdAt": "2026-04-27T12:00:00.000Z",
  "completedAt": "2026-04-27T12:00:05.000Z",
  "failures": []
}

failures lists up to 50 failed items with externalUserId and an error string so you can retry them. An unknown job returns 404 { "error": "import_job_not_found" }.

Address surface

Entities whose flow captured an address pin carry an address surface:

MethodEndpointWhat it does
POST/entities/:externalUserId/address/verifyMint a presence watch for the captured pin. One live watch per entity; a repeat call returns the existing one (existing: true). 422 no_address_pin when no pin was ever captured.
POST/entities/:externalUserId/address/revokeRevoke the live watch.
POST/entities/:externalUserId/address/reattestA short-window watch answering "still at this address: yes / no / unknown". Billed on resolution like any watch; never live whereabouts.
GET/entities/:externalUserId/addressThe stability attestation: capture tier, presence outcome, months of tenure, strong/moderate/weak grade, and moves in 24 months.
GET/entities/:externalUserId/address/packetThe navigation packet for delivery and recovery teams: the pin with its Plus Code, the entrance-photo URL and the directions your own flow collected.

Monitoring switches

Two per-entity switches, the same ones the dashboard's entity page offers. Both are idempotent: switching on what is already on changes nothing and says so.

MethodEndpointWhat it does
POST/entities/:externalUserId/screening/ongoingBody { "ongoing": true } enrols the entity in sanctions, PEP and adverse-media screening where it is not yet enrolled and resumes any paused rows, so it is re-screened on your risk-based cadence; false pauses them. A pause keeps every past result and adjudication. Returns { ongoing, active, changed }: changed counts the screening rows that moved, and active: false means screening is not running for your organisation at all, so nothing was enrolled.
POST/entities/:externalUserId/address/always-onBody { "enabled": true, "cadenceDays": 90 } turns the entity's presence watch into an always-on chain that re-checks presence every cadenceDays (30, 60, 90, 180 or 365; default 90) and renews itself, minting a watch from the captured pin when none is live (422 no_address_pin when there is no pin). { "enabled": false } stops the chain renewing; the check in flight still runs to its verdict. Returns { enabled, watchId, minted, cadenceDays } or { enabled: false, stopped }. Always-on monitoring is billed per entity per year at the cadence's rate.

Webhooks

Registering entities emits webhook events you can subscribe to:

EventWhen it firesdata
entity.importedA new entity was created.entityId, identityId, externalUserId, environment
identity.resolvedAn entity was linked to a global identity (new or existing).entityId, identityId, environment, matchedExisting
entity.updatedYou edited an entity.entityId, externalUserId, identityId, type, changedFields (names only, never values), environment
entity.deletedYou deleted an entity.entityId, externalUserId, identityId, type, deletedAt, environment
entity.restoredMyaza restored an entity you had deleted, at your request.entityId, externalUserId, identityId, type, restoredAt, environment

Errors

StatusBodyCause
400{ "error": "invalid_request", "details": … }The body failed validation; details is a field-level breakdown.
401{ "error": "Invalid API key" }Missing, unknown, or revoked key.
403{ "error": "secret_key_required" }A publishable (pk_) key was used; Identity Hub is secret-key only.
400{ "error": "field_not_editable", "fields": [...] }A patch tried to change identity facts on a Myaza-verified entity.
404{ "error": "entity_not_found" }No entity with that externalUserId in this org + environment, or it has been deleted.

See Errors for the platform-wide list.

Next steps