{"record":{"id":"471584250126d337","repo":"BookStackApp/BookStack","slug":"errors-api-user-token-not-found","errorCode":null,"errorMessage":"errors.api_user_token_not_found","messagePattern":"errors\\.api_user_token_not_found","errorType":"exception","errorClass":"ApiAuthException","httpStatus":null,"severity":"error","filePath":"app/Api/ApiTokenGuard.php","lineNumber":123,"sourceCode":"        if (empty($authToken)) {\n            throw new ApiAuthException(trans('errors.api_no_authorization_found'));\n        }\n\n        if (!str_contains($authToken, ':') || !str_starts_with($authToken, 'Token ')) {\n            throw new ApiAuthException(trans('errors.api_bad_authorization_format'));\n        }\n    }\n\n    /**\n     * Validate the given secret against the given token and ensure the token\n     * currently has access to the instance API.\n     *\n     * @throws ApiAuthException\n     */\n    protected function validateToken(?ApiToken $token, string $secret): void\n    {\n        if ($token === null) {\n            throw new ApiAuthException(trans('errors.api_user_token_not_found'));\n        }\n\n        if (!Hash::check($secret, $token->secret)) {\n            throw new ApiAuthException(trans('errors.api_incorrect_token_secret'));\n        }\n\n        $now = Carbon::now();\n        if ($token->expires_at <= $now) {\n            throw new ApiAuthException(trans('errors.api_user_token_expired'), 403);\n        }\n\n        if (!$token->user->can(Permission::AccessApi)) {\n            throw new ApiAuthException(trans('errors.api_user_no_api_permission'), 403);\n        }\n    }\n\n    /**\n     * {@inheritdoc}","sourceCodeStart":105,"sourceCodeEnd":141,"githubUrl":"https://github.com/BookStackApp/BookStack/blob/18f8469a1c72f8cc8497e9372635e6dea5028071/app/Api/ApiTokenGuard.php#L105-L141","documentation":"ApiTokenGuard::validateToken throws ApiAuthException('errors.api_user_token_not_found') when no ApiToken record matches the id parsed from the Authorization header. The guard looks up the token by its public id portion; null means no such token exists in the database. This happens before the secret is ever checked.","triggerScenarios":"Using a token id that was deleted from the user's API tokens; typos in the id portion of 'id:secret'; pointing a client at a different BookStack instance than the one that issued the token; database restored/migrated without the api_tokens rows.","commonSituations":"Rotating credentials and deleting the old token while a cron job still uses it; copying the secret but mistyping the id; environment mismatch between staging and production tokens.","solutions":["Check the token id in the Authorization header matches an existing token under the user's profile > API Tokens","Recreate the token in the BookStack UI and update the client with the new '<id>:<secret>' pair","Confirm the client targets the correct BookStack instance/environment","If tokens were lost via DB restore, re-issue all tokens for affected integrations"],"exampleFix":"// before\n'Authorization' => 'Token 999:abc' // id 999 deleted\n// after\n'Authorization' => 'Token 12:Xy9AbCdeFgHiJkLmNoPqRsTuVwXyZ012' // id re-issued via UI","handlingStrategy":"try-catch","validationCode":"// Keep token id/secret together in config and sanity-check both are set and non-empty\nif (empty($config['id']) || empty($config['secret'])) {\n    throw new RuntimeException('BookStack API token id and secret are both required');\n}","typeGuard":"function hasTokenPair(array $config): bool {\n    return isset($config['id'], $config['secret']) && is_string($config['id']) && $config['id'] !== '';\n}","tryCatchPattern":"try {\n    $res = $client->get($url, ['headers' => ['Authorization' => \"Token {$id}:{$secret}\"]]);\n} catch (ClientException $e) {\n    if ($e->getResponse()->getStatusCode() === 401) {\n        // token not found or bad format: alert and stop, do not blind-retry\n    }\n    throw $e;\n}","preventionTips":["Re-issue tokens whenever the target environment changes","Never delete tokens without first deprecating the integrations using them","Store id and secret as one versioned credential pair in your secrets manager","Log the token id (never the secret) in client diagnostics"],"tags":["api","authentication","bookstack"],"backgroundTag":"api-token-not-found","analyzedSha":"18f8469a1c72f8cc8497e9372635e6dea5028071","analyzedAt":"2026-09-02T19:49:33.068Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}