BookStackApp/BookStack · error · ApiAuthException
errors.api_user_no_api_permission
Error message
errors.api_user_no_api_permission
What it means
ApiTokenGuard::validateToken throws ApiAuthException('errors.api_user_no_api_permission') with status 403 when $token->user->can(Permission::AccessApi) is false. The token itself is valid (exists, secret matches, not expired), but the owning user account lacks the 'Access the API' system permission, so all its API tokens are refused.
Solutions
- In BookStack admin, grant the user (or its role) the 'Access the API' permission
- If the user account is intentionally restricted, issue a token under a dedicated integration user with API access and minimal role permissions
- Audit role permission changes when API 403s start appearing
- Keep a dedicated service account for integrations so human permission changes don't break them
Example fix
// before // user 'report-bot' role has no 'Access the API' permission -> all its tokens 403 // after // Admin UI: Edit role 'bots' -> check 'Access the API' -> save; retry the request
Defensive patterns
Strategy: try-catch
Validate before calling
// Ensure the service user has API access before deploying the integration
// Check in admin: the user's role has 'Access the API' permission enabled
// Optionally verify with a cheap authenticated call at startup:
$probe = $client->get('/api/books', ['headers' => ['Authorization' => "Token {$id}:{$secret}"]]);
if ($probe->getStatusCode() !== 200) { throw new RuntimeException('API user lacks AccessApi permission'); } Type guard
function apiProbeSucceeds(array $headers): bool {
$code = (int) (probeBookstack('/api/books', $headers) ?? 500);
return $code === 200;
} Try / catch
try {
$res = $client->get($url, ['headers' => ['Authorization' => "Token {$id}:{$secret}"]]);
} catch (ClientException $e) {
if ($e->getResponse()->getStatusCode() === 403) {
// ask an admin to re-enable 'Access the API' for the token's user/role
}
throw $e;
} Prevention
- Use a dedicated service account whose role never loses API permission
- Re-check role permissions after any BookStack upgrade or role refactor
- Include the AccessApi permission in deployment checklists for integrations
- Separate human accounts from integration accounts to avoid offboard side effects
When it happens
Trigger: An admin revoked or never granted the 'Access the API' permission from the user's role/profile; the token owner's role was changed to one without API access; API access was disabled for a whole role during a security lockdown.
Common situations: Role refactors that silently drop API permission; offboarding processes that strip permissions while integrations still run under that user; new tokens created for a read-only role that lacks AccessApi.
Related errors
- User does not have permission to create a chapter within…
- User does not have permission to create a page within the…
- errors.api_bad_authorization_format
- errors.api_cookie_auth_only_get
- errors.api_incorrect_token_secret
AI-assisted analysis of BookStackApp/BookStack@18f8469a1c (2026-09-02).
Data as JSON: /api/errors/1dad47e94b6e92fa.
Report an issue: GitHub.
Appendix: source
Thrown at app/Api/ApiTokenGuard.php:136
* @throws ApiAuthException
*/
protected function validateToken(?ApiToken $token, string $secret): void
{
if ($token === null) {
throw new ApiAuthException(trans('errors.api_user_token_not_found'));
}
if (!Hash::check($secret, $token->secret)) {
throw new ApiAuthException(trans('errors.api_incorrect_token_secret'));
}
$now = Carbon::now();
if ($token->expires_at <= $now) {
throw new ApiAuthException(trans('errors.api_user_token_expired'), 403);
}
if (!$token->user->can(Permission::AccessApi)) {
throw new ApiAuthException(trans('errors.api_user_no_api_permission'), 403);
}
}
/**
* {@inheritdoc}
*/
public function validate(array $credentials = []): bool
{
if (empty($credentials['id']) || empty($credentials['secret'])) {
return false;
}
$token = ApiToken::query()
->where('token_id', '=', $credentials['id'])
->with(['user'])->first();
if ($token === null) {
return false;View on GitHub (pinned to 18f8469a1c)