403 Forbidden
Client Error
The server understood the request and identified the caller, but refuses to authorize the action. Unlike 401, presenting different or refreshed credentials will not help. The identity is established and explicitly denied access. 403 is the right answer for permission failures, plan-gate violations, IP allow-list rejections, and CORS denials. Some APIs use 404 instead of 403 to avoid leaking the existence of a protected resource; this is a deliberate security trade-off rather than a misuse.
When does this happen?
Return 403 when the caller is authenticated but lacks the rights needed for the action. Typical triggers include a member-role user attempting an admin action, a free-plan account hitting a paid-only endpoint, an IP outside a corporate allow-list trying to reach an internal API, or a CORS preflight that rejects the request's Origin. The response body should describe the policy that denied the request without leaking unrelated information. For example, "plan upgrade required" or "role admin required" rather than dumping the entire ACL. 403 is the standard answer for write-protection on read-only resources (a viewer trying to delete a record). Be careful with org-scoped APIs: returning 403 instead of 404 for a resource that exists in another organization leaks the existence of that resource. Most enterprise APIs return 404 in that case to avoid the leak.
Common causes
- Authenticated user has the wrong role. E.g., a viewer attempting an admin-only delete.
- Plan-gate failure. The endpoint requires a paid plan and the caller is on free tier.
- IP-based allow-list block. The request came from outside a permitted range.
- CORS rejection. The browser's preflight failed because Origin is not in the server's allow-list.
- Read-only file or resource. The server explicitly forbids writes to this path.
- Geo-restriction. Content is blocked in the caller's country due to licensing or compliance.
- Account suspended, banned, or marked read-only by an admin.
- Endpoint disabled for non-internal traffic. For example /admin/* paths that only accept VPN-originating IPs.
How to fix it
- Verify the authenticated user's role matches the endpoint's required role. Most APIs document this in the endpoint description.
- Upgrade the plan if the error message indicates a plan gate. The response body should say which plan unlocks the action.
- Check whether your IP is on the server's allow-list. Common for corporate or admin APIs.
- For CORS errors, inspect the OPTIONS preflight response in DevTools. The failure mode is almost always a missing or wrong Access-Control-Allow-Origin.
- Contact the resource owner if the policy looks wrong. 403 may be a deliberate access control decision, not a bug.
- For org-scoped resources that appear to exist for someone else, do not assume a bug. Many APIs return 403/404 deliberately to avoid leaking existence.
- If 403 is appearing on an action that worked yesterday, check whether the user's account was downgraded, suspended, or had a role changed.
Real-world examples
- Member-role user clicks Delete on a record that only admins can remove.
- Server returns 403 with {"error":"role admin required"}. UI disables the button and surfaces the message.
- Free-plan user calls POST /v1/monitors which requires a paid plan.
- Server returns 403 with {"error":"upgrade_required","upgrade_url":"/billing"}. Frontend redirects the user to the billing page.
- Developer working from a coffee shop tries to access an admin endpoint that only allows office IPs.
- Server returns 403 with {"error":"ip_not_allowed"}. Developer connects to VPN and the request succeeds.
- Browser-based app makes a fetch from app.example.com to api.example.com but api.example.com does not include app.example.com in Access-Control-Allow-Origin.
- Browser preflight returns 403 (or 200 without the right CORS headers) and the actual request never fires. DevTools shows a CORS error in the console.
- User in the EU accesses a US-only video endpoint.
- Server returns 403 with {"error":"geo_restricted"}. Client surfaces a message explaining the content is not available in their region.
- Compromised account is suspended by an admin while the user has an open session.
- Subsequent API calls return 403 with {"error":"account_suspended"}. UI forces a logout and redirects to a support page.
Debugging
- Read the response body. 403s should describe the policy that triggered, e.g. role, plan, or IP restriction.
- Run curl -v with the same credentials and confirm whether the same 403 happens outside the browser (rules out CORS).
- For CORS, capture the OPTIONS preflight in DevTools Network tab and check Access-Control-Allow-Origin matches your origin exactly (no wildcard for credentialed requests).
- Check your IP with a request to a what-is-my-ip service and confirm it matches what the server allows.
- If 403 is intermittent, inspect whether a load balancer or WAF is making the decision. Cloudflare's firewall logs often show the rule that triggered.
How it differs from related codes
HTTP 401
401 means authentication failed. 403 means authentication succeeded but authorization failed. The user can fix 401 by providing credentials; 403 requires a permission change.
HTTP 404
404 means the resource is not found. Many APIs intentionally return 404 instead of 403 when a resource exists but belongs to a different organization, to avoid leaking existence.
HTTP 451
451 is a specialized 403 for legal restrictions. Content blocked by court order or regulation. Use 403 for ordinary permission denials and 451 only when the cause is a legal demand.
Related status codes
See HTTP 403 in your redirect chains?