On this page
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:
https://trust.myaza.app/api/identityEvery 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.
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
{
"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" }
]
}
}| Field | Required | Notes |
|---|---|---|
externalUserId | Yes | Your own stable reference for the user. The entity's primary key within your org + environment. |
metadata | No | Your 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. |
type | No | INDIVIDUAL (default) or BUSINESS. |
kycProvenance | No | How the entity was KYC'd: MYAZA_VERIFIED, EXTERNAL_VERIFIED, or UNVERIFIED (default). |
kycSource | No | Free-text label of the external verification source, when kycProvenance is EXTERNAL_VERIFIED. |
profile | No | Declared attributes, all optional. nationality and declared fields feed monitoring rules (e.g. cross-border). |
profile.walletAddresses | No | Up 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:
{
"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"] }
}metadataechoes your customer metadata exactly as stored.identityIdis the entity's link to a global identity: a person or company shared across organisations and environments. It'snulluntil resolution links one (see below).screeningreflects screening enrolment. It's the string"INACTIVE"when screening is not configured for your organisation, never a fakeQUEUED.
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.
{
"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.)
{
"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
{
"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.
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
{
"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
{
"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
{ "jobId": "job_01j9def789", "status": "PROCESSING", "total": 2 }Poll import progress
GET /entities/import/:jobId:
{
"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:
| Method | Endpoint | What it does |
|---|---|---|
POST | /entities/:externalUserId/address/verify | Mint 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/revoke | Revoke the live watch. |
POST | /entities/:externalUserId/address/reattest | A short-window watch answering "still at this address: yes / no / unknown". Billed on resolution like any watch; never live whereabouts. |
GET | /entities/:externalUserId/address | The stability attestation: capture tier, presence outcome, months of tenure, strong/moderate/weak grade, and moves in 24 months. |
GET | /entities/:externalUserId/address/packet | The 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.
| Method | Endpoint | What it does |
|---|---|---|
POST | /entities/:externalUserId/screening/ongoing | Body { "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-on | Body { "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:
| Event | When it fires | data |
|---|---|---|
entity.imported | A new entity was created. | entityId, identityId, externalUserId, environment |
identity.resolved | An entity was linked to a global identity (new or existing). | entityId, identityId, environment, matchedExisting |
entity.updated | You edited an entity. | entityId, externalUserId, identityId, type, changedFields (names only, never values), environment |
entity.deleted | You deleted an entity. | entityId, externalUserId, identityId, type, deletedAt, environment |
entity.restored | Myaza restored an entity you had deleted, at your request. | entityId, externalUserId, identityId, type, restoredAt, environment |
Errors
| Status | Body | Cause |
|---|---|---|
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
- Event monitoring: stream this entity's transactions for scoring.
- Screening: how watchlist enrolment and matches work.