SALAF EMSDevelopers
salafems.com

Changelog

Every change to the public API, newest first.

Every change to the public API, newest first. Additive changes ship inside v1; anything breaking follows the deprecation policy.

2026-08-15 — The API speaks MCP

The API is now also a Model Context Protocol server at https://api.salafems.com/ext/mcp/v1 — Streamable HTTP, the same Bearer keys, the same tenant boundary, the same scopes. Nothing about REST changes; this is a second, agent-shaped door into the same implementation.

  • 22 curated tools: 12 reads over products, orders, payments, customers, inventory, locations and channels, and 10 writes — one per REST write endpoint. Results are byte-equivalent to the corresponding REST responses.
  • Each tool requires its REST twin's scope. A refusal is an in-band, readable error naming the missing scope; a read-scoped key cannot write.
  • Write tools take idempotency_key as an argument (MCP has no headers) — required on create_order, record_order_payment and adjust_inventory, honoured everywhere else, with the same replay/conflict rules as the header.
  • A companion docs MCP server — no auth, answers from these pages — runs at https://www.salafems.com/developers/mcp. See Build with AI.

2026-08-11 — Write endpoints and idempotency

Ten write endpoints, all running the same code the dashboard runs — the same stock reservation, coupon engine, totals math and status-transition rules. See Writes & idempotency.

ResourceEndpoints
OrdersPOST /orders, PATCH /orders/{id}/status, POST /orders/{id}/cancel
PaymentsPOST /orders/{id}/payments, and the read that completes the resource, GET /orders/{id}/payments
CustomersPOST /customers, PATCH /customers/{id}
InventoryPOST /inventory/adjustments
ProductsPOST /products, PATCH /products/{id}, POST /products/{id}/archive
  • Idempotency-Key is now honoured on every write and required on POST /orders, POST /orders/{id}/payments and POST /inventory/adjustments (400 naming the header when it is missing). A repeat of a completed request replays the original response verbatim with Idempotent-Replayed: true; a repeat with a different body is 422 IDEMPOTENCY_CONFLICT; a repeat while the original is running is 409 IDEMPOTENCY_IN_FLIGHT. Keys are isolated per API key and expire after 24 hours.
  • Company keys must send store_id in the body on every write (422 without it). A store key may repeat its own store id and may not name another (422).
  • Find-or-create customers on order creation, matched on the canonical Bangladesh phone number. An existing customer is never modified by an order.
  • Two new error codes are now reachable: INSUFFICIENT_STOCK and INVALID_STATUS_TRANSITION, both 400, both carrying fields.
  • The write rate-limit bucket is now in use — 30 requests per minute per key by default, counted separately from reads.
  • The OpenAPI description now carries 50 response schemas, so every example in the reference is generated from the same artifact that drives the server's validation instead of being written by hand.

Write scopes are grantable

orders:write, payments:write, customers:write, inventory:write and products:write now have endpoints behind them. payments:read does too. webhooks:manage remains declared but not grantable.

2026-08-11 — Webhooks

Outbound events, so you can stop polling for the things you need to know about quickly. See Webhooks.

  • 24 event types across orders, returns, shipping, inventory, catalog, customers and POS, plus salaf.ping for testing. Subscribe to specific types or to *. The catalog is also readable from the dashboard API.
  • Endpoints are created in the dashboard under Settings → Developer → Webhooks. https only, public addresses only, no redirects followed, five per store by default.
  • HMAC-SHA256 signatures over "{timestamp}.{rawBody}" in X-Salaf-Signature, with published test vectors so you can check your verification code before going live.
  • Rotation dual-signs for 24 hours — the previous secret keeps co-signing as a second v1=, so rotating costs no downtime.
  • At-least-once delivery, no ordering guarantee. De-duplicate on X-Salaf-Event-Id and write handlers as upserts.
  • An initial attempt plus eight retries over ~45 hours (30s, 2m, 10m, 30m, 2h, 6h, 12h, 24h), then dead, with manual retry from the delivery log. An endpoint failing continuously for three days is auto-disabled and the merchant is notified.
  • Payloads are snapshots taken inside the transaction that produced them, built by the same serializers as the REST responses, and kept — with their delivery logs — for 30 days.

2026-08-11 — API usage in the dashboard

Every API key now reports its own traffic under Settings → Developer: requests, errors, rate-limited responses and average latency, per key per UTC day, kept for 13 months.

Nothing changes on the API surface — no new endpoint, no new header, no cost to your requests. It is there so a merchant can see which integration is making which calls, and so "the API is slow" can be answered with numbers.

2026-08-11 — Public read API launch

The first release of the Salaf Commerce API. Everything below is new.

Authentication

  • API keys. salaf_sk_ followed by 40 characters, presented as Authorization: Bearer. Created under Settings → Developer; the secret is shown exactly once and stored only as a SHA-256 hash.
  • Two key types. Store keys read one store; company keys read every live store in the company and are restricted to company owners.
  • Scopes, fixed at creation, with write implying read of the same resource. A key can only be granted scopes the person creating it can back with their own permissions.
  • Rotation with a 24-hour grace window, so a key can be replaced without an outage, plus an immediate option for a leaked key.
  • Immediate revocation — the authentication cache is evicted on revoke rather than left to expire.

Endpoints

All read-only. 16 endpoints across 8 resources:

ResourceEndpoints
ProductsGET /products, GET /products/{id}, GET /variants/{identifier}
CategoriesGET /categories, GET /categories/{id}
BrandsGET /brands, GET /brands/{id}
OrdersGET /orders, GET /orders/{id}
CustomersGET /customers, GET /customers/{id}
InventoryGET /inventory/levels, GET /inventory/movements
LocationsGET /locations, GET /locations/{id}
ChannelsGET /channels

GET /variants/{identifier} accepts an id, a SKU or a barcode — a barcode scanner's output can be looked up directly.

Conventions

  • Cursor pagination on every list: limit (max 100, default 25), starting_after / ending_before, and a meta block carrying has_more and next_cursor.
  • updated_after on orders and products, for incremental syncs.
  • expand=customer,items on orders.
  • Money as strings with two decimals ("1250.00"), so decimal precision survives JSON.
  • ISO-8601 UTC timestamps, always with the Z.
  • snake_case field names throughout.
  • A single error shape — one error object with code, message, optional fields, and request_id.
  • Request ids on every request, returned as X-Request-Id. A caller's own X-Request-Id is honoured.

Limits

  • 120 reads and 30 writes per minute per key by default, raised by plan.
  • X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset on every response; 429 carries Retry-After.

Known limitations at launch

Stated plainly so you can plan around them rather than discover them. Several were lifted the same day — see the write and webhook entries above; the list is kept as it was written.

  • The base path is /ext/v1, not /v1. The shorter alias is written but not yet deployed. When it goes live it will be announced here and both paths will serve the same routes.
  • No write endpoints. Creating orders, adjusting stock and recording payments are built but unreleased.
  • No webhooks. Poll with updated_after in the meantime.
  • updated_after is missing on customers, categories, brands, inventory, locations and channels. Re-read those lists on a schedule.
  • payments:read is grantable but has no endpoint yet. A key can hold the scope; nothing currently accepts it.
  • webhooks:manage is declared but not grantable.
  • No sandbox or test mode. There is no salaf_sk_test_ prefix; every key acts on live data.
  • No SDKs. The four sample languages in these docs are the support level for v1.
  • Locations ignore store_id. They belong to the company and are shared across its stores.
  • Only created_at is sortable. A cursor over a mutable column cannot page reliably — see Pagination.