SALAF EMSDevelopers
salafems.com

Authentication

Key format, store vs company keys, rotation, revocation.

Every request carries an API key in an Authorization header. There is no token exchange, no refresh, and nothing expires unless you ask it to.

Authorization: Bearer salaf_sk_7f3kq9p2xR4mVnB8tLcYwZ1sD6hJ0gKeQaUiOpNm

Key format

A key is the literal prefix salaf_sk_ followed by 40 random base62 characters — around 238 bits of entropy.

The prefix is not decoration. It earns its place three times: secret scanners (GitHub and GitLab push protection among them) match on exactly this shape, so a key committed by accident gets caught; it tells you at a glance which of the tokens in a config file is ours; and it makes a Salaf key structurally impossible to confuse with a JWT. Anything that does not start with salaf_sk_ is rejected before we even look in the database, so a stolen dashboard session token can never be replayed here.

In the dashboard and in our logs a key appears masked, as its prefix sample and last four characters:

salaf_sk_7f3kq9p2…OpNm

Store keys and company keys

The key type is chosen at creation and can never change. It decides how wide the tenant fence is — never how the fence works.

Store keyCompany key
SeesOne storeEvery live store in the company
Created byAnyone with developer.manageCompany owners only
store_id on rowsThe key's own storeVaries per row — partition on it
?store_id= filterMust match the key's own storeNarrows to one store in the company
Best forA single storefront, a POS bridge, one shop's ERPA head office reading across branches

Start with a store key. A company key is a wider blast radius if it leaks, and most integrations only ever touch one store. Reach for a company key when you genuinely need one credential to read several stores.

export SALAF_API_KEY="salaf_sk_YOUR_API_KEY"

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

Narrowing a company key

Pass store_id to restrict a read to one store. The value is validated against your company before any query runs — an id belonging to someone else's company returns 404, not 403, so the parameter can never be used to probe for which store ids exist.

export SALAF_API_KEY="salaf_sk_YOUR_API_KEY"

curl -sS "https://api.salafems.com/ext/v1/orders?store_id=REPLACE_WITH_STORE_ID&limit=50" \
  -H "Authorization: Bearer $SALAF_API_KEY"

Scopes

A key carries a set of scopes, fixed when it is created. Scopes are named resource:action and there are only two verbs — read and write.

ScopeCoversEndpoints today
products:readProducts, variants, categories, brandsYes
orders:readOrders, their items and payment stateYes
customers:readCustomers and their addressesYes
inventory:readStock levels and the movement ledgerYes
locations:readWarehouses and stock locationsYes
channels:readSales channelsYes
payments:readPayments recorded against an orderYes
products:writeCreate, update and archive productsYes
orders:writeCreate orders, change status, cancelYes
customers:writeCreate and update customersYes
inventory:writeStock adjustmentsYes
payments:writeRecord a payment against an orderYes
webhooks:manageManage webhook endpoints via the APIDeclared, not grantable

Three rules govern them:

  • Write implies read of the same resource. A key with orders:write also satisfies orders:read. The reverse is never true, which is the direction that actually matters.
  • Scopes are immutable. No endpoint changes a key's scopes, and the dashboard does not offer it either. Need different scopes? Create a new key and revoke the old one.
  • You can only grant what you hold. A staff user can only tick scopes their own role can back. A cashier who cannot read orders in the dashboard cannot mint a key that reads them over the API.

Every operation in the reference shows the scope it requires. A key missing it gets 403 FORBIDDEN_SCOPE — the endpoint is never silently reduced to an empty result.

Rotation

Rotating replaces a key without an outage. Salaf issues a new key and sets an expiry on the old one instead of killing it, so both work during the overlap:

  1. Rotate in the dashboard and copy the new secret.
  2. Both keys are valid for the grace window — 24 hours by default.
  3. Deploy the new key wherever the old one lives.
  4. The old key stops working when the window closes. Nothing to remember, no second visit to the dashboard.

If you rotate because a key leaked, choose the immediate option instead: the old key dies at once and the overlap is skipped. A brief outage is the correct trade when the alternative is a live credential in someone else's hands.

Revocation

Revoking is permanent — a revoked key never comes back, and there is no "delete" at all, because the row is audit history.

Revocation takes effect immediately. Authentication is cached for speed, but every revocation and rotation explicitly evicts that cache rather than waiting for it to expire, so there is no window in which a killed key still works.

Why a request was rejected

All authentication failures return 401 and are told apart by their code:

CodeMeaning
UNAUTHORIZEDThe header is missing or malformed, the key is unknown, your plan does not include API access, or the store or company is not live.
KEY_REVOKEDThe key existed and was deliberately killed.
KEY_EXPIREDThe key's clock ran out — either its own expiry date, or the end of a rotation grace window.

Two more you will see:

  • 403 FORBIDDEN_SCOPE — the key is fine, but it was not granted this scope.
  • 404 NOT_FOUND — for an id in another tenant. Never 403; a 403 would confirm the record exists.

Keeping keys safe

  • Server-side only. A key grants everything its scopes allow across a whole store. It has no business in a browser, a mobile app, or anything else a customer can open.
  • Environment variables, not source. Every sample in these docs reads SALAF_API_KEY from the environment for exactly this reason.
  • One key per integration. Separate keys mean you can revoke the one that leaked without taking down the other four, and last_used_at in the dashboard tells you which is which.
  • Only the scopes it needs. A reporting tool that reads orders should not hold customers:read as well.