BookStackApp/BookStack · error · ApiAuthException

errors.api_user_token_not_found

Error message

errors.api_user_token_not_found

What it means

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.

Solutions

  1. Check the token id in the Authorization header matches an existing token under the user's profile > API Tokens
  2. Recreate the token in the BookStack UI and update the client with the new '<id>:<secret>' pair
  3. Confirm the client targets the correct BookStack instance/environment
  4. If tokens were lost via DB restore, re-issue all tokens for affected integrations

Example fix

// before
'Authorization' => 'Token 999:abc' // id 999 deleted
// after
'Authorization' => 'Token 12:Xy9AbCdeFgHiJkLmNoPqRsTuVwXyZ012' // id re-issued via UI
Defensive patterns

Strategy: try-catch

Validate before calling

// Keep token id/secret together in config and sanity-check both are set and non-empty
if (empty($config['id']) || empty($config['secret'])) {
    throw new RuntimeException('BookStack API token id and secret are both required');
}

Type guard

function hasTokenPair(array $config): bool {
    return isset($config['id'], $config['secret']) && is_string($config['id']) && $config['id'] !== '';
}

Try / catch

try {
    $res = $client->get($url, ['headers' => ['Authorization' => "Token {$id}:{$secret}"]]);
} catch (ClientException $e) {
    if ($e->getResponse()->getStatusCode() === 401) {
        // token not found or bad format: alert and stop, do not blind-retry
    }
    throw $e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of BookStackApp/BookStack@18f8469a1c (2026-09-02). Data as JSON: /api/errors/471584250126d337. Report an issue: GitHub.

Appendix: source

Thrown at app/Api/ApiTokenGuard.php:123

        if (empty($authToken)) {
            throw new ApiAuthException(trans('errors.api_no_authorization_found'));
        }

        if (!str_contains($authToken, ':') || !str_starts_with($authToken, 'Token ')) {
            throw new ApiAuthException(trans('errors.api_bad_authorization_format'));
        }
    }

    /**
     * Validate the given secret against the given token and ensure the token
     * currently has access to the instance API.
     *
     * @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}

View on GitHub (pinned to 18f8469a1c)