Authentication and scopes
One credential, sent one way. A key belongs to one store, carries a fixed set of scopes, and is named in the history of everything it changes.
Sending the key
Authorization: Bearer srk2_your_key_hereThat is the only accepted form. There is no query parameter, no basic auth, no cookie, and no signed-request scheme. The API lives on its own hostname, api.stockroom.app, with no session and no CSRF: a key is the whole of the credential.
The key identifies the store too, so there is no store id in the URL. One key, one store - a key can never read or write another store's data, and a request for an id that belongs to a different store is a 404, never a 403.
Scopes
The merchant picks scopes when creating the key. Ask for the narrowest set that does your job: the store sees the list on the key, and a warehouse scanner that can also archive suppliers is a question you will have to answer.
| Scope | Lets the key |
|---|---|
read | Read everything the API exposes: purchase orders, suppliers, adjustments, history and the catalog mirror. Always present - every key can read, and asking for a write scope adds it automatically. |
write:purchase_orders | Create and edit purchase orders, send them, mark them ordered or on the way, record and void receipts, record payments, close, reopen, cancel what is left, archive, duplicate, delete a draft. |
write:suppliers | Create, edit, archive and merge suppliers, and write the per-supplier catalog. |
write:inventory | Create, edit, apply and cancel stock adjustments. |
write:manufacturing | Reserved for builds and recipes, which are not in the API yet. A key can hold it today so that nothing has to change when they land. |
webhooks | List, create, edit and delete webhook subscriptions, send tests, and redeliver. |
A call outside the key's scopes is refused before anything happens, with the scope it needed in a header:
HTTP/1.1 403 Forbidden
X-Required-Scope: write:purchase_orders
{
"error": {
"code": "insufficient_scope",
"message": "This API key does not have the `write:purchase_orders` scope."
}
}Scopes cannot be edited after a key exists. To widen or narrow what an integration can do, the merchant creates a new key and revokes the old one - which is also the audit trail of when that happened.
Expiry and revocation
A key expires never, in 90 days or in 1 year, chosen at creation. GET /v2/whoamireturns key.expires_at, so a long-running integration can warn someone before the date rather than stopping on it. An expired or revoked key gets401 unauthorized - the same answer as a key that never existed, on purpose.
Attribution: your key is named in the history
Every write is recorded against the key that made it. A purchase order's history showsvia API - ERP sync, using the key's name, and so does an adjustment's. The merchant can tell your integration's receive from a person's, and from another integration's.
That is the practical reason to name keys after systems rather than people, and to use one key per integration. It also means a key's name is merchant-visible text - Warehouse scanner, not test key 3.
Every refusal, in one place
| Status | Code | What happened |
|---|---|---|
401 | unauthorized | No key, a malformed header, or a key that is wrong, revoked or expired. |
402 | feature_not_enabled | The store does not have API access. Nothing you can fix in code - the merchant asks for it under Settings, then API. |
403 | insufficient_scope | The key is good and lacks the scope this route needs. X-Required-Scope names it. |
404 | not_found | No such record in this store. Includes a record that exists in another store. |
429 | rate_limited | Over a rate limit. Retry-After says when to come back. |
Conventions covers the rest of the error envelope, including validation failures and the status-machine refusals a write can get.