# Categories

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 categories

`GET /ext/v1/categories` · Requires scope `products:read`

Catalog structure; `parent_id` builds the tree client-side.

### 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. |
| `status` | `string` | No | Filter to one of the values below. One of: `active`, `inactive`. |

### Request

**cURL**

```bash
export SALAF_API_KEY="salaf_sk_YOUR_API_KEY"

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

**JavaScript**

```javascript
const url = new URL("https://api.salafems.com/ext/v1/categories");
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/categories",
    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/categories?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 categories.
- `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",
      "parent_id": null,
      "name": "T-Shirts",
      "slug": "t-shirts",
      "image": null,
      "description": null,
      "status": "active",
      "position": 0,
      "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"
  }
}
```

## Get a category by id

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

One category node; follow `parent_id` for its ancestors.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | `uuid` | Yes | The category 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/categories/REPLACE_WITH_ID" \
  -H "Authorization: Bearer $SALAF_API_KEY"
```

**JavaScript**

```javascript
const url = new URL("https://api.salafems.com/ext/v1/categories/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/categories/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/categories/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 category.
- `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` — Category 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",
  "parent_id": null,
  "name": "T-Shirts",
  "slug": "t-shirts",
  "image": null,
  "description": null,
  "status": "active",
  "position": 0,
  "created_at": "2026-08-11T10:00:00.000Z",
  "updated_at": "2026-08-11T10:00:00.000Z"
}
```
