> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sakawave.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Customer use cases

> Four complete integrations — onboarding, keeping records in step, finding customers, and offboarding — with the error branches that actually occur.

Each flow below is complete: the happy path, and the failures worth handling.

## 1. Onboard a customer and give them a wallet

The most common flow. Create the customer, then open an account naming them.

```bash theme={null}
CUSTOMER=$(curl --silent --show-error --fail-with-body \
  --request POST \
  --url "$SAKAWAVE_BASE_URL/v1/customers" \
  --header "Authorization: Bearer $SAKAWAVE_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "type": "individual",
    "jurisdiction": "NG",
    "externalReference": "user_84021",
    "email": "ada@example.com",
    "phoneNumber": "+2348012345678",
    "address": { "line1": "14 Marina Road", "city": "Lagos", "country": "NG" },
    "firstName": "Ada",
    "lastName": "Okafor"
  }')

CUSTOMER_ID=$(printf '%s' "$CUSTOMER" | jq --raw-output '.id')

curl --request POST \
  --url "$SAKAWAVE_BASE_URL/v1/accounts" \
  --header "Authorization: Bearer $SAKAWAVE_API_KEY" \
  --header "Content-Type: application/json" \
  --data "{
    \"name\": \"Ada NGN wallet\",
    \"type\": \"operational\",
    \"currency\": \"NGN\",
    \"customerId\": \"$CUSTOMER_ID\"
  }"
```

Passing `customerId` is what makes the account a wallet belonging to that customer. Omit it and the account belongs to your business.

**The branch that matters.** If the customer was created but the account call failed, you have a customer with no wallet — and retrying the whole flow hits `409 customer_already_exists`. Make the create idempotent from your side:

```ts theme={null}
async function ensureCustomer(reference: string, payload: object) {
  const created = await post('/v1/customers', { ...payload, externalReference: reference });
  if (created.status !== 409) return created.body;

  // Somebody — possibly a previous run of this job — already created them.
  const found = await get(`/v1/customers?externalReference=${encodeURIComponent(reference)}`);
  if (found.body.items.length === 1) return found.body.items[0];

  // A 409 with no match means an identity number collided, not the reference.
  throw new Error(`customer_already_exists but ${reference} is not on file`);
}
```

That last branch is worth keeping. `409 customer_already_exists` covers `externalReference` *and* every identity number, so "already exists" does not always mean "the one I am looking for".

## 2. Keep your records and Sakawave in step

Your system is the source of truth for customer details. When something changes there, push it.

```bash theme={null}
curl --request PATCH \
  --url "$SAKAWAVE_BASE_URL/v1/customers/$CUSTOMER_ID" \
  --header "Authorization: Bearer $SAKAWAVE_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "email": "ada.okafor@example.com",
    "address": { "line1": "22 Awolowo Road", "city": "Lagos", "country": "NG" }
  }'
```

Three rules govern `PATCH`:

* **Absent leaves alone; `null` clears.** `{"middleName": null}` erases a middle name. Omitting `middleName` keeps it.
* **`address` replaces wholesale.** It is not merged field by field. Send the complete address, or you will silently clear `line2`, `state`, and `postalCode`.
* **Type-specific fields are refused, not ignored.** Sending `name` for an individual returns `400 customer_fields_do_not_match_type` with the offending names in `fields`. This catches the common bug where one code path patches every customer with the same payload shape.

```json theme={null}
{
  "statusCode": 400,
  "message": "These fields do not apply to a individual customer: name.",
  "code": "customer_fields_do_not_match_type",
  "fields": ["name"]
}
```

If you drive updates from your own change events, guard against a loop: a `PATCH` emits `customer.updated`, and a receiver that writes back on every event will chase its own tail.

## 3. Find customers

Reads are filtered with `field=value`, or `field:operator=value` where the field supports more than equality.

```http theme={null}
GET /v1/customers?type=individual&jurisdiction:in=NG,GH&createdAt:gte=2026-08-01T00:00:00Z&sort=-createdAt&pageSize=50
```

| Need | Query |
| - | - |
| One customer by your own reference | `?externalReference=user_84021` |
| Everyone onboarded this month | `?createdAt:gte=2026-08-01T00:00:00Z` |
| Business customers in two countries | `?type=business&jurisdiction:in=NG,GH` |
| Free-text over names, email, and reference | `?search=okafor` |
| Oldest first | `?sort=createdAt` |

A repeated parameter and a comma-separated one mean the same thing: `jurisdiction:in=NG&jurisdiction:in=GH` equals `jurisdiction:in=NG,GH`.

Unknown filters are rejected by name rather than ignored — `?statuss=active` fails with `400` and tells you which parameter it did not recognise. This is deliberate: a silently ignored filter returns a wider result set than you asked for, which is how the wrong customers end up in a report.

**Paging a full export.** Page size defaults to 25 and caps at 100; a larger value is rejected rather than quietly capped, so your arithmetic stays right. Results are ordered newest first with `id` as a tiebreaker, so pages are stable unless a customer is created mid-scan — pin `createdAt:lte` at the start of the job if that matters.

```bash theme={null}
SCAN_START=$(date -u +%Y-%m-%dT%H:%M:%SZ)
PAGE=1
while : ; do
  BODY=$(curl --silent --fail-with-body \
    --url "$SAKAWAVE_BASE_URL/v1/customers?pageSize=100&page=$PAGE&createdAt:lte=$SCAN_START" \
    --header "Authorization: Bearer $SAKAWAVE_API_KEY")
  printf '%s' "$BODY" | jq --compact-output '.items[]'
  [ "$(printf '%s' "$BODY" | jq --raw-output '.pagination.hasNext')" = "true" ] || break
  PAGE=$((PAGE + 1))
done
```

## 4. Offboard a customer

Archiving is refused while the customer still holds anything live.

```bash theme={null}
curl --request DELETE \
  --url "$SAKAWAVE_BASE_URL/v1/customers/$CUSTOMER_ID" \
  --header "Authorization: Bearer $SAKAWAVE_API_KEY"
```

A `204` means archived. A `409 customer_has_active_products` means there is still an open wallet account or a live card:

```json theme={null}
{
  "statusCode": 409,
  "message": "Close this customer’s active wallets and terminate their cards before archiving the customer.",
  "code": "customer_has_active_products"
}
```

The order is therefore: close every account, terminate every card, then archive.

<Warning>
  Cards can be terminated through this API. **Accounts cannot be closed through it** — closing is a dashboard action. An offboarding flow built entirely on the Business API therefore stops one step short of archiving, and needs a person to finish it. See [the accounts overview](/services/accounts/overview#lifecycle).
</Warning>

Two consequences to plan for:

* **The reference is released.** After archiving, `externalReference` and the identity numbers become reusable by a new customer. If your own records rely on that reference being unique forever, keep your own history — Sakawave will not stop it being reissued.
* **There is no unarchive.** Recreating produces a new `id`. Anything you stored against the old one still points at a customer that reads answer `404` for.

<Note>
  Archiving emits no webhook. A local mirror should detect archives by reconciling against `GET /v1/customers`, treating a record that has disappeared as archived.
</Note>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.