diff --git a/app/services/environments/stop_stale_service.rb b/app/services/environments/stop_stale_service.rb
index b2f1d74365a470db0495c9eea6eb73fd5b30f17d..7b7032cb6708c1b66ab9ed810e5f9773b56ce932 100644
--- a/app/services/environments/stop_stale_service.rb
+++ b/app/services/environments/stop_stale_service.rb
@@ -18,7 +18,7 @@ def execute
         )
       end
 
-      ServiceResponse.success(message: 'Successfully scheduled stale environments to stop')
+      ServiceResponse.success(message: 'Successfully requested stop for all stale environments')
     end
   end
 end
diff --git a/doc/api/environments.md b/doc/api/environments.md
index 89b4bb6a1de5af854c76f441cd39d02a511f3c20..8bf3d3c8bce97c9ead58444335f0e2dfafecdb22 100644
--- a/doc/api/environments.md
+++ b/doc/api/environments.md
@@ -412,3 +412,28 @@ Example response:
   "updated_at": "2019-05-27T18:55:13.252Z"
 }
 ```
+
+## Stop stale environments
+
+Issue stop request to all environments that were last modified or deployed to before a specified date. Excludes protected environments. Returns `200` if stop request was successful and `400` if the before date is invalid. For details of exactly when the environment is stopped, see [Stop an environment](../ci/environments/index.md#stop-an-environment).
+
+```plaintext
+POST /projects/:id/environments/stop_stale
+```
+
+| Attribute | Type           | Required | Description                                                                                                                                                                                    |
+|-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `id`      | integer/string | yes      | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user                                                                                 |
+| `before`  | date           | yes      | Stop environments that have been modified or deployed to before the specified date. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). Valid inputs are between 10 years ago and 1 week ago |
+
+```shell
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/environments/stop_stale?before=10%2F10%2F2021"
+```
+
+Example response:
+
+```json
+{
+  "message": "Successfully requested stop for all stale environments"
+}
+```
diff --git a/doc/ci/environments/index.md b/doc/ci/environments/index.md
index f2a513fdde511f96721752405e70cf1af57e8c63..c93100b355acc7d6f2c5be25334f361b95191fc3 100644
--- a/doc/ci/environments/index.md
+++ b/doc/ci/environments/index.md
@@ -436,6 +436,8 @@ There are multiple ways to clean up [dynamic environments](#create-a-dynamic-env
 - If you do _NOT_ use [merge request pipelines](../pipelines/merge_request_pipelines.md), GitLab stops an environment [when the associated feature branch is deleted](#stop-an-environment-when-a-branch-is-deleted).
 - If you set [an expiry period to an environment](../yaml/index.md#environmentauto_stop_in), GitLab stops an environment [when it's expired](#stop-an-environment-after-a-certain-time-period).
 
+To stop stale environments, you can [use the API](../../api/environments.md#stop-stale-environments).
+
 #### Stop an environment when a branch is deleted
 
 You can configure environments to stop when a branch is deleted.
diff --git a/lib/api/environments.rb b/lib/api/environments.rb
index cc3e41e343fe3199d9a431c16bf37f64d43c8e88..64510a9615afe9470818a977baa17b47e963ad50 100644
--- a/lib/api/environments.rb
+++ b/lib/api/environments.rb
@@ -188,6 +188,35 @@ class Environments < ::API::Base
         present environment, with: Entities::Environment, current_user: current_user
       end
 
+      desc 'Stop stale environments' do
+        detail 'It returns `200` if stale environment check was scheduled successfully'
+        failure [
+          { code: 400, message: 'Bad request' },
+          { code: 401, message: 'Unauthorized' }
+        ]
+        tags %w[environments]
+      end
+      params do
+        requires :before,
+                 type: DateTime,
+                 desc: 'Stop all environments that were last modified or deployed to before this date.'
+      end
+      post ':id/environments/stop_stale' do
+        authorize! :stop_environment, user_project
+
+        bad_request!('Invalid Date') if params[:before] < 10.years.ago || params[:before] > 1.week.ago
+
+        service_response = ::Environments::StopStaleService.new(user_project, current_user, params.slice(:before)).execute
+
+        if service_response.error?
+          status 400
+        else
+          status 200
+        end
+
+        present message: service_response.message
+      end
+
       desc 'Get a specific environment' do
         success Entities::Environment
         failure [
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index 049c45002071e07d9a8618da03116b2a97137e10..6164555ad1917b40f2fa8653ed6c4f1f8333b40d 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -4,12 +4,14 @@
 
 RSpec.describe API::Environments, feature_category: :continuous_delivery do
   let_it_be(:user) { create(:user) }
+  let_it_be(:developer) { create(:user) }
   let_it_be(:non_member) { create(:user) }
   let_it_be(:project) { create(:project, :private, :repository, namespace: user.namespace) }
   let_it_be_with_reload(:environment) { create(:environment, project: project) }
 
   before do
     project.add_maintainer(user)
+    project.add_developer(developer)
   end
 
   describe 'GET /projects/:id/environments', :aggregate_failures do
@@ -182,6 +184,50 @@
     end
   end
 
+  describe 'POST /projects/:id/environments/stop_stale' do
+    context 'as a maintainer' do
+      it 'returns a 200' do
+        post api("/projects/#{project.id}/environments/stop_stale", user), params: { before: 1.week.ago.to_date.to_s }
+
+        expect(response).to have_gitlab_http_status(:ok)
+      end
+
+      it 'returns a 400 for bad input date' do
+        post api("/projects/#{project.id}/environments/stop_stale", user), params: { before: 1.day.ago.to_date.to_s }
+
+        expect(response).to have_gitlab_http_status(:bad_request)
+        expect(json_response['message']).to eq('400 Bad request - Invalid Date')
+      end
+
+      it 'returns a 400 for service error' do
+        expect_next_instance_of(::Environments::StopStaleService) do |service|
+          expect(service).to receive(:execute).and_return(ServiceResponse.error(message: 'Test Error'))
+        end
+
+        post api("/projects/#{project.id}/environments/stop_stale", user), params: { before: 1.week.ago.to_date.to_s }
+
+        expect(response).to have_gitlab_http_status(:bad_request)
+        expect(json_response['message']).to eq('Test Error')
+      end
+    end
+
+    context 'a non member' do
+      it 'rejects the request' do
+        post api("/projects/#{project.id}/environments/stop_stale", non_member), params: { before: 1.week.ago.to_date.to_s }
+
+        expect(response).to have_gitlab_http_status(:not_found)
+      end
+    end
+
+    context 'a developer' do
+      it 'rejects the request' do
+        post api("/projects/#{project.id}/environments/stop_stale", developer), params: { before: 1.week.ago.to_date.to_s }
+
+        expect(response).to have_gitlab_http_status(:forbidden)
+      end
+    end
+  end
+
   describe 'PUT /projects/:id/environments/:environment_id' do
     it 'returns a 200 if name and external_url are changed' do
       url = 'https://mepmep.whatever.ninja'