Appearance
Customers
Every order is delivered to a customer who has at least one address. Customers are scoped to your merchant account — phone numbers and emails are de-duplicated within your account and per customer type, so the same phone number can exist once as an individual customer and once as a business customer.
Create a customer
POST /integration/merchant/customer
Request body:
json
{
"name": "Sara Ahmad",
"phoneNumber": "+96550001234",
"email": "[email protected]",
"addresses": [
{
"label": "Home",
"addressType": "home",
"governorateId": "<governorate-id>",
"neighborhoodId": "<neighborhood-id>",
"blockId": "4",
"streetId": "12",
"houseNumber": "8",
"buildingName": "Al-Salam Tower",
"floorNumber": "3",
"apartmentNumber": "11"
}
]
}| Field | Required | Notes |
|---|---|---|
name | Yes | Customer full name. |
phoneNumber | Yes | Validated and normalized. Must be unique within your merchant account per customer type. |
email | No | Validated if provided. Unique within your merchant account per customer type. |
customerType | No | individual (default) or business. Required to be business for B2B orders. |
businessName | Business only | Required when customerType is business — missing returns 400 VALIDATION_FAILED with errors.businessName. |
contactPersonName | No | Contact person at the business. |
addresses | No | Up to 5. The first address becomes the default. label is the only schema-required address field; governorateId and neighborhoodId are required for pricing. |
Response: 201 Created
json
{
"success": true,
"data": {
"customer": {
"_id": "665f...",
"name": "Sara Ahmad",
"phoneNumber": "96550001234",
"addresses": [ { "_id": "665f...", "label": "Home", "isDefault": true, /* ... */ } ]
}
}
}Store customer._id and the relevant addresses[]._id — you need both to create an order.
Find or create a customer (upsert)
POST /integration/merchant/customer/upsert
Convenience endpoint for the common "do I already know this customer?" flow — ideal for e-commerce checkouts. Looks the customer up by phone number within your merchant account:
- Found → returns the existing customer —
200 OK,created: false - Not found → creates and links a new customer —
201 Created,created: true
Request body:
json
{ "phoneNumber": "+96550001234", "name": "Sara Ahmad", "email": "[email protected]" }| Field | Required | Notes |
|---|---|---|
phoneNumber | Yes | Lookup key. Normalized before matching (see note below). |
name | On create | Required only when no customer matches — send it always so either path succeeds. |
email | Optional | Stored on creation only. |
Response:
json
{ "success": true, "data": { "customer": { /* full customer */ }, "created": true } }The customer shape is identical in both cases. A newly created customer has no addresses — add one with Add an address before creating orders.
Phone normalization
Phone numbers are normalized before storage and matching: spaces, dashes, the leading + and 00 are stripped, so +965 5000-1234 is stored and matched as 96550001234. Any format you send will match the same customer.
Address fields
| Field | Required | Description |
|---|---|---|
label | Schema required | A name for the address, e.g. "Home", "Office". |
governorateId | Required for pricing | From the area tree. Orders cannot be priced without it. |
neighborhoodId | Required for pricing | From the area tree. Orders cannot be priced without it. |
addressType | Optional | home (default) | apartment | office. |
blockId | Optional — strongly preferred | Improves pricing accuracy and routing. |
streetId | Optional | Street within the block. |
parcelId | Optional | Parcel identifier. |
houseNumber | Optional | House or villa number. |
buildingName | Optional | Name of the building or tower. |
floorNumber | Optional | Floor within a building. |
apartmentNumber | Optional | Apartment unit number. |
officeNumber | Optional | Office unit number. |
paciNumber | Optional | Kuwait Civil ID (PACI) parcel number. |
line1 | Optional | Free-text fallback address line. |
coordinates | Optional | { "lat": <number>, "lng": <number> }. |
Required for pricing
At minimum, each address needs governorateId and neighborhoodId — the delivery fee is resolved from them. Provide blockId as well whenever you can. If a customer gives you their Kuwait Civil ID (PACI number), resolve the full hierarchy in one call via GET /governorate-area/civil-id.
List customers
GET /integration/merchant/customer
Returns your merchant's customers with pagination. The list is always returned under data.list with data.pagination.
Query parameters:
| Parameter | Description |
|---|---|
page / limit | Pagination — default 1 / 20. |
phoneNumber | Exact match. Normalized before matching, so any format works (+965 5000-1234 matches 96550001234). |
email | Exact match, case-insensitive. |
name | Partial match, case-insensitive. |
search | Free-text search across name, email, and phone at once. |
customerType | individual or business. |
governorateId / neighborhoodId | Only customers with an address in the given area. |
active | true or false. |
sortField / sortDirection | Default createdAt / desc. |
bash
curl "https://www.wasal.org/api/v1/integration/merchant/customer?phoneNumber=%2B96550001234" \
-H "Authorization: Bearer pk_live_YOUR_KEY_HERE"Response: 200 OK
json
{
"success": true,
"data": {
"list": [
{
"_id": "665f1e2a9b3c4d5e6f708192",
"name": "Sara Ahmad",
"phoneNumber": "96550001234",
"email": "[email protected]",
"active": true,
"addresses": [
{
"_id": "665f1e2a9b3c4d5e6f708193",
"label": "Home",
"isDefault": true,
"governorateId": "<governorate-id>",
"neighborhoodId": "<neighborhood-id>",
"blockId": "4",
"governorateName": "Hawalli",
"neighborhoodName": "Salmiya",
"blockName": "Block 4"
}
],
"defaultAddress": { /* the default address, same shape as above */ },
"createdAt": "2025-05-20T09:15:00.000Z"
}
],
"pagination": { "page": 1, "limit": 20, "total": 1, "totalPages": 1 }
}
}
phoneNumber,namecan be combined (AND logic). Usesearchinstead when you want a single query matched against all three fields.
Get a customer
GET /integration/merchant/customer/:customerId
Returns a single customer (must belong to your merchant), including all addresses.
Query parameters:
| Parameter | Description |
|---|---|
includeRecentOrders | true to also return the customer's 5 most recent orders with your merchant under data.recentOrders. Omitted by default. |
bash
curl "https://www.wasal.org/api/v1/integration/merchant/customer/665f1e2a9b3c4d5e6f708192?includeRecentOrders=true" \
-H "Authorization: Bearer pk_live_YOUR_KEY_HERE"Response: 200 OK
json
{
"success": true,
"data": {
"customer": {
"_id": "665f1e2a9b3c4d5e6f708192",
"name": "Sara Ahmad",
"phoneNumber": "96550001234",
"email": "[email protected]",
"addresses": [
{
"_id": "665f1e2a9b3c4d5e6f708193",
"label": "Home",
"isDefault": true,
"addressType": "home",
"governorateId": "<governorate-id>",
"neighborhoodId": "<neighborhood-id>",
"blockId": "4",
"streetId": "12",
"houseNumber": "8",
"governorateName": "Hawalli",
"neighborhoodName": "Salmiya",
"blockName": "Block 4",
"streetName": "Street 12"
}
],
"createdAt": "2025-05-20T09:15:00.000Z",
"updatedAt": "2025-06-01T10:30:00.000Z"
},
"recentOrders": [
{
"_id": "6660a1b2c3d4e5f607081920",
"orderNumber": "ACME-000042",
"status": "delivered",
"pricing": { "deliveryFee": 1.5, "orderValue": 12.0 },
"orderBranchName": "Salmiya Branch",
"createdAt": "2025-06-01T10:30:00.000Z"
}
]
}
}data.recentOrders is only present when includeRecentOrders=true is sent.
Returns 404 CUSTOMER_NOT_FOUND if the customer doesn't exist or is not linked to your merchant.
Update a customer
PUT /integration/merchant/customer/:customerId
Updates the customer's name, phoneNumber, email, customerType, businessName, or contactPersonName. Only send the fields you want to change.
Request body:
json
{ "name": "Sara A. Al-Rashidi", "email": "[email protected]" }Response: 200 OK — updated customer object.
Add an address
POST /integration/merchant/customer/:customerId/address
Adds an address to an existing customer (max 5 total). The first address added to a customer with no addresses becomes the default.
Request body:
json
{
"label": "Office",
"governorateId": "<governorate-id>",
"neighborhoodId": "<neighborhood-id>",
"blockId": "7",
"streetId": "3",
"buildingName": "Trade Center",
"officeNumber": "402"
}label, governorateId, and neighborhoodId are required; the rest are optional (see Address fields).
Response: 201 Created
json
{
"success": true,
"data": {
"address": { "_id": "665f...", "label": "Office", "isDefault": false /* ... */ },
"addresses": [ /* all of the customer's addresses */ ]
}
}Update an address
PUT /integration/merchant/customer/:customerId/address/:addressId
Updates fields on an existing address. Send the same fields accepted at creation. Wasal verifies the customer belongs to your merchant before applying the change.
Set the default address
PUT /integration/merchant/customer/:customerId/address/:addressId/default
Marks the given address as the customer's default. When an order omits customerAddressId, the default address is used.
Delete an address
DELETE /integration/merchant/customer/:customerId/address/:addressId
Removes an address from the customer. The customer must have at least one remaining address before you can create new orders for them.
Delete a customer
DELETE /integration/merchant/customer/:customerId
Removes the customer from your merchant account. The customer record is soft-deleted and no longer accessible through your merchant key.
Customers with outstanding orders cannot be deleted. If the customer has one or more non-terminal orders (
draft,pending,assigned,on_way_to_merchant,picked_up,in_transit,on_hold) under your merchant account, the request returns409 CUSTOMER_HAS_ORDERS. Cancel or resolve the outstanding orders before deleting the customer.
Response: 200 OK
json
{ "success": true, "message": "Customer removed from merchant successfully" }Errors
| HTTP | code | Meaning |
|---|---|---|
| 400 | VALIDATION_FAILED | One or more fields invalid — see errors. |
| 400 | MAX_ADDRESSES_EXCEEDED | Customer already has 5 addresses. |
| 404 | CUSTOMER_NOT_FOUND | Customer does not exist or is not linked to your merchant. |
| 404 | ADDRESS_NOT_FOUND | Address does not exist on this customer. |
| 409 | CUSTOMER_PHONE_DUPLICATE | A customer with this phone already exists for your merchant. |
| 409 | CUSTOMER_EMAIL_DUPLICATE | A customer with this email already exists for your merchant. |
| 409 | CUSTOMER_HAS_ORDERS | Customer has outstanding (non-terminal) orders and cannot be deleted. |
See the full Error Reference.
