Skip to content

Scopes ​

A scope is a resource and a verb: purchase_orders:read, sell_out:write. A key holds a list of them, chosen at mint, and reaches exactly what they name.

The reach matrix ​

The New key drawer shows one row per resource and three states in each: None, Read, Read and write. That is the whole grammar. A resource with no public write offers two states.

ResourceScopesOpensSold with
Productsproducts:read19 operationsEvery plan
Product datapim:read2 operationsThe Product Data add-on
Stockinventory:read5 operationsEvery plan
Supplierssuppliers:readsuppliers:write23 operationsEvery plan
Purchase orderspurchase_orders:readpurchase_orders:write26 operationsEvery plan
Salessales:read15 operationsEvery plan
Ordersorders:readorders:write18 operationsEvery plan
Returnsreturns:readreturns:write7 operationsEvery plan
Stocktakesstocktakes:readstocktakes:write9 operationsEvery plan
Sandboxsandbox:readsandbox:write1 operationEvery plan
Movementsmovements:read1 operationEvery plan
Accountsaccounts:read7 operationsEssentials+
Order bookorder_book:readorder_book:write10 operationsEssentials+
Sell-outsell_out:readsell_out:write6 operationsEssentials+
Planningplanning:read7 operationsPro
Cashcash:read1 operationPro
Reportsreports:read3 operationsPro
Metricsmetrics:read2 operationsEvery plan

Ticking Read and write sends both strings. resource:write includes resource:read at the door, so a key minted with ["purchase_orders:write"] alone can still list the orders it posts.

Opens is how many operations of that resource this train publishes. Every row is a resource a key can be minted for today, and the rows here are exactly what the contract publishes: a scope string that is not on this page is one the mint refuses. The changelog is where a new resource arrives.

Read and write are decided by the verb, with one named exception ​

GET is a read. POST, PATCH, PUT and DELETE are writes. There is no list of write operations to keep in step, which has one consequence worth stating plainly: a mutating operation added to a public resource next year requires :write on the day it lands, because its method is POST. A key holding only :read is never widened by a release.

The exception is a question asked with a body. POST /metrics/query hands the compiler a query and gets rows back: it writes nothing, and it costs metrics:read. A key that had to hold metrics:write to ask a question would carry a string promising writes to a resource that has none. Where you need the cost of an operation, read its own x-tightly-scopes rather than inferring it from the method.

The read-only resources, and why they stay that way ​

  • Products and Product data: the catalogue is written by PIM write-back and by the Shopify sync. Two writers on one catalogue is how a merchant's own edit gets overwritten by a sync nobody triggered.
  • Stock: stock arrives by ETL or by a posted stocktake. A public write here would be a fourth source of truth for one number.
  • Sales: what the channels reported, as they reported it. It arrives by ETL, and a public write would be a second account of one week.
  • Accounts: reads today. The write path mints a trading partner for an unmatched account name, which is a decision a person makes before it is a decision an API makes.
  • Planning: the plan as published and the open to buy that comes out of it. Two writers on one plan is how a planner's own edit disappears under a sync nobody triggered.
  • Movements: the stock ledger. A movement is written by the document that moved the stock, so a public write here would be a way to make on hand disagree with the ledger it is the sum of.
  • Cash and Reports: composed from the plan, the order book and what sold. Nothing on either is typed by hand, so there is nothing for a write to change.
  • Metrics: the dictionary, and the compiler that answers questions from it. A query is a question, not a change, which is why asking one costs metrics:read.

What no key can reach ​

Absence is the default: an operation is public because the registry names it, never because of anything about the operation itself. Refused by construction rather than by list:

  • Identity and commerce. Sign-in, users, accounts, organisations, billing, notifications, files.
  • Staff operations. Every admin prefix, and anything gated above read-and-write.
  • Connection plumbing. Connections, mapping, integrations, the join desk.
  • Copilot and analytics. Ask, chart generation.
  • Inbound callbacks. The endpoints other systems post to.
  • Composed page payloads. The dashboard, the basket, Home, Inbox. Their shape follows a page's design, so they would break every time a page moved.
  • Declaring, amending, approving, closing or retiring a commitment. These are a person's act.

Asking for one of those returns 403 not_public, whatever it is and whoever asked. One answer covers a staff operation, an unmapped operation and an operation gated above what a key can be, because saying which is which maps the surface for somebody who should not have it.

A refusal names the word, not the string ​

json
{
  "code": "scope_missing",
  "message": {
    "code": "scope_missing",
    "desc": "This key cannot read Order book.",
    "doc_url": "https://docs.tightly.io/api/guides/errors#scope_missing"
  }
}

The sentence uses the resource's word, because that is what somebody ticked on the reach matrix and what the page in this reference is called. The scope string is something a developer meets once, in a mint request body.

Reading the scopes a key needs ​

Every operation in the reference states its scopes on the operation itself, and each resource page here lists them per operation. In the spec they are the x-tightly-scopes extension, so a generated client can carry them too:

bash
curl -s https://docs.tightly.io/openapi/2026-11.json \
  | jq -r '.paths | to_entries[] | .key as $p | .value | to_entries[]
           | "\(.key|ascii_upcase) \($p)\t\(.value["x-tightly-scopes"]|join(" "))"'

Minting a key by API ​

The Developer page is the door most people use. The same thing over HTTP, for an admin's own session:

bash
curl -X POST "https://api.app.tightly.io/api/v1/developer/keys" \
  -H "Authorization: Bearer <a user token>" \
  -H "X-Organization-ID: <your organisation>" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "Warehouse sync",
        "scopes": ["inventory:read", "purchase_orders:write"],
        "expires_in_days": 365
      }'

GET /api/v1/developer/scopes returns the resources, their words and their plan gates, which is what the reach matrix is drawn from. Minting a key is an admin act on a user session, never something one key can do for another: a key cannot mint a key.

Tightly API, version 2026-11.