Appearance
Customers
Onboard a customer and open its main wallet
POST
/api/v1/tenant/customersScope customers:write · Write — send Idempotency-Key and X-Request-Id
Request body
| Field | Type | Required | Description |
|---|---|---|---|
customerType | enum | yes | One of person, organization. |
displayName | string | yes | |
dateOfBirth | object | no | ISO-8601 date. |
regionCode | object | no | |
preferredLocale | object | no | BCP-47; defaults to en. |
identifiers | object[] | yes | |
identifiers[].class | enum | yes | One of contact, identity_document. |
identifiers[].type | string | yes | e.g. msisdn, email, national_id, passport. |
identifiers[].value | string | yes | Clear-text value; reads return it masked. |
identifiers[].isPrimary | boolean | yes | |
identifiers[].verificationStatus | enum | yes | One of unverified, pending, verified, rejected, expired, revoked. |
identifiers[].verifiedAt | object | no | ISO-8601. |
identifiers[].issuingCountry | object | no | ISO-3166-1 alpha-2. |
kyc | object | yes | |
kyc.tierCode | string | yes | Code of an active KYC level, e.g. tier_1. |
kyc.reviewType | enum | yes | One of onboarding. |
kyc.verificationSource | string | yes | |
kyc.sourceReference | string | yes | Opaque reference into your KYC system. |
kyc.evidence | object | yes | |
kyc.riskRating | enum | yes | One of low, medium, high, prohibited. |
agreements | object[] | yes | |
agreements[].agreementType | enum | yes | One of wallet_opening, privacy, fee_schedule, mandate, other. |
agreements[].documentType | string | yes | |
agreements[].documentVersion | string | yes | |
agreements[].documentUri | string | yes | |
agreements[].documentHash | string | yes | SHA-256 of the accepted document, 64 hex characters. |
agreements[].acceptedAt | string | yes | ISO-8601. |
agreements[].acceptedChannel | string | yes | Your channel the consent was captured in. |
agreements[].evidence | object | yes | |
rulesProfileKey | object | no | The wallet profile to open the customer's wallet on; it decides what the wallet can do. Must be one your account is approved for. Omit it to use your account's default profile. |
clientReference | object | no | Your own reference; stored and returned verbatim, never interpreted. |
metadata | object | no |
Example (generated from the schema)
json
{
"customerType": "person",
"displayName": "string",
"identifiers": [
{
"class": "contact",
"type": "string",
"value": "string",
"isPrimary": true,
"verificationStatus": "unverified"
}
],
"kyc": {
"tierCode": "string",
"reviewType": "onboarding",
"verificationSource": "string",
"sourceReference": "string",
"evidence": {},
"riskRating": "low"
},
"agreements": [
{
"agreementType": "wallet_opening",
"documentType": "string",
"documentVersion": "string",
"documentUri": "string",
"documentHash": "string",
"acceptedAt": "2026-09-03T10:15:30.000Z",
"acceptedChannel": "string",
"evidence": {}
}
]
}Response · 201
| Field | Type | Description |
|---|---|---|
uid | string | |
status | enum | One of pending, active, restricted, suspended, deceased, closed. |
customerType | enum | One of person, organization. |
displayName | string | |
walletUid | string | The customer's main wallet; you can credit it immediately (POST /tenant/wallet-credits). |
kycTierCode | string | Code of the customer's KYC level, e.g. tier_1. |
rulesProfileKey | string | The wallet profile the customer's wallet was opened on. |
identifiers | object[] | |
identifiers[].uid | string | |
identifiers[].class | enum | One of contact, identity_document. |
identifiers[].type | string | |
identifiers[].maskedValue | string | Masked; the raw value is never returned in this field. |
identifiers[].isPrimary | boolean | |
identifiers[].verificationStatus | enum | One of unverified, pending, verified, rejected, expired, revoked. |
clientReference | object | null | Your reference, exactly as you supplied it. |
createdAt | string |
Example (generated from the schema)
json
{
"uid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"status": "pending",
"customerType": "person",
"displayName": "string",
"walletUid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"kycTierCode": "string",
"rulesProfileKey": "string",
"identifiers": [
{
"uid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"class": "contact",
"type": "string",
"maskedValue": "string",
"isPrimary": true,
"verificationStatus": "unverified"
}
],
"clientReference": {},
"createdAt": "2026-09-03T10:15:30.000Z"
}Errors
400 401 403 404 409 422 429 500 503 — one body shape for all of them, see Errors.
Customer summary: status, KYC, wallets, identifiers, agreements, mandates
GET
/api/v1/tenant/customers/{uid}Scope customers:read
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
uid | string | yes |
Response · 200
| Field | Type | Description |
|---|---|---|
uid | string | |
status | enum | One of pending, active, restricted, suspended, deceased, closed. |
customerType | enum | One of person, organization. |
displayName | string | |
kyc | object | null | |
kyc.tierCode | string | Code of the customer's KYC level, e.g. tier_1. |
kyc.tierName | string | Display name of the KYC level. |
kyc.riskRating | enum | One of low, medium, high, prohibited. |
kyc.approvedAt | string | |
kyc.limits | object | |
kyc.limits.currency | string | |
kyc.limits.maxBalanceMinor | number | Integer minor units (e.g. 250000 = ETB 2,500.00). |
kyc.limits.maxSingleTxnMinor | object | null | Minor units; null where the KYC level sets no limit. |
kyc.limits.aggregateDailyLimitMinor | number | Minor units. |
kyc.limits.monthlyOutLimitMinor | object | null | Minor units; null where the KYC level sets no limit. |
wallets | object[] | |
wallets[].walletUid | string | |
wallets[].purpose | enum | One of main, incentive, commission, savings, other. |
wallets[].status | enum | One of pending, active, debit_blocked, credit_blocked, frozen, dormant, closed. |
wallets[].currency | string | |
wallets[].rulesProfileKey | string | The wallet profile the wallet was opened on. |
wallets[].balance | object | null | |
wallets[].balance.currency | string | |
wallets[].balance.currentMinor | number | Minor units. |
wallets[].balance.heldMinor | number | Minor units. |
wallets[].balance.availableMinor | number | Minor units. |
wallets[].balance.asOf | string | |
identifiers | object[] | |
identifiers[].uid | string | |
identifiers[].class | enum | One of contact, identity_document. |
identifiers[].type | string | |
identifiers[].maskedValue | string | Masked; the raw value is never returned in this field. |
identifiers[].isPrimary | boolean | |
identifiers[].verificationStatus | enum | One of unverified, pending, verified, rejected, expired, revoked. |
identifiers[].value | object | null | Clear text only when you hold the customers:pii scope and class is contact; identity documents are never unmasked. Every unmasked read is audited. |
agreements | object[] | |
agreements[].agreementType | enum | One of wallet_opening, privacy, fee_schedule, mandate, other. |
agreements[].documentVersion | string | |
agreements[].acceptedAt | string | |
mandatesCount | number | Number of debit mandates you hold on this customer. |
clientReference | object | null | Your reference, exactly as you supplied it. |
createdAt | string |
Example (generated from the schema)
json
{
"uid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"status": "pending",
"customerType": "person",
"displayName": "string",
"kyc": {
"tierCode": "string",
"tierName": "string",
"riskRating": "low",
"approvedAt": "2026-09-03T10:15:30.000Z",
"limits": {
"currency": "ETB",
"maxBalanceMinor": 0,
"maxSingleTxnMinor": {},
"aggregateDailyLimitMinor": 0,
"monthlyOutLimitMinor": {}
}
},
"wallets": [
{
"walletUid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"purpose": "main",
"status": "pending",
"currency": "ETB",
"rulesProfileKey": "string",
"balance": {
"currency": "ETB",
"currentMinor": 0,
"heldMinor": 0,
"availableMinor": 0,
"asOf": "string"
}
}
],
"identifiers": [
{
"uid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"class": "contact",
"type": "string",
"maskedValue": "string",
"isPrimary": true,
"verificationStatus": "unverified",
"value": {}
}
],
"agreements": [
{
"agreementType": "wallet_opening",
"documentVersion": "string",
"acceptedAt": "2026-09-03T10:15:30.000Z"
}
],
"mandatesCount": 0,
"clientReference": {},
"createdAt": "2026-09-03T10:15:30.000Z"
}Errors
400 401 403 404 429 500 503 — one body shape for all of them, see Errors.
List a customer's identifiers, masked
GET
/api/v1/tenant/customers/{uid}/identifiersScope customers:read
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
uid | string | yes |
Response · 200
| Field | Type | Description |
|---|---|---|
data | object[] | |
data[].uid | string | |
data[].class | enum | One of contact, identity_document. |
data[].type | string | |
data[].maskedValue | string | Masked; the raw value is never returned in this field. |
data[].isPrimary | boolean | |
data[].verificationStatus | enum | One of unverified, pending, verified, rejected, expired, revoked. |
data[].issuingCountry | object | null | |
data[].verifiedAt | object | null | |
data[].validFrom | string | |
data[].validTo | object | null | |
data[].payoutEligible | boolean | True for a verified contact identifier inside its validity window; only these can be a payout destination. |
Example (generated from the schema)
json
{
"data": [
{
"uid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"class": "contact",
"type": "string",
"maskedValue": "string",
"isPrimary": true,
"verificationStatus": "unverified",
"issuingCountry": {},
"verifiedAt": {},
"validFrom": "2026-09-03T10:15:30.000Z",
"validTo": {},
"payoutEligible": true
}
]
}Errors
400 401 403 404 429 500 503 — one body shape for all of them, see Errors.
Attach an identifier to a customer
POST
/api/v1/tenant/customers/{uid}/identifiersScope customers:write · Write — send Idempotency-Key and X-Request-Id
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
uid | string | yes |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
class | enum | yes | One of contact, identity_document. |
type | string | yes | e.g. msisdn, email, national_id, passport. |
value | string | yes | Clear-text value; reads return it masked. |
isPrimary | boolean | no | Demotes the previous primary of the same class. |
verificationStatus | enum | yes | One of unverified, pending, verified, rejected, expired, revoked. |
verifiedAt | object | no | ISO-8601. |
issuingCountry | object | no | ISO-3166-1 alpha-2. |
Example (generated from the schema)
json
{
"class": "contact",
"type": "string",
"value": "string",
"verificationStatus": "unverified"
}Response · 201
| Field | Type | Description |
|---|---|---|
uid | string | |
class | enum | One of contact, identity_document. |
type | string | |
maskedValue | string | Masked; the raw value is never returned in this field. |
isPrimary | boolean | |
verificationStatus | enum | One of unverified, pending, verified, rejected, expired, revoked. |
issuingCountry | object | null | |
verifiedAt | object | null | |
validFrom | string | |
validTo | object | null | |
payoutEligible | boolean | True for a verified contact identifier inside its validity window; only these can be a payout destination. |
Example (generated from the schema)
json
{
"uid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"class": "contact",
"type": "string",
"maskedValue": "string",
"isPrimary": true,
"verificationStatus": "unverified",
"issuingCountry": {},
"verifiedAt": {},
"validFrom": "2026-09-03T10:15:30.000Z",
"validTo": {},
"payoutEligible": true
}Errors
400 401 403 404 409 422 429 500 503 — one body shape for all of them, see Errors.
Update an identifier's verification state
PATCH
/api/v1/tenant/customers/{uid}/identifiers/{iid}Scope customers:write · Write — send Idempotency-Key and X-Request-Id
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
uid | string | yes | |
iid | string | yes |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
verificationStatus | enum | yes | One of unverified, pending, verified, rejected, expired, revoked. |
verifiedAt | object | null | yes | ISO-8601; required when verificationStatus is verified. |
validTo | object | no | ISO-8601; retires the identifier at this instant. |
Example (generated from the schema)
json
{
"verificationStatus": "unverified",
"verifiedAt": {}
}Response · 200
| Field | Type | Description |
|---|---|---|
uid | string | |
class | enum | One of contact, identity_document. |
type | string | |
maskedValue | string | Masked; the raw value is never returned in this field. |
isPrimary | boolean | |
verificationStatus | enum | One of unverified, pending, verified, rejected, expired, revoked. |
issuingCountry | object | null | |
verifiedAt | object | null | |
validFrom | string | |
validTo | object | null | |
payoutEligible | boolean | True for a verified contact identifier inside its validity window; only these can be a payout destination. |
Example (generated from the schema)
json
{
"uid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"class": "contact",
"type": "string",
"maskedValue": "string",
"isPrimary": true,
"verificationStatus": "unverified",
"issuingCountry": {},
"verifiedAt": {},
"validFrom": "2026-09-03T10:15:30.000Z",
"validTo": {},
"payoutEligible": true
}Errors
400 401 403 404 409 422 429 500 503 — one body shape for all of them, see Errors.
List a customer's document acceptances
GET
/api/v1/tenant/customers/{uid}/agreementsScope customers:read
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
uid | string | yes |
Response · 200
| Field | Type | Description |
|---|---|---|
data | object[] | |
data[].uid | string | |
data[].agreementType | enum | One of wallet_opening, privacy, fee_schedule, mandate, other. |
data[].documentType | string | |
data[].documentVersion | string | |
data[].documentUri | string | |
data[].documentHash | string | |
data[].status | enum | One of accepted, revoked, expired, superseded. |
data[].acceptedAt | string | |
data[].acceptedChannel | string | |
data[].evidenceHash | string | SHA-256 of the canonical consent evidence. |
Example (generated from the schema)
json
{
"data": [
{
"uid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"agreementType": "wallet_opening",
"documentType": "string",
"documentVersion": "string",
"documentUri": "string",
"documentHash": "string",
"status": "accepted",
"acceptedAt": "2026-09-03T10:15:30.000Z",
"acceptedChannel": "string",
"evidenceHash": "string"
}
]
}Errors
400 401 403 404 429 500 503 — one body shape for all of them, see Errors.
Record a customer's acceptance of a document
POST
/api/v1/tenant/customers/{uid}/agreementsScope customers:write · Write — send Idempotency-Key and X-Request-Id
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
uid | string | yes |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
agreementType | enum | yes | One of wallet_opening, privacy, fee_schedule, mandate, other. |
documentType | string | yes | e.g. terms, fee_schedule_v3, privacy_notice. |
documentVersion | string | yes | |
documentUri | string | yes | |
documentHash | string | yes | SHA-256 of the accepted document, 64 hex characters. |
acceptedAt | string | yes | ISO-8601. |
acceptedChannel | string | yes | Your channel the consent was captured in; portal is not accepted. |
evidence | object | yes | Your proof of the acceptance; never returned — evidenceHash stands in for it. |
Example (generated from the schema)
json
{
"agreementType": "wallet_opening",
"documentType": "string",
"documentVersion": "string",
"documentUri": "string",
"documentHash": "string",
"acceptedAt": "2026-09-03T10:15:30.000Z",
"acceptedChannel": "string",
"evidence": {}
}Response · 201
| Field | Type | Description |
|---|---|---|
uid | string | |
agreementType | enum | One of wallet_opening, privacy, fee_schedule, mandate, other. |
documentType | string | |
documentVersion | string | |
documentUri | string | |
documentHash | string | |
status | enum | One of accepted, revoked, expired, superseded. |
acceptedAt | string | |
acceptedChannel | string | |
evidenceHash | string | SHA-256 of the canonical consent evidence. |
Example (generated from the schema)
json
{
"uid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"agreementType": "wallet_opening",
"documentType": "string",
"documentVersion": "string",
"documentUri": "string",
"documentHash": "string",
"status": "accepted",
"acceptedAt": "2026-09-03T10:15:30.000Z",
"acceptedChannel": "string",
"evidenceHash": "string"
}Errors
400 401 403 404 409 422 429 500 503 — one body shape for all of them, see Errors.
Submit a KYC update or KYC level change for a customer
POST
/api/v1/tenant/customers/{uid}/kyc-versionsScope customers:write · Write — send Idempotency-Key and X-Request-Id
Adds a new KYC version to the customer's history; earlier versions are never edited. A move to Level 1 (upgrade/downgrade to tier_1, risk rating not prohibited) is approved on your declaration alone and takes effect immediately: the previous version is superseded, the customer's wallets move to the new KYC level, a wallet whose balance is above that level's maximum is flagged credit_blocked, and customer.kyc_changed is delivered to your webhook. Every other submission is pending until Neo Wallet's compliance review approves or rejects it (the webhook fires then). An upgrade/downgrade must move the KYC level's maximum balance in the direction it names (422). Only one pending version per customer (409).
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
uid | string | yes |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
tierCode | string | yes | Code of an active KYC level, e.g. tier_1. |
reviewType | enum | yes | One of upgrade, downgrade, periodic_review, event_driven_review. |
verificationSource | string | yes | |
sourceReference | string | yes | Opaque reference into your KYC system. |
evidence | object | yes | Your verification evidence; never returned — evidenceHash stands in for it. |
riskRating | enum | yes | One of low, medium, high, prohibited. |
Example (generated from the schema)
json
{
"tierCode": "string",
"reviewType": "upgrade",
"verificationSource": "string",
"sourceReference": "string",
"evidence": {},
"riskRating": "low"
}Response · 201
| Field | Type | Description |
|---|---|---|
uid | string | |
customerUid | string | |
tierCode | string | Code of the KYC level this version asserts. |
reviewType | enum | One of upgrade, downgrade, periodic_review, event_driven_review. |
state | enum | approved when your assertion is accepted outright (a move to Level 1); otherwise pending until Neo Wallet's compliance review decides, announced by the customer.kyc_changed webhook. One of pending, approved, rejected, expired, superseded. |
verificationSource | string | |
sourceReference | string | null | |
riskRating | enum | null | One of low, medium, high, prohibited. |
evidenceHash | string | SHA-256 of the canonical evidence JSON. |
effectiveFrom | string | null | Set when the version is approved. |
createdAt | string |
Example (generated from the schema)
json
{
"uid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"customerUid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
"tierCode": "string",
"reviewType": "upgrade",
"state": "pending",
"verificationSource": "string",
"sourceReference": "string",
"riskRating": "low",
"evidenceHash": "string",
"effectiveFrom": "string",
"createdAt": "2026-09-03T10:15:30.000Z"
}Errors
400 401 403 404 409 422 429 500 503 — one body shape for all of them, see Errors.