Skip to main content
Baobyte AML Suite
Documentation contents

API reference

Customers

20 operations in this area of the contract. Every one declares the permission it requires.

Contract version 0.2.0All areas

Operations

GET/v1/cdd-forms

The CDD questions in force for a customer type and tier

Required permission: onboarding.customer.readgetCDDForm

BRD §7.2-05 requires the CDD/EDD fields and required documents per customer type to be configurable without a deploy, so the form is a read rather than a static asset.

The response carries the configuration provenance — `source` is `pack_default` or `tenant_override`, and `version` is the document version — so a submitted application records which form it was answering. Re-rendering a two-year-old application against today's definition would show a different set of questions than the customer actually saw.

The active document is re-validated against the storage vocabulary on the way out. A form containing a field the platform cannot store is refused HERE rather than at submit time with a customer at the counter (ADR-0024).

Show parameters and responses

Parameters

NameInTypeDescription
customer_typerequiredquerystring`individual` or `corporate` in the shipped document (Q27).
tierrequiredquerytier_1 | tier_2 | tier_3 | corporate—

Responses

  • 200The resolved formCDDForm
  • 404The document defines no such customer type or tier. The message names what it does define.
  • 422—
GET/v1/customers

The customer book

Required permission: onboarding.customer.readlistCustomers

Filters by status, risk and cursor, and free-text searches by `q` against the party's `display_name` and the customer's own `external_ref`, both by case-insensitive substring match against the trigram indexes migration 00022 added.

**`q` does not reach BVN or NIN.** Both are encrypted under the subject's own key with an HMAC blind index beside the ciphertext (ADR-0019), and the blind index is an exact-match structure — it answers "does this value equal this identifier", never "does this identifier contain this substring". `FindPartyByIdentifier` is the only lawful way to look one up, and it takes the whole value. Indexing plaintext identifiers to make substring search possible would put them back outside the encryption boundary this platform exists to keep them inside, so this endpoint does not do it. A front-office user searching by a partial BVN gets no matches on that term, not a slower or approximate one.

Show parameters and responses

Parameters

NameInTypeDescription
statusquerydraft | pending_documents | pending_edd | pending_approval | active | rejected | exited—
riskquerylow | medium | high | prohibited—
qquerystringMatches the party's display name and the customer's own external_ref. Does not search BVN, NIN or any other encrypted identifier — see the operation description.
limitqueryintegerPage size, capped at 200.
cursorquerystringOpaque keyset cursor from a previous response. Offset pagination is deliberately not offered: `OFFSET 500000` is a table scan, and if it were offered it would be used (ADR-0012). Only the collections that document this parameter can be resumed. The rest report `truncated: true` and refuse a cursor rather than silently returning the first page again — see the `truncated` field on the page schemas.

Responses

  • 200CustomersCustomerPage
POST/v1/customers

Onboard a customer from a submitted CDD form

Required permission: onboarding.customer.writeonboardCustomer

The write path BRD §4.1-03 gives the relationship/front-office persona, whose entire job description is "onboarding, CDD capture". Until this operation existed the role held `onboarding.customer.read` and nothing else, so the only job the BRD gives that persona could not be performed by anybody through this API at all.

One call, one transaction: the party with its names, identifiers and attributes; the customer relationship and its tier; and the submitted answers stored verbatim against the form version in force. Splitting it would permit a party with no customer — invisible to every list the front office has — or a customer whose CDD answers were never recorded.

**`answers` is keyed by the form's own field keys.** Call `GET /v1/cdd-forms` for the questions in force; a key that is not on the form, a required question left unanswered, or an answer to a question the form's `show_if` predicate did not ask, are all refused together as a `422` listing every problem at once. A front-office user correcting a form one field per round trip is how a five-minute account opening becomes a twenty-minute one.

**Numbers are decoded with `UseNumber` and are never rounded.** A number answered on the form is compared against the field's declared type as an integer, not as a float.

**No identifier is recorded as verified.** `verified_by_source` is unreachable from this path: identifiers are captured, encrypted and blind-indexed as UNVERIFIED, because there is no NIBSS or NIMC adapter and the previous behaviour took the caller's word for it (Q26).

Identifier requirement sets are enforced with the rest of the form. The official CBN baseline is Tier 1 individuals: BVN or NIN; Tiers 2/3: BVN and NIN. Submission proves presence only. Relationship approval separately requires authoritative electronic verification and refuses a mock or unverified value.

The form's document requirements are snapshotted onto the application. A relationship starts in `pending_documents` when that snapshot has requirements and advances to `pending_approval` as evidence is collected; a form with no required evidence starts directly in `pending_approval`. Approval still requires independent verification of every required, unexpired object (Q32 owns whether the configured list is correct).

Show parameters and responses

Parameters

NameInTypeDescription
Idempotency-KeyheaderstringThe caller's own reference for this operation. ADR-0012 requires it on mutating endpoints because payment-adjacent callers retry and a duplicate filing is a regulatory incident. On the ingest endpoints it is not a separate identity: it supplies `external_ref` when the body omits it, and the uniqueness of (tenant, source, external_ref, booking_ts) in the database is what makes the retry harmless (ADR-0014). Sending both with different values is refused rather than resolved.

Responses

  • 201The customer, the party behind it, the stored application and the form it answeredOnboardResult
  • 400—
  • 409An identifier given already belongs to a party this platform holds. Refused rather than attached: deciding two records are the same person is a merge, and a merge performed implicitly by an account-opening screen is how two customers become one.
  • 422—
  • 503No CDD form definitions can be resolved on this installation, so no customer can be onboarded. A deployment fault rather than a problem with the request.
GET/v1/customers/{customerID}

A customer profile with its risk decomposition

Required permission: onboarding.customer.readgetCustomer

Returns the profile, the party behind it, its accounts, the latest risk assessment with per-factor contributions, and any resolved beneficial owners — in one response.

Identifiers are returned masked. Decrypting them is a separate, audited action (ADR-0019), because reading a customer's BVN is an event a supervisor is entitled to see a record of.

`assigned_org_unit` is present when the relationship has been routed to a branch or team (`POST /v1/customers/{customerID}/assign`), naming the unit's id and current name rather than only its id — the name is looked up at read time, not denormalised, so a renamed unit does not require rewriting every customer pointed at it.

`pending_application_id` names the customer's onboarding application still awaiting a decision, so the customer workspace and the application-review screen (`GET /v1/onboarding/applications/{applicationID}`) can be linked directly. Absent once the application has been decided, or if the customer has none outstanding.

`screening_hits` carries the party's sanctions and PEP matches, under a SECOND permission on top of the one that opens this route: a caller holding `onboarding.customer.read` but not `screening.hit.read` gets the profile with the key absent rather than empty, so reading the customer book cannot be used to acquire a screening surface the role was never granted.

`corporate_profile` is present when the party is a legal entity, and carries whether its beneficial-ownership register has been VERIFIED and not merely declared — see `GET /v1/customers/{customerID}/risk-assessments` for the sibling endpoint this response does NOT try to be: `risk_assessment` above is only the latest rating, and the full history lives there rather than growing this response without bound.

Show parameters and responses

Parameters

NameInTypeDescription
customerIDrequiredpathstring—

Responses

  • 200The customer workspaceCustomerDetail
  • 404—
PATCH/v1/customers/{customerID}

Correct external_ref, or propose a governed customer-profile change

Required permission: onboarding.customer.writeupdateCustomer

Until this operation existed there was no way to correct a customer once onboarded: `external_ref`, `segment`, `kyc_tier` and structured CDD facts could all be typed wrong at the counter and never fixed.

The two classes of field in this request are handled differently, deliberately.

**`external_ref` is applied directly**, in the same transaction, and the response is a plain `200` with the corrected customer. It is the institution's own customer number and feeds nothing else this platform reasons over — no rule, no threshold, no form resolution reads it — so a typo fixed here cannot silently move what the customer is measured against.

**`segment`, `kyc_tier` and `profile` are NOT applied directly, however this request arrives.** `segment` selects the monitoring thresholds every transaction on this customer's accounts is compared against; `kyc_tier` selects the CDD form and the documents BRD §8.1 requires for the relationship. Both are exactly the class of change this codebase already requires a second reviewer on elsewhere — a configuration threshold, a monitoring rule, the risk model that assigns tiers in the first place, a case's outcome. Address geography, occupation/industry, nationality/country, source of funds, cash intensity and beneficial ownership are themselves screening corroboration, risk inputs or CDD evidence. A manual correction to any of them raises a maker-checker proposal (`workflow.ActionCustomerFieldChange`) instead of changing anything. `reason` is required whenever any governed field is sent: it is what the checker reads before deciding and is retained in workflow and audit metadata. Because it is readable free text, it must not contain names, addresses, identifiers, source-of-funds details or any other profile/PII value.

The staged profile fields themselves never enter the workflow request's readable JSONB or the append-only audit metadata. They are canonicalised (including NBS state/LGA codes for Nigerian addresses), encrypted under the party's subject DEK in immutable staging, and represented to workflow only by an opaque id, a digest of unpredictable ciphertext, and sorted field names. The separately supplied non-sensitive `reason` remains readable and auditable. On approval the same transaction applies the values, retains superseded ownership edges, invalidates any attestation over a replaced register, runs ongoing screening for the party and replacement owners, writes a `profile_update` risk assessment and opens/reconciles a `risk_change` KYC review. Any failed control rolls the whole approval back.

Identifiers and documents are deliberately not accepted. Identifier verification has its own authority-backed lifecycle; evidence documents are immutable.

Sending `external_ref` together with `segment` or `kyc_tier` in one request applies the first immediately and raises the proposal for the second in the same transaction; the response is a `202` naming the pending approval, and `customer` in that body already reflects the applied `external_ref`.

Show parameters and responses

Parameters

NameInTypeDescription
customerIDrequiredpathstring—

Responses

  • 200external_ref was corrected, and nothing else was requestedCustomer
  • 202A segment, KYC-tier or structured profile change is pending a second pair of eyesobject
  • 400—
  • 404—
  • 409Another external_ref is already in use, or a governed customer-profile proposal is already pending for this customer.
  • 422—
POST/v1/customers/{customerID}/assign

Route a customer relationship to an owning branch or team

Required permission: onboarding.customer.assignassignCustomerOrgUnit

Sets or clears `assigned_org_unit_id`, the same organisational hierarchy `POST /v1/iam/org-units` builds and `PATCH /v1/iam/users/{userID}` already routes staff into — applied here to the customer relationship rather than to a member of staff.

Send `org_unit_id` to assign, or `clear_org_unit` to unroute. The unit is validated by the composite tenant-carrying foreign key rather than by a lookup: a unit id from another tenant is refused the same way an unknown one is.

Its own permission, separate from `onboarding.customer.write`: which desk owns a relationship is an operational routing decision, not a correction to the relationship's own data, and a tenant may reasonably grant one right without the other.

Show parameters and responses

Parameters

NameInTypeDescription
customerIDrequiredpathstring—

Responses

  • 200The customer, with its assignment changedCustomer
  • 400—
  • 404The customer does not exist, or org_unit_id names no unit in this tenant.
  • 422—
POST/v1/customers/{customerID}/beneficial-owners

Declare a corporate customer's beneficial owners

Required permission: onboarding.ownership.writedeclareBeneficialOwners

Direct capture is an onboarding-only writer, available while the customer is `draft` or `pending_documents`. Once review can have begun, a beneficial-owner mutation must use the encrypted customer-profile change and maker-checker approval workflow; this endpoint refuses rather than bypassing that control.

A declaration, not a verification: it records what the customer told the institution. `owns_party_id` is how a chain is expressed — omit it and the owner holds a stake in the customer itself, set it and the owner holds a stake in another party already in the chain, which is what makes the resolver return more than a flat list.

Direct stakes are summed across declarations and refused past 100%. Every mutation invalidates the prior register attestation and active-edge verification before writing, because a checker did not examine the changed ownership graph.

Show parameters and responses

Parameters

NameInTypeDescription
customerIDrequiredpathstring—

Responses

  • 201The resolved ownership chain, which may be longer than what was declaredobject
  • 400—
  • 404—
  • 409That party is already a declared direct owner, or onboarding has progressed beyond direct capture and the governed customer-profile change workflow is required.
  • 422—
POST/v1/customers/{customerID}/beneficial-owners/verification

Record that a declared ownership register was checked against evidence

Required permission: onboarding.ownership.verifyverifyBeneficialOwners

A separate right from declaring the register, deliberately: the declaration is what the customer said and the verification is what the institution confirmed, and one person holding both makes the second a formality.

BRD §8.1 requires beneficial ownership "capture and verification" and does not say what verification is. Each value of `method` is a human attesting they compared the declared register against a document they held; none is a direct registry query, because the integrations adapter that would make one does not exist. Whether CBN requires a direct CAC check is a legal interpretation rather than a default this API gets to write (Q28).

`reference` is required: a verification with no evidence reference is a boolean somebody set.

Show parameters and responses

Parameters

NameInTypeDescription
customerIDrequiredpathstring—

Responses

  • 200The corporate profile with its verification provenanceCorporateProfile
  • 404—
  • 422—
POST/v1/customers/{customerID}/edd

Complete an enhanced due diligence review

Required permission: onboarding.customer.eddcompleteCustomerEDD

`app.customer.edd_completed_at` had no writer at all, in any deployment, by any route — so a customer whom risk scoring moved into `pending_edd` could never leave that state, and "EDD is triggered automatically for high-risk customers" was true and useless.

The resulting onboarding status is derived from `outcome` server-side and is not a field on this request: letting a caller name the resulting state is how a declined EDD becomes an active account. `cleared` activates the customer; `declined` rejects them.

Findings are required. An enhanced due diligence review that concluded nothing in writing did not happen, and this text is what an examiner reads when asking why a high-risk customer was accepted. A database CHECK pairs the completion timestamp with the actor, so a background job cannot sign one off — enhanced due diligence is a judgement.

Show parameters and responses

Parameters

NameInTypeDescription
customerIDrequiredpathstring—

Responses

  • 200The customer in its new stateCustomer
  • 404—
  • 409The customer is not in a state this completes from — not flagged for EDD, or already completed. The message says which, because "not found" would send a compliance officer looking for a customer who is right there.
  • 422—
POST/v1/customers/{customerID}/exit

Formally offboard an active customer relationship

Required permission: onboarding.customer.exitexitCustomer

Moves `onboarding_status` to `exited` — a value the CHECK constraint on `app.customer` has accepted since this platform's first onboarding migration, and that nothing, before this operation, could ever write.

Only reachable from `active`. A relationship that never reached `active` is declined or rejected through the existing EDD and application-review paths, not exited: those are the record of a relationship that did not start, and this is the record of one that formally ended.

`reason` is required. It is not routed through four-eyes the way a segment or KYC-tier change is (`PATCH /v1/customers/{customerID}`): exiting does not change what the platform measures the customer against, it stops measuring the customer at all, and the record of why is the required reason and the audit event this writes, the same shape `completeCustomerEDD`'s findings requirement already establishes for a judgement of this kind.

Show parameters and responses

Parameters

NameInTypeDescription
customerIDrequiredpathstring—

Responses

  • 200The customer, now exitedCustomer
  • 404—
  • 409The customer is not active, so there is no relationship to exit.
  • 422—
POST/v1/customers/{customerID}/identifiers/reveal

Decrypt an active customer's identifiers

Required permission: onboarding.identifiers.revealrevealCustomerIdentifiers

`GET /v1/onboarding/applications/{applicationID}` reveals an applicant's identifiers exactly once, at review. Nothing gave that same audited reveal to an MLRO investigating a case, or answering a supervisory or regulatory request, against a customer who has been active for two years — the front office's one look at the counter was the only reveal this platform could ever produce for a relationship's whole lifetime.

`RevealIdentifiers` (the same method the application review calls) already writes its own `pii.read` audit event on every call, unconditionally — this endpoint adds no further permission check inside the transaction beyond the one this operation declares, because the decryption and its log entry are one atomic act, not two.

A crypto-shredded subject's identifiers come back as an empty list rather than a 404 or an error: the customer relationship and its audit history survive an NDPA erasure, the identity fields behind it do not (Q5). No amount of holding this permission resurrects a destroyed key.

Show parameters and responses

Parameters

NameInTypeDescription
customerIDrequiredpathstring—

Responses

  • 200The customer's identifiers, with `value` populatedobject
  • 404—
GET/v1/customers/{customerID}/profile

Reveal the mutable structured customer profile for correction

Required permission: onboarding.customer.writerevealCustomerProfile

Returns only the structured facts supported by the governed maintenance workflow. Address is decrypted under the party's subject key, so this is separate from the ordinary customer workspace: opening the editor is an explicit sensitive read recorded both in the central access catalogue and as a field-specific `pii.read` event.

`legacy_address`, when present, is an older free-text value retained for readability. A correction must submit a structured `address`; Nigerian addresses are resolved to the embedded NBS state/LGA catalogue and stored as canonical codes.

Identifiers and document content are absent by design and cannot be changed here.

Show parameters and responses

Parameters

NameInTypeDescription
customerIDrequiredpathstring—

Responses

  • 200The audited mutable-profile revealCustomerProfile
  • 404—
GET/v1/customers/{customerID}/risk-assessments

A customer's full rating history

Required permission: onboarding.customer.readlistCustomerRiskAssessments

`GET /v1/customers/{customerID}` returns only the LATEST risk assessment, because "why is this customer rated low today" is the question a customer profile answers first and making an analyst fetch a whole history for it would be wrong the other way. This is the history behind that one number.

`app.risk_assessment` is append-only by design (BRD §8.2): a re-score writes a new row rather than overwriting the old one, so a rating from eighteen months ago, and the model version it was pinned to, is still readable exactly as it stood. Until this operation existed nothing on the API surface could reach any assessment but the latest — the history was always in the database and had no door.

Each entry carries its own per-factor decomposition, the same shape the latest assessment on the customer response does, newest first.

Show parameters and responses

Parameters

NameInTypeDescription
customerIDrequiredpathstring—
limitqueryintegerPage size, capped at 200.
cursorquerystringOpaque keyset cursor from a previous response. Offset pagination is deliberately not offered: `OFFSET 500000` is a table scan, and if it were offered it would be used (ADR-0012). Only the collections that document this parameter can be resumed. The rest report `truncated: true` and refuse a cursor rather than silently returning the first page again — see the `truncated` field on the page schemas.

Responses

  • 200The rating history, newest firstRiskAssessmentPage
  • 404—
POST/v1/identifiers/verify

Verify a captured identifier against its authoritative source

Required permission: onboarding.customer.writeverifyIdentifier

**This operation answers `503` unless an identity provider is configured. No production NIBSS/NIMC provider ships with this build, and that refusal is the honest state rather than a bug.**

BRD §8.1 requires "BVN/NIN verification integration". The provider registry and bounded adapter seam exist, but there is no production NIBSS adapter, NIMC adapter, credentials or commercial agreement. No authority is invented here. A deterministic mock may be enabled only outside production; its result is stored with `verification_assurance=mock` and cannot satisfy the CBN approval gate.

It is documented and routed rather than omitted for two reasons. The refusal becomes a machine-readable state of the product instead of a `404` that reads as "we forgot"; and the generated client carries the shape a real answer will have, so integrating a provider is a server change rather than a contract change.

What this replaces was worse than absent. `verified_by_source` used to be filled from a string the CALLER supplied, so a front-office user could type "NIBSS" beside a number nobody had checked and every screen and every examiner afterwards read a verified identifier. That field is now unreachable from the write path. The service refuses BEFORE decrypting anything, so no PII access is logged for a call that could never succeed.

See open question Q26 for the remaining provider procurement, credential, retry and operational-ownership decisions. An outage may leave capture pending, but it cannot be presented as an authoritative verification or activate the relationship.

Show parameters and responses

Responses

  • 200The identifier, now carrying the authority that verified itPartyIdentifier
  • 404—
  • 422—
  • 503No verification provider is configured. The identifier stays captured and UNVERIFIED, which is the true state (Q26).
GET/v1/onboarding/applications/{applicationID}

A submitted CDD application, with the applicant's identifiers revealed

Required permission: onboarding.application.reviewreviewOnboardingApplication

The second pair of eyes on an application. Loading one DECRYPTS the applicant's identifiers and therefore writes a `pii.read` event into the hash chain (ADR-0019), which is why it carries its own permission rather than riding on the customer read: approving an application whose BVN nobody could read is the same defect as approving a form whose answers cannot be stored — a control exercised on something nobody examined.

`submitted_data` is the answer set exactly as it arrived, against the form version named on the record, so the application can be re-rendered as the customer saw it.

A crypto-shredded subject comes back with no identifiers rather than an error: the application and the relationship survive an NDPA erasure; the identity behind them does not (Q5).

**Opening an application under review SCREENS its subject** (`mode: onboarding`), and the result comes back on `screening`. That is where BRD §8.4's screening-at-onboarding happens: the review is the moment the control is exercised, so the hits arrive in the same payload as the decrypted identifiers and a checker reads the name, the BVN and what the lists say in one pass. A decided application is not re-screened — reading a historical record is not a control — and a screening failure does not block the review; it comes back as `screening_unavailable` with the reason, so "not screened" can never read as "clear".

The first successful onboarding run is pinned to the application. Reopening the review does not manufacture duplicate runs or hits; analysts decide the pinned run's hits in the screening queue and approval remains blocked until none are unresolved.

Show parameters and responses

Parameters

NameInTypeDescription
applicationIDrequiredpathstring—

Responses

  • 200The application, its subject and the identity data a checker needsApplicationReview
  • 404—
  • 410The subject's key was destroyed under a data-subject erasure request.
POST/v1/onboarding/applications/{applicationID}/decision

Approve or reject a customer application

Required permission: onboarding.application.decidedecideOnboardingApplication

Enforces maker-checker. Approval is fail-closed and atomic: every required document must be verified and unexpired, the pinned onboarding screening run must have no unresolved hit, a fresh explainable risk assessment is written and applied, then the relationship moves to `active` or `pending_edd`. Rejection requires a written reason and moves the relationship to `rejected`.

Show parameters and responses

Parameters

NameInTypeDescription
applicationIDrequiredpathstring—

Responses

  • 200Application and customer state after the decisionApplicationDecision
  • 404—
  • 409Maker-checker violation, missing evidence, unresolved screening hits or stale state
  • 422—
POST/v1/onboarding/applications/{applicationID}/documents

Upload immutable CDD evidence

Required permission: onboarding.customer.writeuploadOnboardingDocument

Stores an encrypted PDF or image against the document requirements snapshotted when the application was submitted. The object store receives ciphertext under an opaque, server-generated key; the database records a plaintext SHA-256 digest and encrypted storage reference. Reusing the same UUID `Idempotency-Key` with the same bytes returns the same record, while reusing it for different evidence is refused.

Show parameters and responses

Parameters

NameInTypeDescription
applicationIDrequiredpathstring—
Idempotency-Keyrequiredheaderstring—
doc_typerequiredquerystring—
filenamerequiredquerystring—
expires_onquerystring—

Responses

  • 201Evidence stored, encrypted and linked to the applicationOnboardingDocument
  • 400—
  • 404—
  • 409—
  • 413Document exceeds the 20 MiB evidence limit
  • 415Only PDF, JPEG and PNG evidence is accepted
  • 422—
GET/v1/onboarding/documents/{documentID}/content

Download and audit access to CDD evidence

Required permission: onboarding.application.reviewdownloadOnboardingDocument

Decrypts the immutable object, verifies its SHA-256 digest and writes a `pii.read` event in the same transaction. The response media type is the one recorded at upload.

Show parameters and responses

Parameters

NameInTypeDescription
documentIDrequiredpathstring—

Responses

  • 200Original evidence bytes
  • 404—
  • 410The subject's key was destroyed under an erasure request
POST/v1/onboarding/documents/{documentID}/decision

Verify or reject one evidence object

Required permission: onboarding.application.decidedecideOnboardingDocument

A maker-checker decision: the uploader cannot verify or reject their own object. Evidence is immutable; a rejected object remains in history and the front office uploads a new one.

Show parameters and responses

Parameters

NameInTypeDescription
documentIDrequiredpathstring—

Responses

  • 200Evidence decision recordedOnboardingDocument
  • 404—
  • 409—
  • 422—
GET/v1/reference-data/nigeria-locations

Read the Nigerian state, LGA and FCT area-council catalogue

Required permission: onboarding.customer.readgetNigeriaLocations

Returns the one immutable NBS-coded catalogue used by onboarding validation. Source labels are preserved verbatim; explicitly recorded constitutional aliases are metadata, not a claim that Baobyte has corrected the source. The response distinguishes the six FCT area councils from state LGAs and carries source URLs plus the exact embedded-dataset digest so an application can prove which vocabulary it displayed.

Show parameters and responses

Responses

  • 200Nigerian location reference data and provenanceNigeriaLocationCatalog