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.
| Order | Field | How it is compared |
|---|---|---|
| 1 | customer_id | Exact match on the customer id |
| 2 | email | Trimmed, lowercased, so [email protected] matches [email protected] |
| 3 | phone | Trimmed only, then compared character for character |
Email is forgiving. Phone is not, and that asymmetry is where merchant customer lists go wrong.
Phone matching is an exact string comparison
+237670000000, 237670000000 and +237 6 70 00 00 00 are three different customers to this
lookup. Normalise to E.164 once, in your own code, before the number ever reaches the API. The
rules are in Phone number formats.
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.
An identifier that resolves to nothing is a 422, not a new customer
This is the one place where the API refuses to create. Pass customer or customer_id and Wajub
will find that customer or fail the call with a 422 on the field you used, Customer not found.;
it never falls back to creating one. A stale id, a customer deleted last week, or a live id sent to sandbox all kill the
payment outright. Send email or phone instead when you are not certain the record exists.
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 payment | Customer already has a value | Customer has none |
|---|---|---|
name, email, phone | Ignored | Filled in |
metadata | Merged key by key, your keys win | Filled in |
address | Ignored | Attached as the primary shipping address |
shipping | Ignored | Attached 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.
https://api.wajub.com/customersAt 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.
emailstringoptionalphone 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].phonestringoptionalemail is given. Send E.164, matching is literal.namestringoptionaltypeenumoptionaldefault : individualindividual or business. Sending business_name alone infers business.business_namestringoptionaltype is business. Becomes the customer name.currencystringoptionalpreferred_languagestringoptionaladdressobjectoptionalcountry, city, state, postal_code, address_line1, address_line2.billingobjectoptionaladdress.customer_groupstringoptionaltagsarrayoptionalmetadataobjectoptionalSend 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.
A duplicate answers 200, not 201
Create a customer whose email or phone already exists and the API returns the existing record
with 200 OK and the message Customer already exists for this merchant. Nothing is duplicated,
and nothing is updated either. Branch on code === 201 to tell a real creation from a match, and
never treat the 200 as an error.
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 carries | What the checkout does with it |
|---|---|
| Past cards and Mobile Money numbers | Offers them as one-tap saved methods, default first, ten at most |
preferred_language | Sets the page locale, unless the team or branding setting overrides it |
currency | Becomes 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.
https://api.wajub.com/customers/{id}Listing returns your customers newest first, and accepts filters that map onto how you segment them.
https://api.wajub.com/customers| Query parameter | Accepts |
|---|---|
per_page | 1 to 100, defaults to 25 |
search | Free text over name, email, phone and id |
status | active, inactive, blocked |
type | individual, business |
country | Two-letter country code of the customer's shipping or billing address |
customer_group | The group string you set on the customer |
cursor | An 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.
| Call | Effect |
|---|---|
POST /customers/{id}/block | Sets blocked, stores your reason and a blocked_at timestamp |
POST /customers/{id}/unblock | Returns the customer to active |
POST /customers/{id}/deactivate | Sets inactive |
POST /customers/{id}/activate | Sets active |
Blocking an already blocked customer answers 400, as does unblocking one who is not blocked.
Blocking a customer does not stop them paying
status: blocked is a flag on the customer record and nothing in the payment path reads it. A
blocked customer can create and complete a payment exactly as before. To actually refuse a payer,
add their email or phone to the Shield blocklist, which is checked on every live
transaction and rejects it outright.
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.
https://api.wajub.com/customers/{id}/tax_idstypestringrequiredcm.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.valuestringrequiredcountry_codestringrequiredCM.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.
https://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.
There is no undo through the API
The API has no restore endpoint. The Dashboard can restore the record, but the personal data does not come back with it: it was anonymised in the delete request. Deactivate rather than delete when you simply want a customer out of the way.
Webhooks
Three events follow a customer, and they carry the full customer object as their payload.
| Event | Fires when |
|---|---|
customer.created | A customer is created, including one created implicitly by a payment |
customer.updated | Any field changes, including a status change or a merge from a payment |
customer.deleted | The 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.
Related pages
- Customers API referenceEvery field on the resource, and the five generated endpoints.
- Phone number formatsNormalise to E.164 before matching splits your customers in two.
- Shield RulesThe blocklist that actually refuses a payer, by email, phone, IP or BIN.
- Recurring invoicesBill a known customer on a schedule.
- WebhooksRegister an endpoint and verify the signature on customer events.
- PaginationPage numbers for a screen, cursors for an export.