# Getting started

From no key to your first response, in five minutes.

The Salaf Commerce API is a REST API over your store's real data — the same
products, orders, customers and stock your staff see in the dashboard. It
speaks JSON, authenticates with a single header, and needs nothing installed.

This page takes you from no key to a real response. It should take about five
minutes.

> **Note — What ships today**
>
> Reading, writing and webhooks are all live: 17 read endpoints, 10 write
> endpoints, and 24 outbound event types. What is *not* here is listed plainly
> under [what is not here yet](#what-is-not-here-yet).

## Before you start

You need three things, and all three are things a store owner already has or
can grant in a minute:

1. **A Salaf EMS account** with access to the store you want to integrate.
2. **A plan that includes API access.** The `api_access` feature is checked on
   every single request, not just when the key is made — so a plan change takes
   effect on the next call in either direction. Without it every request
   answers `401 UNAUTHORIZED` with the message *"API access is not included in
   your current plan."*
3. **The `developer` permission** on your user (`developer.read` to see keys,
   `developer.manage` to create them). Company owners have it already.

## Create an API key

Keys are created in the dashboard, never through the API.

1. Open **Settings → Developer** and choose **Create API key**.
2. Pick the key type:
   - **Store key** — reads one store. This is the right default.
   - **Company key** — reads every live store in the company. Only a company
     owner can create one.
3. Give it a name you will recognise in six months (*"NetSuite nightly sync"*
   beats *"test"*).
4. Tick the scopes it needs. You can only grant scopes your own role can back —
   a user who cannot read orders in the dashboard cannot mint a key that reads
   them over the API.
5. Optionally set an expiry date.

> **Warning — The secret is shown exactly once**
>
> Salaf stores only a SHA-256 hash of your key, so there is no "show it again"
> button — not for you, and not for our support team. Copy it into your secret
> manager on the spot. If you lose it, rotate the key.

A key looks like this — the literal prefix `salaf_sk_` followed by 40
characters:

```text
salaf_sk_7f3kq9p2xR4mVnB8tLcYwZ1sD6hJ0gKeQaUiOpNm
```

## Make your first request

Authenticate by putting the key in an `Authorization: Bearer` header. That is
the whole authentication story — no signing, no token exchange, no expiry to
refresh.

Ask for one product. If the key works, this returns data; if it does not, the
error tells you precisely why.

**cURL**

```bash
export SALAF_API_KEY="salaf_sk_YOUR_API_KEY"

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

**JavaScript**

```javascript
const url = new URL("https://api.salafems.com/ext/v1/products");
Object.entries({
  limit: "1",
}).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/products",
    params={
        "limit": "1",
    },
    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/products?limit=1');
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'];
```

## Read the response

A list endpoint answers with the rows under `data` and a cursor under `meta`:

```json
{
  "data": [
    {
      "id": "7c4e1d90-3b2a-4f58-9e6d-1a5c8b30f2d4",
      "store_id": "b21c5f04-9d7e-4c11-8f3a-6e0d2b915a77",
      "name": "Premium Cotton T-Shirt",
      "slug": "premium-cotton-t-shirt",
      "status": "active",
      "delivery_charge": "60.00",
      "variants": [
        {
          "id": "0f9c1b8e-5f21-4a3d-9b6c-2d7e8a41c503",
          "sku": "TSHIRT-BLK-M",
          "name": "Black / M",
          "price": "1250.00",
          "discount_price": "1100.00",
          "status": "active"
        }
      ],
      "created_at": "2026-07-14T09:12:44.000Z",
      "updated_at": "2026-08-09T17:31:02.000Z"
    }
  ],
  "meta": {
    "has_more": true,
    "next_cursor": "7c4e1d90-3b2a-4f58-9e6d-1a5c8b30f2d4"
  }
}
```

Four conventions hold everywhere, so learning them once is enough:

| Convention                            | Looks like                   | Why                                                                                                                         |
| ------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| Field names are `snake_case`          | `payment_status`             | Stable public names, decoupled from our internal columns.                                                                   |
| Money is a **string** with 2 decimals | `"1250.00"`                  | JSON numbers are IEEE-754 doubles and cannot hold every `Decimal(12,2)` exactly. `1250.10` must not read back as `1250.1`.  |
| Timestamps are ISO-8601 **UTC**       | `"2026-08-09T17:31:02.000Z"` | Always with the `Z`. No local time, ever.                                                                                   |
| Lists are cursor-paginated            | `meta.next_cursor`           | Pages stay stable while the store keeps taking orders. See [Pagination](https://www.salafems.com/developers/pagination.md). |

Fetching a single object returns that object directly — there is no envelope
around it, and no `success` field to check. The HTTP status is the answer.

## The base URL

> **Warning — Use /ext/v1 — it is the path that works today**
>
> The production base URL is:
>
> `https://api.salafems.com/ext/v1`
>
> A shorter `https://api.salafems.com/v1` alias is planned and the proxy rule
> for it is written, but it has **not been deployed** — it will not resolve
> yet. Build against `/ext/v1`. When the alias goes live it will be announced
> in the [changelog](https://www.salafems.com/developers/changelog.md), both paths will serve the same
> routes, and nothing you have written will break.

Running the backend locally, the same routes are on your own port with no proxy
in the way:

```bash
# Default local backend port is 5000 (6003 under Docker Compose).
curl -sS "http://localhost:5000/ext/v1/products?limit=1" \
  -H "Authorization: Bearer $SALAF_API_KEY"
```

## When something goes wrong

Errors are always the same shape: one `error` object with a stable machine
`code`, a human `message`, and a `request_id`.

```json
{
  "error": {
    "code": "FORBIDDEN_SCOPE",
    "message": "This API key is missing the `products:read` scope.",
    "request_id": "4f1c9b02-7d3e-4a85-9c16-b0e2f7a34d59"
  }
}
```

The three you are most likely to hit in the first five minutes:

- **`401 UNAUTHORIZED`** — the header is missing or malformed, the key is
  unknown, or your plan does not include API access.
- **`403 FORBIDDEN_SCOPE`** — the key is valid but was not given this scope.
  Scopes are fixed when a key is created, so make a new key.
- **`404 NOT_FOUND`** — the id does not exist *in the store this key can see*.
  A record belonging to another tenant returns 404 rather than 403, on purpose:
  a 403 would confirm the id exists.

Keep the `request_id`. It is echoed in the `X-Request-Id` response header and
is what support needs to find your exact request in our logs. Full list on the
[Errors](https://www.salafems.com/developers/errors.md) page.

## What is not here yet

Being straight about the boundaries, so you can plan around them:

| Capability                                                                    | Status                                                                                         |
| ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| Reading products, orders, customers, stock, locations, channels               | **Available**                                                                                  |
| Creating orders and payments, adjusting stock, writing products and customers | **Available** — see [Writes & idempotency](https://www.salafems.com/developers/idempotency.md) |
| Webhooks (outbound events)                                                    | **Available** — see [Webhooks](https://www.salafems.com/developers/webhooks.md)                |
| Managing webhook endpoints over the API                                       | Dashboard only. The `webhooks:manage` scope is declared but not grantable                      |
| Reading or writing returns, shipments and POS sessions                        | Webhook events only — there is no REST endpoint for them yet                                   |
| SDKs for any language                                                         | Not planned for v1 — the four sample languages are the support level                           |
| A sandbox or test-mode key                                                    | Not planned for v1. There is no `salaf_sk_test_`; keys act on live data                        |

## Next steps

- [Authentication](https://www.salafems.com/developers/authentication.md) — store vs company keys,
  rotation without downtime, and revocation.
- [Pagination](https://www.salafems.com/developers/pagination.md) — the `updated_after` pattern every
  sync integration ends up needing.
- [Writes & idempotency](https://www.salafems.com/developers/idempotency.md) — every write endpoint and
  the `Idempotency-Key` contract.
- [Webhooks](https://www.salafems.com/developers/webhooks.md) — stop polling for the things you need to
  know about quickly.
- [Guides](https://www.salafems.com/developers/guides.md) — four end-to-end recipes.
- [API reference](https://www.salafems.com/developers/reference.md) — every endpoint, generated from the
  published OpenAPI description.
