# Customers

Part of the [Salaf Commerce API reference](https://www.salafems.com/developers/reference.md). Paths below are shown exactly as the server routes them — prefix them with `https://api.salafems.com`. Authenticate every request with `Authorization: Bearer salaf_sk_YOUR_API_KEY`; each operation names the scope its key must hold. Every failure uses the one error envelope — see [Errors](https://www.salafems.com/developers/errors.md).

## List customers

`GET /ext/v1/customers` · Requires scope `customers:read`

Addresses are on the detail read only — a page of 100 customers should not carry every address any of them ever saved.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | `number` (default `25`, max 100) | No | Page size. |
| `starting_after` | `string` | No | Return the page AFTER this object id (the previous page's `next_cursor`). |
| `ending_before` | `string` | No | Return the page BEFORE this object id. |
| `sort` | `string` (default `-created_at`) | No | Newest first by default. Only `created_at` is sortable — a cursor over a mutable key (like `updated_at`) cannot page reliably. Use `updated_after` to sync changes. One of: `created_at`, `-created_at`. |
| `store_id` | `string` | No | Restrict to one store. Required breadth control for company-scoped keys; on a store-scoped key it must match the key's own store. |

### Request

**cURL**

```bash
export SALAF_API_KEY="salaf_sk_YOUR_API_KEY"

curl -sS "https://api.salafems.com/ext/v1/customers?limit=25" \
  -H "Authorization: Bearer $SALAF_API_KEY"
```

**JavaScript**

```javascript
const url = new URL("https://api.salafems.com/ext/v1/customers");
Object.entries({
  limit: "25",
}).forEach(([key, value]) => url.searchParams.set(key, value));

const response = await fetch(url, {
  headers: {
    Authorization: `Bearer ${process.env.SALAF_API_KEY}`,
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}

const { data, meta } = await response.json();
```

**Python**

```python
import os

import requests

response = requests.get(
    "https://api.salafems.com/ext/v1/customers",
    params={
        "limit": "25",
    },
    headers={
        "Authorization": f"Bearer {os.environ['SALAF_API_KEY']}",
    },
    timeout=30,
)

if not response.ok:
    error = response.json()["error"]
    raise RuntimeError(f"{error['code']}: {error['message']}")

page = response.json()
rows, meta = page["data"], page["meta"]
```

**PHP**

```php
<?php

$key = getenv('SALAF_API_KEY');

$ch = curl_init('https://api.salafems.com/ext/v1/customers?limit=25');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key],
    CURLOPT_TIMEOUT => 30,
]);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$response = json_decode($body, true);

if ($status >= 400) {
    throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}

$rows = $response['data'];
$meta = $response['meta'];
```

### Responses

- `200` — A cursor page of customers.
- `401` — Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.
- `403` — The key's scopes do not cover this endpoint.
- `422` — Validation failed — `error.fields` maps each offending field to its messages.
- `429` — Rate limit exceeded for this key. Honor `Retry-After` and the `X-RateLimit-*` headers.

**Example 200 response**

```json
{
  "data": [
    {
      "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "full_name": "Rafiqul Islam",
      "email": null,
      "phone": "01712345678",
      "status": "active",
      "type": "manual",
      "gender": null,
      "created_at": "2026-08-11T10:00:00.000Z",
      "updated_at": "2026-08-11T10:00:00.000Z"
    }
  ],
  "meta": {
    "has_more": true,
    "next_cursor": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001"
  }
}
```

## Create a customer

`POST /ext/v1/customers` · Requires scope `customers:write`

Phones are canonicalized (`+880…` spellings collapse to `01…`) and unique per store, as is email — a duplicate is a 409. API-created customers are always type `manual`.

### Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | `string` | No | Optional, but honored: send it to make retries of this request safe. |

### Body parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `store_id` | `uuid` | No | Target store. REQUIRED with a company-scoped key; with a store-scoped key it may only repeat the key's own store. |
| `full_name` | `string` | Yes | — |
| `phone` | `string` | No | Bangladeshi mobile, any accepted spelling — stored canonical (`01712345678`). Unique per store. |
| `email` | `string` | No | — |
| `status` | `string` (default `active`) | No | — One of: `active`, `inactive`, `blocked`. |
| `gender` | `string` | No | — One of: `male`, `female`. |

### Request

**cURL**

```bash
export SALAF_API_KEY="salaf_sk_YOUR_API_KEY"

curl -sS -X POST "https://api.salafems.com/ext/v1/customers" \
  -H "Authorization: Bearer $SALAF_API_KEY" \
  -H "Idempotency-Key: e7a1…-your-uuid" \
  -H "Content-Type: application/json" \
  -d '{
  "full_name": "Rafiqul Islam",
  "phone": "01712345678"
}'
```

**JavaScript**

```javascript
const url = new URL("https://api.salafems.com/ext/v1/customers");

const response = await fetch(url, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SALAF_API_KEY}`,
    "Idempotency-Key": "e7a1…-your-uuid",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "full_name": "Rafiqul Islam",
    "phone": "01712345678"
  }),
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}

const object = await response.json();
```

**Python**

```python
import os

import requests

payload = {
    "full_name": "Rafiqul Islam",
    "phone": "01712345678",
}

response = requests.post(
    "https://api.salafems.com/ext/v1/customers",
    json=payload,
    headers={
        "Authorization": f"Bearer {os.environ['SALAF_API_KEY']}",
        "Idempotency-Key": "e7a1…-your-uuid",
    },
    timeout=30,
)

if not response.ok:
    error = response.json()["error"]
    raise RuntimeError(f"{error['code']}: {error['message']}")

obj = response.json()
```

**PHP**

```php
<?php

$key = getenv('SALAF_API_KEY');

$payload = json_encode([
    'full_name' => 'Rafiqul Islam',
    'phone' => '01712345678',
]);

$ch = curl_init('https://api.salafems.com/ext/v1/customers');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Idempotency-Key: e7a1…-your-uuid', 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_TIMEOUT => 30,
]);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$response = json_decode($body, true);

if ($status >= 400) {
    throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}
```

### Responses

- `201` — The created customer, addresses included.
- `401` — Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.
- `403` — The key's scopes do not cover this endpoint.
- `409` — A customer with this phone or email already exists.
- `422` — Validation failed — `error.fields` maps each offending field to its messages.
- `429` — Rate limit exceeded for this key. Honor `Retry-After` and the `X-RateLimit-*` headers.

**Example 200 response**

```json
{
  "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "full_name": "Rafiqul Islam",
  "email": null,
  "phone": "01712345678",
  "status": "active",
  "type": "manual",
  "gender": null,
  "created_at": "2026-08-11T10:00:00.000Z",
  "updated_at": "2026-08-11T10:00:00.000Z",
  "addresses": [
    {
      "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "label": "Home",
      "recipient_name": "Rafiqul Islam",
      "recipient_phone": "01712345678",
      "address_line": "House 12, Road 5",
      "city": "Dhaka",
      "area": "Dhanmondi",
      "postal_code": "1209",
      "is_default": true,
      "created_at": "2026-08-11T10:00:00.000Z",
      "updated_at": "2026-08-11T10:00:00.000Z"
    }
  ]
}
```

## Get a customer by id, with their addresses

`GET /ext/v1/customers/{id}` · Requires scope `customers:read`

The customer plus every saved address, default address first.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `uuid` | Yes | The customer id. |

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `store_id` | `uuid` | No | Restrict the lookup to one store. Breadth control for company-scoped keys; on a store-scoped key it must match the key's own store. |

### Request

**cURL**

```bash
export SALAF_API_KEY="salaf_sk_YOUR_API_KEY"

curl -sS "https://api.salafems.com/ext/v1/customers/REPLACE_WITH_ID" \
  -H "Authorization: Bearer $SALAF_API_KEY"
```

**JavaScript**

```javascript
const url = new URL("https://api.salafems.com/ext/v1/customers/REPLACE_WITH_ID");

const response = await fetch(url, {
  headers: {
    Authorization: `Bearer ${process.env.SALAF_API_KEY}`,
  },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}

const object = await response.json();
```

**Python**

```python
import os

import requests

response = requests.get(
    "https://api.salafems.com/ext/v1/customers/REPLACE_WITH_ID",
    headers={
        "Authorization": f"Bearer {os.environ['SALAF_API_KEY']}",
    },
    timeout=30,
)

if not response.ok:
    error = response.json()["error"]
    raise RuntimeError(f"{error['code']}: {error['message']}")

obj = response.json()
```

**PHP**

```php
<?php

$key = getenv('SALAF_API_KEY');

$ch = curl_init('https://api.salafems.com/ext/v1/customers/REPLACE_WITH_ID');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key],
    CURLOPT_TIMEOUT => 30,
]);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$response = json_decode($body, true);

if ($status >= 400) {
    throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}
```

### Responses

- `200` — The customer.
- `401` — Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.
- `403` — The key's scopes do not cover this endpoint.
- `404` — Customer not found.
- `422` — Validation failed — `error.fields` maps each offending field to its messages.
- `429` — Rate limit exceeded for this key. Honor `Retry-After` and the `X-RateLimit-*` headers.

**Example 200 response**

```json
{
  "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "full_name": "Rafiqul Islam",
  "email": null,
  "phone": "01712345678",
  "status": "active",
  "type": "manual",
  "gender": null,
  "created_at": "2026-08-11T10:00:00.000Z",
  "updated_at": "2026-08-11T10:00:00.000Z",
  "addresses": [
    {
      "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "label": "Home",
      "recipient_name": "Rafiqul Islam",
      "recipient_phone": "01712345678",
      "address_line": "House 12, Road 5",
      "city": "Dhaka",
      "area": "Dhanmondi",
      "postal_code": "1209",
      "is_default": true,
      "created_at": "2026-08-11T10:00:00.000Z",
      "updated_at": "2026-08-11T10:00:00.000Z"
    }
  ]
}
```

## Update a customer

`PATCH /ext/v1/customers/{id}` · Requires scope `customers:write`

Same rules as creation (canonical phone, per-store uniqueness). The seeded walk-in system record cannot be edited through any surface, this one included.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `uuid` | Yes | The customer id. |

### Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | `string` | No | Optional, but honored: send it to make retries of this request safe. |

### Body parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `store_id` | `uuid` | No | Target store. REQUIRED with a company-scoped key; with a store-scoped key it may only repeat the key's own store. |
| `full_name` | `string` | No | — |
| `phone` | `string` | No | — |
| `email` | `string` | No | — |
| `status` | `string` | No | — One of: `active`, `inactive`, `blocked`. |
| `gender` | `string` | No | — One of: `male`, `female`. |

### Request

**cURL**

```bash
export SALAF_API_KEY="salaf_sk_YOUR_API_KEY"

curl -sS -X PATCH "https://api.salafems.com/ext/v1/customers/REPLACE_WITH_ID" \
  -H "Authorization: Bearer $SALAF_API_KEY" \
  -H "Idempotency-Key: e7a1…-your-uuid" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "rafiq@example.com"
}'
```

**JavaScript**

```javascript
const url = new URL("https://api.salafems.com/ext/v1/customers/REPLACE_WITH_ID");

const response = await fetch(url, {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.SALAF_API_KEY}`,
    "Idempotency-Key": "e7a1…-your-uuid",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "email": "rafiq@example.com"
  }),
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.code}: ${error.message} [${error.request_id}]`);
}

const object = await response.json();
```

**Python**

```python
import os

import requests

payload = {
    "email": "rafiq@example.com",
}

response = requests.patch(
    "https://api.salafems.com/ext/v1/customers/REPLACE_WITH_ID",
    json=payload,
    headers={
        "Authorization": f"Bearer {os.environ['SALAF_API_KEY']}",
        "Idempotency-Key": "e7a1…-your-uuid",
    },
    timeout=30,
)

if not response.ok:
    error = response.json()["error"]
    raise RuntimeError(f"{error['code']}: {error['message']}")

obj = response.json()
```

**PHP**

```php
<?php

$key = getenv('SALAF_API_KEY');

$payload = json_encode([
    'email' => 'rafiq@example.com',
]);

$ch = curl_init('https://api.salafems.com/ext/v1/customers/REPLACE_WITH_ID');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'PATCH',
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Idempotency-Key: e7a1…-your-uuid', 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_TIMEOUT => 30,
]);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$response = json_decode($body, true);

if ($status >= 400) {
    throw new RuntimeException("{$response['error']['code']}: {$response['error']['message']}");
}
```

### Responses

- `200` — The updated customer, addresses included.
- `401` — Missing/invalid API key, revoked or expired key, plan without API access, or a dead store.
- `403` — The key's scopes do not cover this endpoint.
- `404` — Customer not found.
- `409` — A customer with this phone or email already exists.
- `422` — Validation failed — `error.fields` maps each offending field to its messages.
- `429` — Rate limit exceeded for this key. Honor `Retry-After` and the `X-RateLimit-*` headers.

**Example 200 response**

```json
{
  "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "store_id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
  "full_name": "Rafiqul Islam",
  "email": null,
  "phone": "01712345678",
  "status": "active",
  "type": "manual",
  "gender": null,
  "created_at": "2026-08-11T10:00:00.000Z",
  "updated_at": "2026-08-11T10:00:00.000Z",
  "addresses": [
    {
      "id": "5f7d2f60-0d1c-4b3a-9a68-6f4d21f6a001",
      "label": "Home",
      "recipient_name": "Rafiqul Islam",
      "recipient_phone": "01712345678",
      "address_line": "House 12, Road 5",
      "city": "Dhaka",
      "area": "Dhanmondi",
      "postal_code": "1209",
      "is_default": true,
      "created_at": "2026-08-11T10:00:00.000Z",
      "updated_at": "2026-08-11T10:00:00.000Z"
    }
  ]
}
```
