Skip to content

Customers

The payer record Wajub reuses across payments, and what it changes at checkout.

You have customers whether or not you asked for any. Every call to POST /payments resolves a customer before it records the transaction, so the very first payment you created in sandbox already produced one. The resource is not an opt-in extra, it is the payer identity every payment is attached to.

What follows is how that resolution works, when it goes wrong, and what a customer actually buys you on the checkout page.

Every payment creates or finds one

A payment must carry at least one of email, phone or customer_id. With none of the three the API answers 422 and nothing is recorded.

Given one, Wajub looks for a matching customer on your account, in the environment you are calling. If it finds one, the payment is attached to it. If it does not, it creates the customer and attaches the payment to that. Either way you get a customer object back on the payment, carrying the id you can reuse from then on.

Sandbox and live are separate populations. A customer created by a sandbox payment is invisible to a live one, and the ids differ: cus_test_… in sandbox, cus_… in production.

How Wajub decides two payments are the same person

Matching runs in a fixed order, and stops at the first hit.

OrderFieldHow it is compared
1customer_idExact match on the customer id
2emailTrimmed, lowercased, so [email protected] matches [email protected]
3phoneTrimmed only, then compared character for character

Email is forgiving. Phone is not, and that asymmetry is where merchant customer lists go wrong.

When a payment carries both an email and a phone, the email decides. The phone is only consulted if no customer matched the email, which means an existing customer can be found by email and then quietly gain a phone number they never had.

Pay as a known customer

Once you hold an id, pass it and skip the contact fields entirely. The customer field accepts the id as a plain string, and customer_id accepts the same value; they are two spellings of one thing.

curl https://api.wajub.com/payments \
  -H "Authorization: sk_test.xxxxxxxxxxxxxxxx…" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 5000,
    "currency": "XAF",
    "customer": "cus_test_sAaim5apjocIgtlhzJY3wQ8s",
    "reference": "order-4173"
  }'

The identifier does not have to be an id. If the value you pass matches no customer id, Wajub tries it as a phone number, then as an email address. So "customer": "[email protected]" and "customer": "+237670000000" both resolve, which is convenient when your own database stores the email and not the Wajub id.

A payment never overwrites a customer

When a payment matches an existing customer, the data you sent is merged in, but only into the gaps. A field that already holds a value is left alone.

Sent on the paymentCustomer already has a valueCustomer has none
name, email, phoneIgnoredFilled in
metadataMerged key by key, your keys winFilled in
addressIgnoredAttached as the primary shipping address
shippingIgnoredAttached as the primary billing address

Note the second address row: a top-level shipping object on the payment lands in the customer's billing address, not in the shipping one. Send the delivery address as customer.address.

The practical consequence: you cannot correct a customer through a payment. Send a payment with "name": "Amina Diallo" for a customer already named Amina N. and the old name survives. Use PUT /customers/{id} for corrections.

PUT /customers/{id} changes only the fields you send. The name changes only when you send it: first_name and last_name are refused, and sending business_name on an individual leaves their name alone (on a business, a new business_name becomes its name unless you send name too). An email or a phone already held by another customer of the account answers 422 on that field, A customer with this email already exists. or A customer with this phone already exists.

The first payment drops the address

Addresses are only attached on the merge path, when the customer already existed. A brand new customer created by a payment is stored with its email, name, phone and metadata alone, so an address sent on that first payment is not kept on the customer record. Create the customer up front if the address matters to you.

Create a customer up front

Creating explicitly is worth it when you know the payer before they pay: a signup, an imported customer base, a B2B account with a tax id and a billing address.

POSThttps://api.wajub.com/customers

At least one of email or phone is required. Everything else is optional, and the fields below are the ones worth knowing; the full list is in the API reference.

emailstringoptional
Required unless phone is given. Stored as sent: a payment still finds the customer whatever the case, but a second POST /customers with [email protected] creates a new customer next to [email protected].
phonestringoptional
Required unless email is given. Send E.164, matching is literal.
namestringoptional
Display name, the only name field you can set.
typeenumoptionaldefault : individual
individual or business. Sending business_name alone infers business.
business_namestringoptional
Required when type is business. Becomes the customer name.
currencystringoptional
Preferred currency, used as the checkout default when auto-convert is on.
preferred_languagestringoptional
Locale for the hosted checkout, below the team and branding settings.
addressobjectoptional
Shipping address: country, city, state, postal_code, address_line1, address_line2.
billingobjectoptional
Billing address, same shape as address.
customer_groupstringoptional
Free-form segment you can filter the list on.
tagsarrayoptional
Strings, up to 50 characters each.
metadataobjectoptional
Free-form key-value data of your own.

Send it from your server with the secret key.

curl https://api.wajub.com/customers \
  -H "Authorization: sk_test.xxxxxxxxxxxxxxxx…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cus-signup-9182" \
  -d '{
    "email": "[email protected]",
    "phone": "+237670000000",
    "name": "Amina Diallo",
    "metadata": { "user_id": "u_9182" }
  }'

A new customer comes back as 201 Created, with the id you will pass on payments from now on.

Response · 201 Created
{
"code": 201,
"status": "Created",
"message": "Customer created",
"customer": {
"id": "cus_test_sAaim5apjocIgtlhzJY3wQ8s",
"type": "individual",
"name": "Amina Diallo",
"email": "[email protected]",
"phone": "+237670000000",
"status": "active",
"blocked": false,
"tags": [
],
"metadata": {
"user_id": "u_9182"
},
"sandbox": true,
"created_at": "2026-09-11T10:24:00+00:00"
}
}

Idempotency-Key works here as it does on payments: the same key replays the original response for 24 hours instead of opening a second record. It is the safe way to retry a signup hook.

A marketplace creating a customer on behalf of one of its subaccounts adds the X-Sync header to this call. The merchant's other customers stay out of reach: an existing customer is returned only when they already paid through the connection. Account types has the details.

What a customer changes on the checkout page

This is the part that pays for the extra call. A payment attached to a known customer renders a different page.

What the customer carriesWhat the checkout does with it
Past cards and Mobile Money numbersOffers them as one-tap saved methods, default first, ten at most
preferred_languageSets the page locale, unless the team or branding setting overrides it
currencyBecomes the pre-selected currency when auto-convert is enabled

Saved methods are the real win for returning payers: the number is already there, masked, and they confirm on the handset without typing it again.

Saving a method is the payer's decision

A card or Mobile Money number is only kept when the payer ticks the box on the checkout page. It is never inferred and never defaulted on, so a customer record with no saved methods is the normal state until someone opts in. Only cards and Mobile Money are reusable; wallets and bank transfers are not stored.

Retrieve and list

Retrieving takes the customer id and returns the same object as the create call.

GEThttps://api.wajub.com/customers/{id}

Listing returns your customers newest first, and accepts filters that map onto how you segment them.

GEThttps://api.wajub.com/customers
Query parameterAccepts
per_page1 to 100, defaults to 25
searchFree text over name, email, phone and id
statusactive, inactive, blocked
typeindividual, business
countryTwo-letter country code of the customer's shipping or billing address
customer_groupThe group string you set on the customer
cursorAn opaque cursor from a previous response

Pagination has two modes and the default is the page-number one. Send no cursor and meta holds current_page, last_page, per_page and total, plus the next_cursor and has_more that let you go on with a cursor. Send that next_cursor as cursor and the endpoint switches to cursor mode, where meta holds per_page, next_cursor, prev_cursor and has_more instead. Cursor mode is what you want for a full export, since it stays correct while new customers are being created. Pagination covers both.

Status is a label, not a gate

A customer is active, inactive or blocked, and four endpoints move them between those states.

CallEffect
POST /customers/{id}/blockSets blocked, stores your reason and a blocked_at timestamp
POST /customers/{id}/unblockReturns the customer to active
POST /customers/{id}/deactivateSets inactive
POST /customers/{id}/activateSets active

Blocking an already blocked customer answers 400, as does unblocking one who is not blocked.

Tax ids

A business customer's tax registration numbers are kept in their own list, one entry per number: a Cameroonian NIU, a Senegalese NINEA, an EU VAT number. The list is available in live only.

POSThttps://api.wajub.com/customers/{id}/tax_ids
typestringrequired
The kind of number: cm.niu, sn.ninea, ci.ifu, ke.pin, ng.tin, gh.tin, gh.vat, eu.vat, gb.vat or us.ein. Another type is accepted too, and only its length is checked.
valuestringrequired
The number itself, with or without the spaces and dashes its format writes.
country_codestringrequired
ISO 3166-1 alpha-2, such as CM.

Add the number from your server, with a live secret key:

curl https://api.wajub.com/customers/cus_sAaim5apjocIgtlhzJY3wQ8s/tax_ids \
  -H "Authorization: $WAJUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "cm.niu",
    "value": "M123456789000L",
    "country_code": "CM"
  }'

The number comes back pending, and Wajub checks it in the background. verification_status then becomes verified when the value matches the format of its type, failed when it does not, and unavailable when the check could not run. The formats are the ones your own registration number is checked against (Tax configuration lists them): a Cameroonian NIU is a letter, twelve digits and a letter. For an eu.vat number the check goes further and asks the European VIES service whether the number exists.

Adding the same type and number again does not add a copy: the call answers 200 with the existing entry, back to pending while it is checked again. That is how you retry an unavailable check.

GET /customers/{id}/tax_ids lists the numbers with their status, and DELETE /customers/{id}/tax_ids/{tax_id} removes one.

Deleting a customer is an erasure

Deletion is not the reversible archive it is on most resources.

DELETEhttps://api.wajub.com/customers/{id}

The record is removed from your lists and its personal data is anonymised immediately, in the same request, to satisfy a right-to-erasure demand without a second step. The anonymised record stays in the Dashboard's deleted customers until someone on your team deletes it for good. A full copy is kept in an archive for 120 days, then permanently deleted. Past payments survive, since they are financial records: their customer details are pseudonymised, and they keep the wallet or account number they were paid with. The customer.deleted event carries the anonymised record, and the customer's details are removed from the events and webhook deliveries already recorded. A repeated call answers the same 200, and finishes an erasure an earlier call left incomplete. With X-Sync the call answers 403: a platform does not erase a merchant's customers. Deleting a payer lists everything it removes.

Webhooks

Three events follow a customer, and they carry the full customer object as their payload.

EventFires when
customer.createdA customer is created, including one created implicitly by a payment
customer.updatedAny field changes, including a status change or a merge from a payment
customer.deletedThe customer is deleted and anonymised

customer.created is noisier than it looks: it fires for every new payer your checkout sees, not only for the ones you create yourself. Registration, verification and retries are covered in Webhooks.

Pocket, the payer's side

Your customers have their own space, Pocket, at pocket.wajub.com. A payer signs in with their email or phone number and a one-time code, and finds every live payment made with that address or number, across all the merchants on Wajub.

From there they can:

  • open a dispute on a payment;
  • ask you for a refund, with a reason. The request lands in your Dashboard's refund requests, where you approve or reject it;
  • remove a saved payment method, or choose their default one;
  • read and download the invoices sent to them;
  • delete their data.

There is nothing to integrate: Pocket reads the same customers and payments your integration creates, which is one more reason to send the payer's real email and phone.

In the Dashboard

Customers lists the same records with a timeline per customer, and their payments, refunds and disputes on their own tabs. Tags, groups and notes are editable there, and bulk actions apply a change across a selection.

Two tools have no API equivalent. Customers → Export produces a CSV or Excel of the list and is live only. Settings → Tools → Customer merges folds duplicates into a single record, which is how you repair a list already split across three phone formats.

What did you think of this content?