Customer Management

AI skills for customer onboarding, customer lookup, and admin user management.

Overview

This skill enables AI agents and applications to onboard new customers, look up existing customers, and manage tenant admin users using Eclipse APIs.

It supports:

  • Onboarding new customers (create record → trigger KYC → check eligibility → provision wallet)
  • Looking up customers by phone, identity number, or name
  • Creating and managing tenant admin portal users

All base URLs follow the pattern: {baseUrl}/eclipse-conductor/rest/v1/tenants/{tenantId}/... for tenant-scoped operations. Phone numbers must be digits only with no + prefix (e.g. 27821234567).


1. Customer Onboarding

Trigger: "onboard a new customer", "create a customer", "register a user"

Required inputs: tenantId, firstName, lastName, phone1, email, userTypeId, plus one identity document — nationalIdentityNumber, or a passport (see Identity Document Alternatives). externalUniqueId is optional.

New customer onboarding follows four steps: create the customer record, trigger KYC, check wallet eligibility, then provision the wallet. KYC must run before wallet provisioning, but there is no separate "wait for KYC to resolve" step — wallet eligibility is evaluated synchronously against the customer's current KYC state each time you call it, so you check eligibility immediately after ratification returns.

Step 1 — Create Customer

POST {baseUrl}/eclipse-conductor/rest/v1/tenants/{tenantId}/customers
Authorization: Bearer {jwt}
Content-Type: application/json
{
  "firstName": "Sipho",
  "lastName": "Dlamini",
  "phone1": "27821234567",
  "email": "[email protected]",
  "nationalIdentityNumber": "9001015009087",
  "userTypeId": 1,
  "externalUniqueId": "cust-a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

externalUniqueId is optional — use a UUID unique to this customer if you need to reference it later by that value. There is no version field on customer creation.

Response:

{
  "customerId": 3847291,
  "firstName": "Sipho",
  "lastName": "Dlamini",
  "phone1": "27821234567",
  "email": "[email protected]",
  "nationalIdentityNumber": "9001015009087",
  "status": "ACTIVE",
  "userTypeId": 1,
  "externalUniqueId": "cust-a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "created": "2026-05-19T08:30:00.000Z"
}

Identity Document Alternatives

South African ID holders provide nationalIdentityNumber. Passport holders provide these instead — and must also supply a visa/residence permit:

FieldRequiredFormat
passportNumberYes5–20 alphanumeric characters
passportCountryYesISO 3166-1 alpha-2, e.g. ZA
passportExpiryYesyyyyMMdd, must not be expired
passportPlaceOfIssuanceYes2–60 characters
permitNumberYesFree text
permitExpiryYesyyyyMMdd, must not be expired

The permit is required because ratification checks lastNameMatchesPermit, passportNumberMatchesPermit, and isPermitExpiryDateMatch against it. Upload both a PASSPORT and a PERMIT document before ratifying — see Upload Identity Documents.

Switching a customer between the two document types on a later update? Set the unused type's fields to null explicitly — omitting them leaves the old value on file.

Step 2 — Trigger KYC

POST {baseUrl}/eclipse-conductor/rest/v2/tenants/{tenantId}/customers/{customerId}/ratify
Authorization: Bearer {jwt}
Content-Type: application/json

Upload the customer's identity document(s) first — see Upload Identity Documents — otherwise checks like nationalIdentityIsLegitimate have nothing to verify against.

Request body may be empty ({}) to run the default check set, or include type (NORMAL or COMPARISON) and checksToRun — a comma-separated string of check names — to limit which checks are executed.

Ratification is synchronous — the endpoint runs the configured checks and returns the result in the same response. There is no separate polling step.

Response: An EclipseKycResultV2 object with one field per configured check (e.g. sanctionsListCheck, selfieMatchesNationalIdentity, nationalIdentityIsLegitimate) plus lastModified. There is no single top-level status field — read each relevant check individually. Each check field has checked (was it run), passed (did it pass), pending (still in progress), and inReview (awaiting manual review) booleans.

{
  "lastModified": "2026-05-19T08:30:20.000Z",
  "sanctionsListCheck": { "checked": true, "passed": true, "pending": false },
  "selfieMatchesNationalIdentity": { "checked": true, "passed": true, "pending": false }
}
📘

Note

pending: true on a check field means that specific check is still in progress — it does not mean the overall ratify call is incomplete (the call already returned). See KYC & Compliance for how to interpret individual check results.

Step 3 — Check Wallet Eligibility

Before provisioning a wallet, confirm which wallet types the customer is eligible for. Eligibility is evaluated on demand against each wallet type's KYC ruleset — it is not a stored value you wait on, so call it immediately after Step 2 returns, and treat it as authoritative: a passed: false on an individual check does not by itself mean no wallet can be provisioned, since that check may not be part of the ruleset for the wallet type you need.

GET {baseUrl}/eclipse-conductor/rest/v1/tenants/{tenantId}/customers/{customerId}/wallet-types
Authorization: Bearer {jwt}

Response:

[
  { "walletTypeId": 5001, "allowed": true },
  { "walletTypeId": 5002, "allowed": false, "reason": "sanctionsListCheck failed" }
]

reason (only populated for expression-based rulesets) explains why a type is disallowed, or why the wallet will be created BARRED if allowed. This response has no name field — if you need the wallet type's name (e.g. to select a specific type like "Card Enabled Digital Wallet" among several eligible ones), cross-reference walletTypeId against GET {baseUrl}/eclipse-conductor/rest/v1/tenants/{tenantId}/wallet-types, which does return name.

Only provision a wallet for a walletTypeId where allowed is true. Do not proceed if no eligible types are returned. See KYC/B Rulesets for how a wallet type's ruleset determines allowed.

Step 4 — Create Wallet

POST {baseUrl}/eclipse-conductor/rest/v1/tenants/{tenantId}/customers/{customerId}/wallets
Authorization: Bearer {jwt}
Content-Type: application/json
{
  "walletTypeId": 5001,
  "currency": "ZAR",
  "status": "ACTIVE",
  "externalUniqueId": "wal-a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

walletTypeId is required. status, currency, and externalUniqueId are optional. There is no version field on wallet creation. Use a UUID distinct from the customer's externalUniqueId if you set one.

Response:

{
  "walletId": 1092847,
  "customerId": 3847291,
  "walletTypeId": 5001,
  "name": "Primary Wallet",
  "currency": "ZAR",
  "currentBalance": 0.00,
  "availableBalance": 0.00,
  "status": "ACTIVE",
  "created": "2026-05-19T08:30:15.000Z"
}

Notes

  • externalUniqueId is optional on both customer and wallet creation. If supplied, use a unique UUID for each — duplicate values are rejected with 409 Conflict.
  • There is no version field on customer or wallet creation — but PUT .../customers/{customerId} (updating an existing customer, e.g. to correct details after a failed eligibility check) does require the current version. Fetch the customer fresh (GET .../customers/{customerId}) immediately before the update rather than reusing a copy from earlier in the flow — KYC and document uploads can bump the version in between, and a stale version is rejected with "The data being updated has already been updated by something else."
  • EclipseKycResultV2 has no top-level status — read the individual check fields relevant to the wallet type's KYC rule set. See KYC & Compliance.
  • Wallet eligibility (Step 3) is evaluated live against the current KYC check results each time it's called — it is not gated behind a separate "wait for KYC" step.

API Reference: POST /customers · POST /customers/{id}/ratify (v2) · GET /customers/{id}/wallet-types · POST /customers/{id}/wallets · POST /customers/{id}/documents


2. Customer Lookup

Trigger: "find customer", "look up user", "search for customer", "who is customer X"

Required inputs: At least one of: phone number, name, identity number, or externalId; plus tenantId

Step 1 — Search by Phone or Identity

GET {baseUrl}/eclipse-conductor/rest/v1/tenants/{tenantId}/customers?identity=27821234567&limit=10
Authorization: Bearer {jwt}

Response:

[
  {
    "customerId": 3847291,
    "firstName": "Sipho",
    "lastName": "Dlamini",
    "phone1": "27821234567",
    "email": "[email protected]",
    "nationalIdentityNumber": "9001015009087",
    "status": "ACTIVE",
    "created": "2026-05-19T08:30:00.000Z"
  }
]

Step 2 (Alternative) — Search by Name

GET {baseUrl}/eclipse-conductor/rest/v1/tenants/{tenantId}/customers?firstName=Sipho&lastName=Dlamini&limit=10
Authorization: Bearer {jwt}

Step 3 — Get Full Customer Profile

GET {baseUrl}/eclipse-conductor/rest/v1/tenants/{tenantId}/customers/{customerId}
Authorization: Bearer {jwt}

Response:

{
  "customerId": 3847291,
  "title": "MR",
  "firstName": "Sipho",
  "lastName": "Dlamini",
  "phone1": "27821234567",
  "email": "[email protected]",
  "nationalIdentityNumber": "9001015009087",
  "status": "ACTIVE",
  "created": "2026-05-19T08:30:00.000Z",
  "lastModified": "2026-05-19T08:30:00.000Z"
}

Notes

  • If multiple results are returned, list them and ask the user to confirm which customer they mean.
  • Pagination is supported via limit and offset.
  • The identity query parameter matches against phone number or identity number.

API Reference: GET /customers · GET /customers/{customerId}


3. Admin User Management

Trigger: "create admin user", "add operator user", "new portal user"

Required inputs: tenantId, firstName, lastName, email, phone1, identity, role(s)

Step 1 — Create Admin User

POST {baseUrl}/eclipse-conductor/rest/v1/tenants/{tenantId}/admin-users
Authorization: Bearer {jwt}
Content-Type: application/json
{
  "firstName": "Thembi",
  "lastName": "Nkosi",
  "email": "[email protected]",
  "phone1": "27794567890",
  "identity": "thembi.nkosi",
  "password": "initialPassword123!",
  "positions": ["LEVEL_01"]
}

Response:

{
  "adminUserId": 58291,
  "firstName": "Thembi",
  "lastName": "Nkosi",
  "email": "[email protected]",
  "phone1": "27794567890",
  "identity": "thembi.nkosi",
  "positions": ["LEVEL_01"]
}

Step 2 — List Admin Users

GET {baseUrl}/eclipse-conductor/rest/v1/tenants/{tenantId}/admin-users
Authorization: Bearer {jwt}

Notes

  • identity (the login username) must be unique across the platform. Using email as identity is a common convention.
  • password is required on creation; it may be passed as cleartext (Eclipse will hash it) or pre-hashed with BCrypt.
  • positions controls access level. Valid values: TENANT_SYSTEM, LEVEL_01LEVEL_20, ONBOARDING. The specific permissions attached to each level are configured per tenant.
  • Phone number must be digits only with no + prefix (e.g. 27794567890).

API Reference: POST /admin-users · GET /admin-users


4. Customer Self-Registration (ONBOARDING Pattern)

Trigger: "build a customer onboarding PWA", "self-service registration", "customer signs themselves up", "onboarding flow for a mobile app"

Required inputs: tenantId, ONBOARDING admin user credentials (identity + password), baseUrl

This pattern bootstraps customer-direct API access. An admin user with the ONBOARDING position acts as a semi-public bootstrap credential embedded in the app. It creates the customer record and sets their login credentials, then immediately hands off to a customer-owned JWT. The ONBOARDING JWT is never used again once the customer has authenticated.

📘

Note

The ONBOARDING admin user must be created by a tenant administrator before this flow can run. See Admin User Management — set "positions": ["ONBOARDING"]. The ONBOARDING position is hard-coded to four permissions only: ProfileOfNewCustomer.CREATE.Allowed, ProfileOfNewCustomer.UPDATE.Allowed, ProfileOfNewCustomer.READ.Allowed, and Attachment.CREATE.Allowed. It cannot be granted any other permissions.

Step 1 — Obtain ONBOARDING JWT

Log in as the ONBOARDING admin user to get a bootstrap JWT.

POST {baseUrl}/eclipse-conductor/rest/v1/authentication/login
Content-Type: application/json
{
  "identity": "onboarding-user",
  "password": "onboardingPassword"
}

Response (200 OK):

{
  "headerName": "Authorization",
  "headerValue": "Bearer eyJhbGci...",
  "expiresEpochSecs": 1774277221
}

Use headerValue as the Authorization header for steps 2–4. Do not store the ONBOARDING credentials in client-side JavaScript — inject them via a backend proxy or environment variable at build time.

Step 2 — Check for Duplicate Customer

Before creating a record, confirm no customer with the same phone number or identity already exists.

HEAD {baseUrl}/eclipse-conductor/rest/v1/tenants/{tenantId}/customers?phone1=27821234567
Authorization: Bearer {onboardingJwt}
ResponseMeaning
200 OKCustomer already exists — prompt user to log in instead
404 Not FoundNo existing customer — proceed to Step 3

Supported query parameters: phone1, email, identity, nationalIdentityNumber, passportNumber, asylumRefNumber. At least one is required.

Step 3 — Create Customer Record

POST {baseUrl}/eclipse-conductor/rest/v1/tenants/{tenantId}/customers
Authorization: Bearer {onboardingJwt}
Content-Type: application/json
{
  "firstName": "Sipho",
  "lastName": "Dlamini",
  "phone1": "27821234567",
  "email": "[email protected]",
  "nationalIdentityNumber": "9001015009087",
  "userTypeId": 1,
  "externalUniqueId": "cust-a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

externalUniqueId is optional — use a unique UUID if you need to reference this customer by it later. There is no version field on customer creation. phone1 must be digits only with no + prefix.

Response (200 OK):

{
  "customerId": 3847291,
  "firstName": "Sipho",
  "lastName": "Dlamini",
  "phone1": "27821234567",
  "status": "ACTIVE"
}

Save customerId — it is required for step 4.

Step 4 — Create Customer Identity (Login Credentials)

This is the critical step. Once a password identity exists on the customer, the ONBOARDING JWT can no longer modify their profile — the access gate is based on whether the customer has zero identities.

POST {baseUrl}/eclipse-conductor/rest/v1/tenants/{tenantId}/customers/{customerId}/identities
Authorization: Bearer {onboardingJwt}
Content-Type: application/json
{
  "identity": "sipho.dlamini",
  "password": "SecureP@ssw0rd123"
}
FieldTypeRequiredDescription
identitystringYesUsername for authentication. 3–40 characters
passwordstringYesCleartext or BCrypt pre-hashed. 3–80 characters
totpEnabledbooleanNoSet true to require TOTP on login
base64EncodedPublicKeystringNoRSA public key for PKI-based authentication

Response (200 OK):

{
  "identity": "sipho.dlamini",
  "userId": 3847291
}

Step 5 — Log In as Customer and Obtain Customer JWT

Discard the ONBOARDING JWT. Authenticate as the newly created customer.

POST {baseUrl}/eclipse-conductor/rest/v1/authentication/login
Content-Type: application/json
{
  "identity": "sipho.dlamini",
  "password": "SecureP@ssw0rd123"
}

Response (200 OK):

{
  "headerName": "Authorization",
  "headerValue": "Bearer eyJhbGci...",
  "expiresEpochSecs": 1774277221
}

The headerValue is the customer JWT. Use it for all subsequent API calls — wallet creation, KYC, card operations, etc.

Notes

  • ONBOARDING JWT is rate-limited. The platform enforces a maximum of 60 mutating requests (POST, PUT, DELETE) per 180 seconds per IP address. A PWA must handle 429 Too Many Requests with backoff.
  • Do not use ONBOARDING JWT after step 4. Once an identity exists on the customer, the ONBOARDING position cannot access their profile. All further operations must use the customer JWT from step 5.
  • ONBOARDING does not run KYC. If your flow requires KYC before wallet provisioning, upload the customer's identity document(s) and trigger ratification after step 5 using the customer JWT or an operator JWT. See Customer Onboarding steps 2–4 and Upload Identity Documents.
  • Check for existing identities before re-running step 4. Use HEAD {baseUrl}/.../customers/{customerId}/identities (returns 200 if identities exist, 404 if none) to avoid a 409 Conflict.
  • ONBOARDING credentials must not be exposed client-side. Treat them as a semi-public secret — any user of the channel can retrieve them if embedded in JavaScript. Use a backend token-exchange endpoint to return the ONBOARDING JWT to the frontend without exposing the underlying credentials.

API Reference: HEAD /customers · POST /customers · POST /customers/{id}/identities · POST /authentication/login


Common Patterns for AI Agents

1. Full Onboarding Flow

  1. Create customer record (optionally include externalUniqueId) — national ID, or passport + permit
  2. Upload identity document(s) — NATIONAL_IDENTITY, or PASSPORT + PERMIT
  3. Trigger KYC ratification — the response returns the check results immediately; there is no async status to poll
  4. Check wallet eligibility (GET .../wallet-types) — confirm at least one allowed: true type. Don't treat a failed check as a hard stop by itself; allowed/reason is the authoritative signal
  5. Not eligible? Surface the failed/pending checks and reason, and offer the customer a chance to correct or supply a better document, then repeat from step 2 or 3
  6. Create wallet (include walletTypeId; status: "ACTIVE" and externalUniqueId are optional)

2. Customer Lookup Before Action

Always look up the customer before performing operations:

  1. Search by phone or identity number
  2. Confirm identity with operator if multiple results are returned
  3. Retrieve full profile to confirm status is ACTIVE

3. Onboarding Checklist

Before completing onboarding, verify:

  • Customer record created (customerId returned)
  • Identity document(s) uploaded and accepted (national ID, or passport + permit)
  • At least one wallet type returned with allowed: true from the eligibility check
  • Wallet created (walletId returned)

Error Handling

[
  {
    "type": "BUSINESS",
    "severity": "LOW",
    "description": "Customer already exists with this phone number",
    "code": "USR004",
    "traceId": "3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d",
    "spanId": "1a2b3c4d5e6f7a8b",
    "environment": "eclipse-sandbox"
  }
]
HTTP StatusMeaningCommon Cause
400Bad requestMissing required field, invalid phone format
401UnauthorisedJWT missing, expired, or malformed
403ForbiddenCaller role does not have permission for this operation
404Not foundCustomer ID does not exist or belongs to a different tenant
409ConflictCustomer with this phone number or identity already exists

When a traceId is present in an error response, always surface it to the user — it is the primary identifier for diagnosing the failure.


Best Practices

  • Include Authorization: Bearer {jwt} on every request. Renew proactively if within 5 minutes of expiry.
  • Always confirm with the operator before re-triggering KYC with no checksToRun — a full re-trigger re-runs every check from scratch (no top-level "status" resets, but previously passed check results are discarded and re-evaluated). This transiently makes checks show pending: true, which can make wallet-type eligibility evaluate as not-allowed until the re-run completes, and re-invokes paid third-party providers. Prefer passing checksToRun (optionally with mergePreviousResult: true) to limit re-ratification to the specific check(s) that need it.
  • Apply limit and offset pagination on all list endpoints. Default to limit=20.
  • Mask sensitive data in output — never log or display full identity numbers.
  • When a traceId is returned in an error, record it and reference it in any support escalation.
  • Tenant-scoped endpoints enforce tenant isolation — never pass a tenantId that does not match the authenticated operator's context.

Did this page help you?