diff --git a/app/models/token.rb b/app/models/token.rb
index b98c6dbf3..17ad03abd 100644
--- a/app/models/token.rb
+++ b/app/models/token.rb
@@ -125,6 +125,12 @@ class Token < ApplicationRecord
     token
   end
 
+  def self.touch_api_token(key)
+    where(action: 'api', value: key)
+      .where("last_used_on IS NULL OR last_used_on <= ?", 1.minute.ago)
+      .update_all(last_used_on: Time.now.utc)
+  end
+
   def self.generate_token_value
     Redmine::Utils.random_hex(20)
   end
diff --git a/app/models/user.rb b/app/models/user.rb
index f2db67c69..4926e592f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -568,7 +568,9 @@ class User < Principal
   end
 
   def self.find_by_api_key(key)
-    Token.find_active_user('api', key)
+    user = Token.find_active_user('api', key)
+    Token.touch_api_token(key) if user
+    user
   end
 
   # Makes find_by_mail case-insensitive
diff --git a/app/views/my/_sidebar.html.erb b/app/views/my/_sidebar.html.erb
index 82a22cef2..a8792edca 100644
--- a/app/views/my/_sidebar.html.erb
+++ b/app/views/my/_sidebar.html.erb
@@ -38,9 +38,14 @@
   <%= javascript_tag("$('#api-access-key').hide();") %>
   <p>
   <% if @user.api_token %>
-  <%= l(:label_api_access_key_created_on, distance_of_time_in_words(Time.now, @user.api_token.created_on)) %>
+    <%= l(:label_api_access_key_created_on, distance_of_time_in_words(Time.now, @user.api_token.created_on)) %><br />
+    <% if @user.api_token.last_used_on %>
+      <%= l(:label_api_access_key_last_used_on, distance_of_time_in_words(Time.now, @user.api_token.last_used_on)) %>
+    <% else %>
+      <%= l(:label_api_access_key_never_used) %>
+    <% end %>
   <% else %>
-  <%= l(:label_missing_api_access_key) %>
+    <%= l(:label_missing_api_access_key) %>
   <% end %>
   (<%= link_to l(:button_reset), my_api_key_path, :method => :post %>)
   </p>
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 2b3a6042a..69f9ef211 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -434,6 +434,8 @@ de:
   label_any_issues_not_in_project: irgendein Ticket nicht im Projekt
   label_api_access_key: API-Zugriffsschlüssel
   label_api_access_key_created_on: Der API-Zugriffsschlüssel wurde vor %{value} erstellt
+  label_api_access_key_last_used_on: "Zuletzt verwendet: vor %{value}"
+  label_api_access_key_never_used: Nie verwendet
   label_applied_status: Zugewiesener Status
   label_ascending: Aufsteigend
   label_ask: Nachfragen
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 90e67bfd8..b97471e90 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1035,6 +1035,8 @@ en:
   label_api_access_key: API access key
   label_missing_api_access_key: Missing an API access key
   label_api_access_key_created_on: "API access key created %{value} ago"
+  label_api_access_key_last_used_on: "Last used: %{value} ago"
+  label_api_access_key_never_used: Never used
   label_profile: Profile
   label_subtask: Subtask
   label_subtask_plural: Subtasks
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 014a47072..74df46e58 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -921,6 +921,8 @@ fr:
   label_api_access_key: Clé d'accès API
   label_missing_api_access_key: Clé d'accès API manquante
   label_api_access_key_created_on: Clé d'accès API créée il y a %{value}
+  label_api_access_key_last_used_on: "Dernier usage : il y a %{value}"
+  label_api_access_key_never_used: Jamais utilisée
   label_profile: Profil
   label_subtask_plural: Sous-tâches
   label_project_copy_notifications: Envoyer les notifications durant la copie du projet
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 51fc2c164..4cfa02716 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -806,6 +806,8 @@ ja:
   label_api_access_key: APIアクセスキー
   label_missing_api_access_key: APIアクセスキーが見つかりません
   label_api_access_key_created_on: "APIアクセスキーは%{value}前に作成されました"
+  label_api_access_key_last_used_on: "最終使用：%{value}前"
+  label_api_access_key_never_used: 未使用
   label_subtask_plural: 子チケット
   label_project_copy_notifications: コピーしたチケットのメール通知を送信する
   label_principal_search: "ユーザーまたはグループの検索:"
diff --git a/test/integration/api_test/authentication_test.rb b/test/integration/api_test/authentication_test.rb
index 4145fb969..ed6eeb1ba 100644
--- a/test/integration/api_test/authentication_test.rb
+++ b/test/integration/api_test/authentication_test.rb
@@ -161,4 +161,39 @@ class Redmine::ApiTest::AuthenticationTest < Redmine::ApiTest::Base
     assert_response :success
     assert_select 'h2', :text => "#{user.initials} #{user.name}"
   end
+
+  def test_api_key_usage_via_header_should_update_last_used_on
+    user = User.generate!
+    token = Token.create!(:user => user, :action => 'api')
+    assert_nil token.last_used_on
+    get '/users/current.xml', :headers => {'X-Redmine-API-Key' => token.value}
+    assert_response :ok
+    assert_not_nil token.reload.last_used_on
+  end
+
+  def test_api_key_usage_via_parameter_should_update_last_used_on
+    user = User.generate!
+    token = Token.create!(:user => user, :action => 'api')
+    assert_nil token.last_used_on
+    get "/users/current.xml?key=#{token.value}"
+    assert_response :ok
+    assert_not_nil token.reload.last_used_on
+  end
+
+  def test_api_key_usage_via_basic_auth_should_update_last_used_on
+    user = User.generate!
+    token = Token.create!(:user => user, :action => 'api')
+    assert_nil token.last_used_on
+    get '/users/current.xml', :headers => credentials(token.value, 'X')
+    assert_response :ok
+    assert_not_nil token.reload.last_used_on
+  end
+
+  def test_failed_api_auth_should_not_update_last_used_on
+    user = User.generate!
+    token = Token.create!(:user => user, :action => 'api')
+    get '/users/current.xml', :headers => {'X-Redmine-API-Key' => 'wrong_key'}
+    assert_response :unauthorized
+    assert_nil token.reload.last_used_on
+  end
 end
diff --git a/test/unit/token_test.rb b/test/unit/token_test.rb
index d40b3a2a1..498e23c6f 100644
--- a/test/unit/token_test.rb
+++ b/test/unit/token_test.rb
@@ -137,4 +137,31 @@ class TokenTest < ActiveSupport::TestCase
     token = Token.create!(:user_id => 999, :action => 'api', :created_on => 2.days.ago)
     assert_nil Token.find_token('api', token.value, 1)
   end
+
+  def test_touch_api_token_should_update_last_used_on_when_never_used
+    token = Token.create!(:user_id => 1, :action => 'api')
+    assert_nil token.last_used_on
+    Token.touch_api_token(token.value)
+    assert_not_nil token.reload.last_used_on
+  end
+
+  def test_touch_api_token_should_update_last_used_on_when_older_than_one_minute
+    token = Token.create!(:user_id => 1, :action => 'api', :last_used_on => 2.minutes.ago)
+    last_used = token.last_used_on
+    Token.touch_api_token(token.value)
+    assert token.reload.last_used_on > last_used
+  end
+
+  def test_touch_api_token_should_not_update_last_used_on_within_one_minute
+    token = Token.create!(:user_id => 1, :action => 'api', :last_used_on => 1.second.ago)
+    last_used = token.reload.last_used_on
+    Token.touch_api_token(token.value)
+    assert_equal last_used.to_i, token.reload.last_used_on.to_i
+  end
+
+  def test_touch_api_token_should_not_affect_other_action_tokens
+    token = Token.create!(:user_id => 1, :action => 'feeds')
+    Token.touch_api_token(token.value)
+    assert_nil token.reload.last_used_on
+  end
 end
diff --git a/db/migrate/20260409000000_add_last_used_on_to_tokens.rb b/db/migrate/20260409000000_add_last_used_on_to_tokens.rb
new file mode 100644
index 000000000..d42cb1563
--- /dev/null
+++ b/db/migrate/20260409000000_add_last_used_on_to_tokens.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddLastUsedOnToTokens < ActiveRecord::Migration[8.1]
+  def change
+    add_column :tokens, :last_used_on, :datetime
+  end
+end
