Skip to content

Manage customers

Create a customer up front, update, list and block one, keep its tax ids, and follow its webhooks.

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, and found whatever its case: a second POST /customers with Amina@Example.com returns the customer stored as amina@example.com.
phonestringoptional
Required unless email is given. Stored in E.164: a number without its international prefix is read in phone_country, else your account's country.
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": "amina@example.com",
    "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": "amina@example.com",
"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.

Update a customer

A payment never corrects a customer, it only fills the fields still empty: see A payment never overwrites a customer. Corrections go through PUT /customers/{id}.

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

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 (whatever its case) 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.

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.

On behalf of a subaccount

A marketplace creating a customer on behalf of one of its subaccounts adds the X-Sync header to POST /customers, and the duplicate rule above applies: when the merchant already has a customer with this email or phone, that customer is returned with 200, and nothing is duplicated. The platform's call only fills the fields the merchant left empty; it never changes one they set. The customer returned, found or created, is open to the connection from then on: the platform can read, list and update them, and use them on its payments and invoices. A payment created with X-Sync and the email or phone of an existing customer is attached to that customer the same way.

With X-Sync, a customer never shows the merchant's notes, block_reason or metadata, and notes sent with X-Sync is ignored. metadata is the platform's own: it reads and writes it as on any customer, each connection has its own, and the merchant's stays theirs. On a customer the merchant already had, POST /customers and payments add the keys the platform has not set yet, and PUT /customers/{id} replaces the platform's metadata. Account types has the details.

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.

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.

What did you think of this content?