# Customers

> Create a person or a company, what each type requires, the ID types, finding a customer by phone or by your own reference, and how updates work.

Every sales invoice and payment made through the API names a customer. A customer is a person or a company, and the type decides the invoice: a company gets a standard invoice that ZATCA clears, a person gets a simplified one. See [Start here](https://docs.kosoor.com.sa/start-here.md#standard-and-simplified).

| Endpoint | Permission |
|---|---|
| `POST /v1/customers` | `customers:write` |
| `GET /v1/customers` | `customers:read` |
| `GET /v1/customers/{id}` | `customers:read` |
| `PATCH /v1/customers/{id}` | `customers:write` |

## Create a person

A person needs `type` and `phone`. The phone is in E.164 form: `+`, the country code, then the number, such as `+966501234567`.

```bash
curl https://api.kosoor.com.sa/v1/customers \
  -H "Authorization: Bearer ksr_test_..." \
  -H "Idempotency-Key: 6f1d2c3b-4a5e-4f60-9b7a-8c9d0e1f2a3b" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "person",
    "firstName": "Sara",
    "lastName": "Al-Harbi",
    "phone": "+966501234567",
    "email": "sara@example.com",
    "referenceId": "crm-88412"
  }'
```

`201 Created`:

```json
{
  "id": "0f8fad5b-d9cb-469f-a165-70867728950e",
  "object": "customer",
  "type": "person",
  "referenceId": "crm-88412",
  "firstName": "Sara",
  "lastName": "Al-Harbi",
  "phone": "+966501234567",
  "email": "sara@example.com",
  "isActive": true,
  "branchId": null,
  "company": null,
  "address": {
    "street": null,
    "buildingNumber": null,
    "postalCode": null,
    "district": null,
    "city": null,
    "country": null
  },
  "metadata": {},
  "standardInvoiceMissing": null,
  "createdAt": "2026-11-07T09:30:12.512Z"
}
```

A person may also send `address.street`, `address.district`, `address.city` and `address.country`. `buildingNumber`, `postalCode` and the `company` object are for companies only, and so is a missing phone.

## Create a company

A company needs `type`, `company.name` and `address.country`. The country is an ISO 3166 two-letter code, such as `SA`. A company does not need a phone, and it never takes `firstName` or `lastName`: its name is `company.name`.

```bash
curl https://api.kosoor.com.sa/v1/customers \
  -H "Authorization: Bearer ksr_test_..." \
  -H "Idempotency-Key: 2b8e4f9a-1c3d-4e5f-8a6b-7c8d9e0f1a2b" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "company",
    "phone": "+966112345678",
    "company": {
      "name": "Al Noor Logistics",
      "vatNumber": "310123456700003"
    },
    "address": {
      "street": "Prince Sultan Road",
      "buildingNumber": "7421",
      "postalCode": "23521",
      "district": "Al Rawdah",
      "city": "Jeddah",
      "country": "SA"
    },
    "referenceId": "crm-90017"
  }'
```

`201 Created`:

```json
{
  "id": "3f1c2a6e-8b4d-4e2a-9c1f-5d7e8a9b0c12",
  "object": "customer",
  "type": "company",
  "referenceId": "crm-90017",
  "firstName": null,
  "lastName": null,
  "phone": "+966112345678",
  "email": null,
  "isActive": true,
  "branchId": null,
  "company": {
    "name": "Al Noor Logistics",
    "vatNumber": "310123456700003",
    "idType": null,
    "idValue": null,
    "creditLimit": 0,
    "creditPeriodDays": null
  },
  "address": {
    "street": "Prince Sultan Road",
    "buildingNumber": "7421",
    "postalCode": "23521",
    "district": "Al Rawdah",
    "city": "Jeddah",
    "country": "SA"
  },
  "metadata": {},
  "standardInvoiceMissing": [],
  "createdAt": "2026-11-07T09:31:05.208Z"
}
```

### What a standard invoice needs

A company can be created with only its name and country, but a standard invoice needs more. `standardInvoiceMissing` lists what this company still lacks, as field paths, and is empty when nothing is missing:

| Needed | When |
|---|---|
| `company.vatNumber`, or `company.idType` with `company.idValue` | always |
| `address.street`, `address.city`, `address.country` | always |
| `address.buildingNumber`, `address.postalCode`, `address.district` | when the country is `SA` |

An invoice for a company that still lacks any of them answers `422 customer_address_incomplete`, with the same list in `missing`. Check `standardInvoiceMissing` before you invoice, and fill the gaps with `PATCH`.

### Company rules

| Field | Rule |
|---|---|
| `company.vatNumber` | 15 digits that start and end with 3 (`400 invalid_vat_number`); no other customer may have it (`409 vat_number_taken`) |
| `company.idType`, `company.idValue` | sent together, both set or both `null`; `idValue` is 1 to 50 letters and digits |
| `address.buildingNumber` | 4 digits when the country is `SA`; up to 20 characters elsewhere |
| `address.postalCode` | 5 digits when the country is `SA`; up to 20 characters elsewhere |
| `company.creditLimit`, `company.creditPeriodDays` | in the reply only; the merchant sets them on the dashboard. A credit limit of 0 means no limit. |

### ID types

When a company has no VAT number, or as well as one, `idType` says what kind of number `idValue` is. These are ZATCA's codes for a buyer's ID:

| `idType` | Number |
|---|---|
| `CRN` | Commercial registration number |
| `MOM` | Licence from the Ministry of Municipal and Rural Affairs |
| `MLS` | Licence from the Ministry of Human Resources |
| `700` | 700 number |
| `SAG` | Investment licence (formerly SAGIA, now the Ministry of Investment) |
| `NAT` | National ID |
| `GCC` | GCC ID |
| `IQA` | Iqama (residence permit) |
| `PAS` | Passport |
| `OTH` | Any other ID |

## Fields every customer takes

| Field | Rule |
|---|---|
| `type` | `person` or `company`; required on create and never changes |
| `phone` | E.164; no other customer may have it (`409 phone_taken`) |
| `firstName`, `lastName` | a person's name, up to 100 characters each |
| `email` | a valid address, up to 256 characters |
| `referenceId` | your own id for the customer, up to 100 characters; no other customer may have it (`409 reference_id_taken`); set once |
| `branchId` | the branch the customer belongs to; a branch your key may use (`403 branch_not_allowed`) |
| `isActive` | `true` unless you send `false`; a disabled customer's invoices and payments answer `422 customer_inactive` |
| `metadata` | up to 20 keys of up to 40 characters, values up to 500 characters |

When a value is already taken, the error's `existingId` is the customer that holds it. Use it instead of creating a second customer:

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "phone_taken",
    "message": "Another customer already has this phone number.",
    "param": "phone",
    "docUrl": "https://docs.kosoor.com.sa/errors#phone_taken",
    "existingId": "0f8fad5b-d9cb-469f-a165-70867728950e"
  }
}
```

## Find a customer

`GET /v1/customers` takes exact-match filters, combined with AND:

| Parameter | Matches |
|---|---|
| `phone` | the phone, in E.164 |
| `vatNumber` | `company.vatNumber` |
| `idValue` | `company.idValue` |
| `referenceId` | your own id |

Encode the `+` of a phone as `%2B`. A bare `+` in a query string reads as a space.

```bash
curl "https://api.kosoor.com.sa/v1/customers?phone=%2B966501234567" \
  -H "Authorization: Bearer ksr_test_..."
```

```json
{
  "data": [
    {
      "id": "0f8fad5b-d9cb-469f-a165-70867728950e",
      "object": "customer",
      "type": "person",
      "referenceId": "crm-88412",
      "firstName": "Sara",
      "lastName": "Al-Harbi",
      "phone": "+966501234567",
      "email": "sara@example.com",
      "isActive": true,
      "branchId": null,
      "company": null,
      "address": {
        "street": null,
        "buildingNumber": null,
        "postalCode": null,
        "district": null,
        "city": null,
        "country": null
      },
      "metadata": {},
      "standardInvoiceMissing": null,
      "createdAt": "2026-11-07T09:30:12.512Z"
    }
  ],
  "hasMore": false
}
```

An empty `data` means no customer matches. Without filters, the list pages through every customer, newest first; see [Pagination](https://docs.kosoor.com.sa/pagination.md).

## Update a customer

`PATCH /v1/customers/{id}` changes only the fields the body carries; every other field keeps its value. Inside `company` and `address`, too, only the fields you send change.

```bash
curl -X PATCH https://api.kosoor.com.sa/v1/customers/3f1c2a6e-8b4d-4e2a-9c1f-5d7e8a9b0c12 \
  -H "Authorization: Bearer ksr_test_..." \
  -H "Idempotency-Key: 8d7c6b5a-4f3e-4d2c-9b1a-0f9e8d7c6b5a" \
  -H "Content-Type: application/json" \
  -d '{ "email": "accounts@alnoor.example", "address": { "street": "King Abdulaziz Road" } }'
```

The reply is the whole customer, as `GET` returns it.

| You send | Result |
|---|---|
| a field with a value | the field takes it |
| `email` or `phone` as `null`, or a person's `firstName` or `lastName` | the field is cleared; a person's phone cannot be cleared |
| `metadata` | replaces the whole map; `{}` or `null` empties it |
| `type` | must equal the current type; a customer never changes type |
| `referenceId` | accepted once, while the customer has none; after that, a different value answers `400 invalid_parameter` |
| `company` or `address` as `null` | `400 invalid_parameter`: send the fields to change instead |
| `isActive` as `null` | `400 invalid_parameter`: send `true` or `false` |

A request that breaks any rule changes nothing.

Source: https://docs.kosoor.com.sa/customers
