Skip to content

Customers ​

Onboard a customer and open its main wallet ​

POST/api/v1/tenant/customers

Scope customers:write · Write — send Idempotency-Key and X-Request-Id

Request body ​

FieldTypeRequiredDescription
customerTypeenumnoDefaults to person. One of person, organization.
displayNamestringyes
dateOfBirthstring | nullnoISO-8601 date.
regionCodestring | nullno
preferredLocalestring | nullnoBCP-47; defaults to en.
identifiersobject[]yes
identifiers[].classenumnoDerived from type for phone, email and identity-document types; required only for a type Neo Wallet does not recognise. One of contact, identity_document.
identifiers[].typestringyese.g. msisdn, email, national_id, passport.
identifiers[].valuestringyesClear-text value; reads return it masked.
identifiers[].isPrimaryboolean | nullnoThe first identifier is primary unless one of them says otherwise.
identifiers[].verificationStatusenumnoDefaults to verified for a contact identifier (you verified it to reach the customer) and pending for an identity document (Neo Wallet's compliance review verifies those). One of unverified, pending, verified, rejected, expired, revoked.
identifiers[].verifiedAtstring | nullnoISO-8601; defaults to now for a verified identifier.
identifiers[].issuingCountrystring | nullnoISO-3166-1 alpha-2.
kycobjectyes
kyc.tierCodestringyesCode of an active KYC level, e.g. tier_1.
kyc.verificationSourcestring | nullnoThe KYC system or provider that verified the customer; defaults to client_asserted.
kyc.sourceReferencestring | nullnoOpaque reference into your KYC system.
kyc.evidenceobjectyes
kyc.riskRatingenumyesOne of low, medium, high, prohibited.
agreementsobject[]yesThe documents the customer accepted in your channel. wallet_opening, privacy and fee_schedule are required: a request missing any of them is refused with 422 RequiredAgreementsMissing and nothing is created.
agreements[].agreementTypeenumyesOne of wallet_opening, privacy, fee_schedule, mandate, other.
agreements[].documentTypestring | nullnoDefaults to agreementType.
agreements[].documentVersionstringyes
agreements[].documentUristringyes
agreements[].documentHashstringyesSHA-256 of the accepted document, 64 hex characters.
agreements[].acceptedAtstringyesISO-8601.
agreements[].acceptedChannelstring | nullnoYour channel the consent was captured in; defaults to client_channel.
agreements[].evidenceobjectyes
rulesProfileKeystring | nullnoThe 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.
clientReferencestring | nullnoYour own reference for this customer; stored and returned verbatim, never interpreted. Unique among your customers: a second customer with the same value is refused with 409.
metadataobjectno
Example (placeholder values)
json
{
  "displayName": "string",
  "identifiers": [
    {
      "type": "string",
      "value": "string"
    }
  ],
  "kyc": {
    "tierCode": "string",
    "evidence": {},
    "riskRating": "low"
  },
  "agreements": [
    {
      "agreementType": "wallet_opening",
      "documentVersion": "string",
      "documentUri": "string",
      "documentHash": "string",
      "acceptedAt": "2026-09-03T10:15:30.000Z",
      "evidence": {}
    }
  ]
}

Response · 201 ​

FieldTypeDescription
uidstring
statusenumOne of pending, active, restricted, suspended, deceased, closed.
customerTypeenumOne of person, organization.
displayNamestring
walletUidstringThe customer's main wallet; you can credit it immediately (POST /tenant/wallet-credits).
kycTierCodestringCode of the customer's KYC level, e.g. tier_1.
rulesProfileKeystringThe wallet profile the customer's wallet was opened on.
identifiersobject[]
identifiers[].uidstring
identifiers[].classenumOne of contact, identity_document.
identifiers[].typestring
identifiers[].maskedValuestringThe identifier value, masked.
identifiers[].isPrimaryboolean
identifiers[].verificationStatusenumOne of unverified, pending, verified, rejected, expired, revoked.
clientReferencestring | nullYour reference, exactly as you supplied it.
createdAtstring
Example (placeholder values)
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": "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.

Customer summary: status, KYC, wallets, identifiers, agreements, mandates ​

GET/api/v1/tenant/customers/{uid}

Scope customers:read

Path parameters ​

NameTypeRequiredDescription
uidstringyes

Response · 200 ​

FieldTypeDescription
uidstring
statusenumOne of pending, active, restricted, suspended, deceased, closed.
customerTypeenumOne of person, organization.
displayNamestring
kycobject | null
kyc.tierCodestringCode of the customer's KYC level, e.g. tier_1.
kyc.tierNamestringDisplay name of the KYC level.
kyc.riskRatingenumOne of low, medium, high, prohibited.
kyc.approvedAtstring
kyc.limitsobject
kyc.limits.currencystring
kyc.limits.maxBalancestringA decimal amount with two decimal places.
kyc.limits.maxSingleTxnstring | nullA decimal amount; null where the KYC level sets no limit.
kyc.limits.aggregateDailyLimitstringA decimal amount with two decimal places.
kyc.limits.monthlyOutLimitstring | nullA decimal amount; null where the KYC level sets no limit.
walletsobject[]
wallets[].walletUidstring
wallets[].purposeenumOne of main, incentive, commission, savings, other.
wallets[].statusenumOne of pending, active, debit_blocked, credit_blocked, frozen, dormant, closed.
wallets[].currencystring
wallets[].rulesProfileKeystringThe wallet profile the wallet was opened on.
wallets[].balanceobject | null
wallets[].balance.currencystring
wallets[].balance.currentstringA decimal amount with two decimal places.
wallets[].balance.heldstringA decimal amount with two decimal places.
wallets[].balance.availablestringA decimal amount with two decimal places.
wallets[].balance.asOfstring
identifiersobject[]
identifiers[].uidstring
identifiers[].classenumOne of contact, identity_document.
identifiers[].typestring
identifiers[].maskedValuestringThe identifier value, masked.
identifiers[].isPrimaryboolean
identifiers[].verificationStatusenumOne of unverified, pending, verified, rejected, expired, revoked.
identifiers[].payoutEligiblebooleanWhether a payout may go to this identifier: a verified contact identifier that is valid now.
agreementsobject[]
agreements[].agreementTypeenumOne of wallet_opening, privacy, fee_schedule, mandate, other.
agreements[].documentVersionstring
agreements[].acceptedAtstring
mandatesCountnumberNumber of debit mandates you hold on this customer.
clientReferenceobject | nullYour reference, exactly as you supplied it.
createdAtstring
Example (placeholder values)
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",
      "maxBalance": "50000.00",
      "maxSingleTxn": "string",
      "aggregateDailyLimit": "25000.00",
      "monthlyOutLimit": "string"
    }
  },
  "wallets": [
    {
      "walletUid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
      "purpose": "main",
      "status": "pending",
      "currency": "ETB",
      "rulesProfileKey": "string",
      "balance": {
        "currency": "ETB",
        "current": "2500.00",
        "held": "0.00",
        "available": "2500.00",
        "asOf": "string"
      }
    }
  ],
  "identifiers": [
    {
      "uid": "01J9Z3K5X8QW2R4T6V8Y0B1C3D",
      "class": "contact",
      "type": "string",
      "maskedValue": "string",
      "isPrimary": true,
      "verificationStatus": "unverified",
      "payoutEligible": true
    }
  ],
  "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.

Attach an identifier to a customer ​

POST/api/v1/tenant/customers/{uid}/identifiers

Scope customers:write · Write — send Idempotency-Key and X-Request-Id

Path parameters ​

NameTypeRequiredDescription
uidstringyes

Request body ​

FieldTypeRequiredDescription
classenumyesOne of contact, identity_document.
typestringyese.g. msisdn, email, national_id, passport.
valuestringyesClear-text value; reads return it masked.
isPrimarybooleannoDemotes the previous primary of the same class.
verificationStatusenumyesOne of unverified, pending, verified, rejected, expired, revoked.
verifiedAtobjectnoISO-8601.
issuingCountryobjectnoISO-3166-1 alpha-2.
Example (placeholder values)
json
{
  "class": "contact",
  "type": "string",
  "value": "string",
  "verificationStatus": "unverified"
}

Response · 201 ​

FieldTypeDescription
uidstring
classenumOne of contact, identity_document.
typestring
maskedValuestringThe identifier value, masked.
isPrimaryboolean
verificationStatusenumOne of unverified, pending, verified, rejected, expired, revoked.
issuingCountryobject | null
verifiedAtobject | null
validFromstring
validToobject | null
payoutEligiblebooleanTrue for a verified contact identifier inside its validity window; only these can be a payout destination.
Example (placeholder values)
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 ​

NameTypeRequiredDescription
uidstringyes
iidstringyes

Request body ​

FieldTypeRequiredDescription
verificationStatusenumyesOne of unverified, pending, verified, rejected, expired, revoked.
verifiedAtobject | nullyesISO-8601; required when verificationStatus is verified.
validToobjectnoISO-8601; retires the identifier at this instant.
Example (placeholder values)
json
{
  "verificationStatus": "unverified",
  "verifiedAt": {}
}

Response · 200 ​

FieldTypeDescription
uidstring
classenumOne of contact, identity_document.
typestring
maskedValuestringThe identifier value, masked.
isPrimaryboolean
verificationStatusenumOne of unverified, pending, verified, rejected, expired, revoked.
issuingCountryobject | null
verifiedAtobject | null
validFromstring
validToobject | null
payoutEligiblebooleanTrue for a verified contact identifier inside its validity window; only these can be a payout destination.
Example (placeholder values)
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.

Record a customer's acceptance of a document ​

POST/api/v1/tenant/customers/{uid}/agreements

Scope customers:write · Write — send Idempotency-Key and X-Request-Id

Path parameters ​

NameTypeRequiredDescription
uidstringyes

Request body ​

FieldTypeRequiredDescription
agreementTypeenumyesOne of wallet_opening, privacy, fee_schedule, mandate, other.
documentTypestringyese.g. terms, fee_schedule_v3, privacy_notice.
documentVersionstringyes
documentUristringyes
documentHashstringyesSHA-256 of the accepted document, 64 hex characters.
acceptedAtstringyesISO-8601.
acceptedChannelstringyesYour channel the consent was captured in; portal is not accepted.
evidenceobjectyesYour proof of the acceptance; never returned — evidenceHash stands in for it.
Example (placeholder values)
json
{
  "agreementType": "wallet_opening",
  "documentType": "string",
  "documentVersion": "string",
  "documentUri": "string",
  "documentHash": "string",
  "acceptedAt": "2026-09-03T10:15:30.000Z",
  "acceptedChannel": "string",
  "evidence": {}
}

Response · 201 ​

FieldTypeDescription
uidstring
agreementTypeenumOne of wallet_opening, privacy, fee_schedule, mandate, other.
documentTypestring
documentVersionstring
documentUristring
documentHashstring
statusenumOne of accepted, revoked, expired, superseded.
acceptedAtstring
acceptedChannelstring
evidenceHashstringSHA-256 of the canonical consent evidence.
Example (placeholder values)
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-versions

Scope 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 ​

NameTypeRequiredDescription
uidstringyes

Request body ​

FieldTypeRequiredDescription
tierCodestringyesCode of an active KYC level, e.g. tier_1.
reviewTypeenumyesOne of upgrade, downgrade, periodic_review, event_driven_review.
verificationSourcestringyes
sourceReferencestringyesOpaque reference into your KYC system.
evidenceobjectyesYour verification evidence; never returned — evidenceHash stands in for it.
riskRatingenumyesOne of low, medium, high, prohibited.
Example (placeholder values)
json
{
  "tierCode": "string",
  "reviewType": "upgrade",
  "verificationSource": "string",
  "sourceReference": "string",
  "evidence": {},
  "riskRating": "low"
}

Response · 201 ​

FieldTypeDescription
uidstring
customerUidstring
tierCodestringCode of the KYC level this version asserts.
reviewTypeenumOne of upgrade, downgrade, periodic_review, event_driven_review.
stateenumapproved 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.
verificationSourcestring
sourceReferencestring | null
riskRatingenum | nullOne of low, medium, high, prohibited.
evidenceHashstringSHA-256 of the canonical evidence JSON.
effectiveFromstring | nullSet when the version is approved.
createdAtstring
Example (placeholder values)
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.