✨ Strapi MCP is now Generally Available - let your agents manage your Strapi content ✨

You created a collection type, added entries, published them, and called the API:

curl https://cms.example.com/api/articles
{
  "data": null,
  "error": {
    "status": 403,
    "name": "ForbiddenError",
    "message": "Forbidden"
  }
}

403 means the request was understood and refused. The route exists, Strapi identified who is asking, and that identity does not have permission to perform this action. This is different from a 401 (no valid credentials at all) and from a 404 (no such route).

Work through these in order.

1. The Public role has no permission on that content type

This resolves the large majority of cases.

Every unauthenticated request to Strapi is executed as the Public role, and that role starts with no permissions on anything you create. A new collection type is invisible to the outside world until you grant access.

Settings → Users & Permissions plugin → Roles → Public

Expand your content type and tick the actions you want to expose:

  • find: the list endpoint, GET /api/articles
  • findOne: the single-entry endpoint, GET /api/articles/:documentId
  • create, update, delete: write actions. Do not enable these for Public.

Click Save. The change applies immediately; you do not need to restart.

Two things to know:

  • find and findOne are separate permissions. Granting only find gives you a working list endpoint and a 403 on every individual entry, which produces a very confusing bug report.
  • Custom controller actions need their own tick. If you added a custom route, its action appears in this list and defaults to off.

2. You are using an API token that lacks the permission

If you are sending Authorization: Bearer <token>, the Public role no longer applies; the token's own permissions do.

Settings → API Tokens

Token types:

TypeAccess
Read-onlyfind and findOne on everything
Full accessEverything, including create, update, delete
CustomExactly the actions you tick

A Custom token silently omits any content type you created after generating it. If your 403 started right after adding a collection type, this is very likely why, so edit the token and grant the new permissions.

Also check the obvious:

  • The header is Authorization: Bearer <token>, with the space and the correct case.
  • The token has not expired. Tokens can be created with a lifespan.
  • You are using the token for the right environment. Tokens are stored in the database, so a staging token does not work against production.
  • The token was copied in full. It is shown exactly once at creation; a truncated paste gives a 401, not a 403, but people frequently conflate the two.

3. You are using a Users & Permissions JWT

If your frontend logs users in via /api/auth/local, requests carry a user JWT and the Authenticated role's permissions apply, a different role from Public, configured on the same screen.

Settings → Users & Permissions plugin → Roles → Authenticated

A common surprise: Public has an endpoint enabled but Authenticated does not, so the API works for logged-out visitors and 403s for logged-in ones.

Do not confuse the two token systems. An API token (Settings → API Tokens) is a server-to-server credential. A user JWT comes from the Users & Permissions login flow. They resolve to different permission sets, and a JWT sent where an API token is expected produces a 403.

4. You are hitting the admin API instead of the content API

Routes under /admin and the Content-Type Builder API require an admin session, not an API token. GET /content-manager/collection-types/api::article.article is an admin route; the public equivalent is GET /api/articles.

If you are scripting against Strapi, use /api/....

5. It is not a 403: you are getting an empty array

A different failure that gets reported as "403-ish": a 200 with "data": [] even though entries exist.

That is Draft & Publish. The REST API returns published documents by default. Entries saved but never published are invisible.

Either publish the entries, or request drafts explicitly:

GET /api/articles?status=draft

Reading drafts requires appropriate permissions, and the Public role should generally not have them.

Coming from Strapi v4: the v4 publicationState=preview parameter was replaced in v5 by status=draft. v4's publicationState=live is now the default behaviour. There is no fallback: status=published returns nothing for a document that has never been published, rather than silently falling back to its draft. If you used publicationState in back-end code via the Entity Service, a codemod handles the change; front-end REST and GraphQL calls have to be updated by hand.

status also applies to writes, and the default catches people out: a POST or PUT publishes immediately unless you pass status=draft. That is the opposite of the Document Service API, which defaults to draft in back-end code.

To query documents by how their draft and published versions relate (never published, modified since last publish, and so on), pair status with publicationFilter:

GET /api/articles?status=draft&publicationFilter=never-published

6. A route-level policy or middleware is rejecting the request

If you added a policy to a route, it runs before the controller and can return 403 on its own:

export default {
  routes: [
    {
      method: 'GET',
      path: '/articles',
      handler: 'article.find',
      config: {
        policies: ['global::is-owner'],
      },
    },
  ],
};

Check src/policies, src/api/*/policies, and the config.policies array on your routes. A policy that throws ForbiddenError produces a response identical to a missing permission.

7. CORS is making it look like a 403

If the failure only happens in the browser and curl works, you are looking at CORS, not permissions. The browser blocks the response and the console message can mention a status you never actually received.

export default [
  'strapi::logger',
  'strapi::errors',
  'strapi::security',
  {
    name: 'strapi::cors',
    config: {
      origin: ['https://www.example.com', 'http://localhost:3000'],
    },
  },
  'strapi::poweredBy',
  'strapi::query',
  'strapi::body',
  'strapi::session',
  'strapi::favicon',
  'strapi::public',
];

Reproduce with curl first. If curl succeeds and the browser does not, it is CORS.

A diagnostic sequence

# 1. Does the route exist at all? 404 = wrong path or the content type has no routes
curl -i https://cms.example.com/api/articles

# 2. Does it work with a full-access token? Yes = a permissions problem, not a code problem
curl -i https://cms.example.com/api/articles \
  -H "Authorization: Bearer $FULL_ACCESS_TOKEN"

# 3. Are there published entries?
curl -s "https://cms.example.com/api/articles?status=draft" \
  -H "Authorization: Bearer $FULL_ACCESS_TOKEN" | jq '.meta.pagination.total'

# 4. What routes does Strapi think it has?
npx strapi routes:list | grep article

strapi routes:list is underused and settles a lot of arguments: it prints every registered route with its method, path, and handler.

Quick reference

SituationWhere to look
No auth headerUsers & Permissions → Roles → Public
Authorization: Bearer <API token>Settings → API Tokens
Logged-in user JWTUsers & Permissions → Roles → Authenticated
find works, findOne 403sTick findOne separately
Custom route 403sTick the custom action; check route policies
200 with empty dataDraft & Publish; publish, or use status=draft
Works in curl, fails in browserCORS
Started after adding a content typeRe-grant permissions on Public / Custom API tokens

One thing not to do

Do not grant find and findOne on every content type to Public as a shortcut. The Public role is the internet. Enable exactly what your frontend needs, and use fields and populate deliberately so you are not exposing internal fields alongside the ones you meant to publish.

Further reading

Theodore Kelechukwu OnyejiakuDevRel and Community | Software Developer | Technical Writer

Theodore is a Technical Writer and a full-stack software developer. He loves writing technical articles, building solutions, and sharing his expertise.

"Cannot destructure property 'client' of 'db.config.connection'" in Strapi
EcosystemIntermediate·6 min read

"Cannot destructure property 'client' of 'db.config.connection'" in Strapi

This error means Strapi loaded no database configuration at all. The file is missing from the build, excluded from the container, or never compiled.

·September 9, 2026
"Cannot send secure cookie over unencrypted connection" in Strapi
EcosystemAdvanced·6 min read

"Cannot send secure cookie over unencrypted connection" in Strapi

Your site is on HTTPS but Strapi thinks the request came over HTTP. The fix is one config key, proxy.koa, plus the proxy headers that make it work.

·September 9, 2026
"Strapi is in production mode" when you are running development mode
EcosystemBeginner·5 min read

"Strapi is in production mode" When you are Running Development Mode

The Content-Type Builder is disabled and Strapi claims production, but your terminal says development. The rule Strapi actually applies, and the fix.

·September 9, 2026