Skip to content
  1. Extras
  2. mxHeadless
  3. Authentication
  4. Scopes and ACL

Authorization

After authentication, mxHeadless checks whether the action is allowed in four steps:

  1. Route permission: public or requires identity
  2. Key or token scope: string like {object}.{action}
  3. MODX ACL: context, resource view, view_unpublished
  4. Field policy: hidden and protected fields

Missing scope on API key or OAuth → 403 scope_denied.

How scopes are checked

IdentityCheck
API key (mxh_*)Key scope list. * grants all actions
OAuth (mxt_*)Token scopes (intersection with client scopes)
SessionmodX->hasPermission() with the same string (resources.read, etc.)
AnonymousPublic GET only. Scopes do not apply

For integrations an API key is usually enough. Session fits mgr or same-origin UI with CSRF.

Core scopes (fixed routes)

ScopeRoutes
resources.readGET /resources, GET /resources/{id}, GET /pages/{uri}
resources.createPOST /resources
resources.updatePUT / PATCH /resources/{id}
resources.deleteDELETE /resources/{id}
contexts.readGET /contexts, GET /contexts/{key}, GET /contexts/{key}/settings
chunks.readGET /chunks, GET /chunks/{id}
templates.readGET /templates, GET /templates/{id}
snippets.readGET /snippets, GET /snippets/{id}
tvs.readGET /tvs, GET /tvs/{id}
categories.readGET /categories, GET /categories/{id}
content_types.readGET /content_types, GET /content_types/{id}
preview?preview=true without session view_unpublished. Also used for include_deleted checks
*All scopes (keys and tokens only)

Meta routes (/, /health, /schema, /docs, /meta/*) and POST /auth/token do not require a scope.

Scopes for /objects/{name}

Pattern from code: {name}.{action}, where {name} is the registry name, not a PHP class and not an objects. prefix.

ScopeMethodPath
{name}.readGET/objects/{name}, /objects/{name}/{id}
{name}.createPOST/objects/{name}
{name}.updatePUT, PATCH/objects/{name}/{id}
{name}.deleteDELETE/objects/{name}/{id}

Examples after MiniShop3 objects are registered:

ScopeMeaning
products.readProduct catalog
categories.readCategories
orders.readOrders (usually not public, plus ACL)
orders.updateOrder update when the object is writable

Registered names: GET /schema or GET /meta/endpoints on a live site.

Example scope sets for a key

Public frontends (read-only content) often work without a key.

CI / preview:

text
resources.read,preview,chunks.read,templates.read

MS3 catalog + CMS:

text
resources.read,products.read,categories.read

Admin API (narrow, no *):

text
resources.read,resources.create,resources.update,orders.read

Create a key: API keys. OAuth: OAuth.

Public vs protected

Anonymous callers can read discovery, health, schema, docs, meta, GET /resources, and GET /pages/{uri} within ACL for published resources.

Elements, contexts, write operations, and /objects/* require credentials.

Context

Bootstrap: mxheadless_context (default web) sets the MODX context when the gateway or api.php initializes. Value mgr is ignored.

Per request: header X-Context or query ?context=. Value must be in mxheadless_allowed_contexts (default web,mgr). Otherwise 422 Invalid context.

Mutations by id find the row across contexts, then check context.{key} / context_{key} access. Writing context_key to an unknown or unloadable context returns 422, not 500.

Fields

Hidden fields never appear in JSON. Protected fields require a separate permission in the definition. Requesting fields= for an unknown or forbidden field → 422.

Preview and deleted

QueryWho
preview=trueSession with view_unpublished or scope preview
include_deleted=1Not for anonymous. Needs preview, resources.update, resources.delete, or matching MODX permissions

See also