diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index 07213ca608a956b70ee44e9ede192975f93de559..45d8cd34359b8e1811e97bcf2df9b71f7b364813 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -24,6 +24,10 @@ module Ci
       owner == current_user
     end
 
+    def own!(user)
+      update(owner: user)
+    end
+
     def inactive?
       !active?
     end
diff --git a/changelogs/unreleased/30892-add-api-support-for-pipeline-schedule.yml b/changelogs/unreleased/30892-add-api-support-for-pipeline-schedule.yml
new file mode 100644
index 0000000000000000000000000000000000000000..26ce84697d065b1c1fb0c37a10751f8142b7ea4e
--- /dev/null
+++ b/changelogs/unreleased/30892-add-api-support-for-pipeline-schedule.yml
@@ -0,0 +1,4 @@
+---
+title: Add API support for pipeline schedule
+merge_request: 11307
+author: dosuken123
diff --git a/doc/api/README.md b/doc/api/README.md
index 1b0f6470b13219df05f80baddf466c2646210f5d..45579ccac4ef759663373ad4607dac2e303adac6 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -33,6 +33,7 @@ following locations:
 - [Notification settings](notification_settings.md)
 - [Pipelines](pipelines.md)
 - [Pipeline Triggers](pipeline_triggers.md)
+- [Pipeline Schedules](pipeline_schedules.md)
 - [Projects](projects.md) including setting Webhooks
 - [Project Access Requests](access_requests.md)
 - [Project Members](members.md)
diff --git a/doc/api/pipeline_schedules.md b/doc/api/pipeline_schedules.md
new file mode 100644
index 0000000000000000000000000000000000000000..433654c18cccad71f89c9721fccf73dd656d4734
--- /dev/null
+++ b/doc/api/pipeline_schedules.md
@@ -0,0 +1,273 @@
+# Pipeline schedules
+
+You can read more about [pipeline schedules](../user/project/pipelines/schedules.md).
+
+## Get all pipeline schedules
+
+Get a list of the pipeline schedules of a project.
+
+```
+GET /projects/:id/pipeline_schedules
+```
+
+| Attribute | Type    | required | Description         |
+|-----------|---------|----------|---------------------|
+| `id`      | integer/string | yes      | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `scope`   | string  | no       | The scope of pipeline schedules, one of: `active`, `inactive` |
+
+```sh
+curl --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules"
+```
+
+```json
+[
+    {
+        "id": 13,
+        "description": "Test schedule pipeline",
+        "ref": "master",
+        "cron": "* * * * *",
+        "cron_timezone": "Asia/Tokyo",
+        "next_run_at": "2017-05-19T13:41:00.000Z",
+        "active": true,
+        "created_at": "2017-05-19T13:31:08.849Z",
+        "updated_at": "2017-05-19T13:40:17.727Z",
+        "owner": {
+            "name": "Administrator",
+            "username": "root",
+            "id": 1,
+            "state": "active",
+            "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+            "web_url": "https://gitlab.example.com/root"
+        }
+    }
+]
+```
+
+## Get a single pipeline schedule
+
+Get the pipeline schedule of a project.
+
+```
+GET /projects/:id/pipeline_schedules/:pipeline_schedule_id
+```
+
+| Attribute    | Type    | required | Description              |
+|--------------|---------|----------|--------------------------|
+| `id`         | integer/string | yes      | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user      |
+| `pipeline_schedule_id` | integer | yes      | The pipeline schedule id           |
+
+```sh
+curl --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13"
+```
+
+```json
+{
+    "id": 13,
+    "description": "Test schedule pipeline",
+    "ref": "master",
+    "cron": "* * * * *",
+    "cron_timezone": "Asia/Tokyo",
+    "next_run_at": "2017-05-19T13:41:00.000Z",
+    "active": true,
+    "created_at": "2017-05-19T13:31:08.849Z",
+    "updated_at": "2017-05-19T13:40:17.727Z",
+    "last_pipeline": {
+        "id": 332,
+        "sha": "0e788619d0b5ec17388dffb973ecd505946156db",
+        "ref": "master",
+        "status": "pending"
+    },
+    "owner": {
+        "name": "Administrator",
+        "username": "root",
+        "id": 1,
+        "state": "active",
+        "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+        "web_url": "https://gitlab.example.com/root"
+    }
+}
+```
+
+## Create a new pipeline schedule
+
+Create a new pipeline schedule of a project.
+
+```
+POST /projects/:id/pipeline_schedules
+```
+
+| Attribute     | Type    | required | Description              |
+|---------------|---------|----------|--------------------------|
+| `id`          | integer/string | yes      | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user      |
+| `description` | string  | yes      | The description of pipeline schedule         |
+| `ref` | string  | yes      | The branch/tag name will be triggered         |
+| `cron ` | string  | yes      | The cron (e.g. `0 1 * * *`) ([Cron syntax](https://en.wikipedia.org/wiki/Cron))       |
+| `cron_timezone ` | string  | no      | The timezone supproted by `ActiveSupport::TimeZone` (e.g. `Pacific Time (US & Canada)`) (default: `'UTC'`)     |
+| `active ` | boolean  | no      | The activation of pipeline schedule. If false is set, the pipeline schedule will deactivated initially (default: `true`) |
+
+```sh
+curl --request POST --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" --form description="Build packages" --form ref="master" --form cron="0 1 * * 5" --form cron_timezone="UTC" --form active="true" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules"
+```
+
+```json
+{
+    "id": 14,
+    "description": "Build packages",
+    "ref": "master",
+    "cron": "0 1 * * 5",
+    "cron_timezone": "UTC",
+    "next_run_at": "2017-05-26T01:00:00.000Z",
+    "active": true,
+    "created_at": "2017-05-19T13:43:08.169Z",
+    "updated_at": "2017-05-19T13:43:08.169Z",
+    "last_pipeline": null,
+    "owner": {
+        "name": "Administrator",
+        "username": "root",
+        "id": 1,
+        "state": "active",
+        "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+        "web_url": "https://gitlab.example.com/root"
+    }
+}
+```
+
+## Edit a pipeline schedule
+
+Updates the pipeline schedule  of a project. Once the update is done, it will be rescheduled automatically.
+
+```
+PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id
+```
+
+| Attribute     | Type    | required | Description              |
+|---------------|---------|----------|--------------------------|
+| `id`          | integer/string | yes      | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user      |
+| `pipeline_schedule_id`  | integer | yes      | The pipeline schedule id           |
+| `description` | string  | no      | The description of pipeline schedule         |
+| `ref` | string  | no      | The branch/tag name will be triggered         |
+| `cron ` | string  | no      | The cron (e.g. `0 1 * * *`) ([Cron syntax](https://en.wikipedia.org/wiki/Cron))       |
+| `cron_timezone ` | string  | no      | The timezone supproted by `ActiveSupport::TimeZone` (e.g. `Pacific Time (US & Canada)`) or `TZInfo::Timezone` (e.g. `America/Los_Angeles`)      |
+| `active ` | boolean  | no      | The activation of pipeline schedule. If false is set, the pipeline schedule will deactivated initially. |
+
+```sh
+curl --request PUT --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" --form cron="0 2 * * *" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13"
+```
+
+```json
+{
+    "id": 13,
+    "description": "Test schedule pipeline",
+    "ref": "master",
+    "cron": "0 2 * * *",
+    "cron_timezone": "Asia/Tokyo",
+    "next_run_at": "2017-05-19T17:00:00.000Z",
+    "active": true,
+    "created_at": "2017-05-19T13:31:08.849Z",
+    "updated_at": "2017-05-19T13:44:16.135Z",
+    "last_pipeline": {
+        "id": 332,
+        "sha": "0e788619d0b5ec17388dffb973ecd505946156db",
+        "ref": "master",
+        "status": "pending"
+    },
+    "owner": {
+        "name": "Administrator",
+        "username": "root",
+        "id": 1,
+        "state": "active",
+        "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+        "web_url": "https://gitlab.example.com/root"
+    }
+}
+```
+
+## Take ownership of a pipeline schedule
+
+Update the owner of the pipeline schedule of a project.
+
+```
+POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/take_ownership
+```
+
+| Attribute     | Type    | required | Description              |
+|---------------|---------|----------|--------------------------|
+| `id`          | integer/string | yes      | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user      |
+| `pipeline_schedule_id`  | integer | yes      | The pipeline schedule id           |
+
+```sh
+curl --request POST --header "PRIVATE-TOKEN: hf2CvZXB9w8Uc5pZKpSB" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13/take_ownership"
+```
+
+```json
+{
+    "id": 13,
+    "description": "Test schedule pipeline",
+    "ref": "master",
+    "cron": "0 2 * * *",
+    "cron_timezone": "Asia/Tokyo",
+    "next_run_at": "2017-05-19T17:00:00.000Z",
+    "active": true,
+    "created_at": "2017-05-19T13:31:08.849Z",
+    "updated_at": "2017-05-19T13:46:37.468Z",
+    "last_pipeline": {
+        "id": 332,
+        "sha": "0e788619d0b5ec17388dffb973ecd505946156db",
+        "ref": "master",
+        "status": "pending"
+    },
+    "owner": {
+        "name": "shinya",
+        "username": "maeda",
+        "id": 50,
+        "state": "active",
+        "avatar_url": "http://www.gravatar.com/avatar/8ca0a796a679c292e3a11da50f99e801?s=80&d=identicon",
+        "web_url": "https://gitlab.example.com/maeda"
+    }
+}
+```
+
+## Delete a pipeline schedule
+
+Delete the pipeline schedule of a project.
+
+```
+DELETE /projects/:id/pipeline_schedules/:pipeline_schedule_id
+```
+
+| Attribute      | Type    | required | Description              |
+|----------------|---------|----------|--------------------------|
+| `id`           | integer/string | yes      | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user      |
+| `pipeline_schedule_id`   | integer | yes      | The pipeline schedule id           |
+
+```sh
+curl --request DELETE --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13"
+```
+
+```json
+{
+    "id": 13,
+    "description": "Test schedule pipeline",
+    "ref": "master",
+    "cron": "0 2 * * *",
+    "cron_timezone": "Asia/Tokyo",
+    "next_run_at": "2017-05-19T17:00:00.000Z",
+    "active": true,
+    "created_at": "2017-05-19T13:31:08.849Z",
+    "updated_at": "2017-05-19T13:46:37.468Z",
+    "last_pipeline": {
+        "id": 332,
+        "sha": "0e788619d0b5ec17388dffb973ecd505946156db",
+        "ref": "master",
+        "status": "pending"
+    },
+    "owner": {
+        "name": "shinya",
+        "username": "maeda",
+        "id": 50,
+        "state": "active",
+        "avatar_url": "http://www.gravatar.com/avatar/8ca0a796a679c292e3a11da50f99e801?s=80&d=identicon",
+        "web_url": "https://gitlab.example.com/maeda"
+    }
+}
+```
diff --git a/lib/api/api.rb b/lib/api/api.rb
index ac113c5200d47cf60c675ea3f99767a177115328..bbdd2039f43846f9d4fe9f59490e740022d913b4 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -110,6 +110,7 @@ module API
     mount ::API::Notes
     mount ::API::NotificationSettings
     mount ::API::Pipelines
+    mount ::API::PipelineSchedules
     mount ::API::ProjectHooks
     mount ::API::Projects
     mount ::API::ProjectSnippets
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 8c5e5c917694ebf13bed5406eefa4b10f6772fe3..e10bd230ae273bd27a4723ba561b88f6e3934488 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -686,6 +686,17 @@ module API
       expose :coverage
     end
 
+    class PipelineSchedule < Grape::Entity
+      expose :id
+      expose :description, :ref, :cron, :cron_timezone, :next_run_at, :active
+      expose :created_at, :updated_at
+      expose :owner, using: Entities::UserBasic
+    end
+
+    class PipelineScheduleDetails < PipelineSchedule
+      expose :last_pipeline, using: Entities::PipelineBasic
+    end
+
     class EnvironmentBasic < Grape::Entity
       expose :id, :name, :slug, :external_url
     end
diff --git a/lib/api/pipeline_schedules.rb b/lib/api/pipeline_schedules.rb
new file mode 100644
index 0000000000000000000000000000000000000000..93d89209934e50234a890d95fb11e13e61872b1c
--- /dev/null
+++ b/lib/api/pipeline_schedules.rb
@@ -0,0 +1,131 @@
+module API
+  class PipelineSchedules < Grape::API
+    include PaginationParams
+
+    before { authenticate! }
+
+    params do
+      requires :id, type: String, desc: 'The ID of a project'
+    end
+    resource :projects, requirements: { id: %r{[^/]+} } do
+      desc 'Get all pipeline schedules' do
+        success Entities::PipelineSchedule
+      end
+      params do
+        use :pagination
+        optional :scope,    type: String, values: %w[active inactive],
+                            desc: 'The scope of pipeline schedules'
+      end
+      get ':id/pipeline_schedules' do
+        authorize! :read_pipeline_schedule, user_project
+
+        schedules = PipelineSchedulesFinder.new(user_project).execute(scope: params[:scope])
+          .preload([:owner, :last_pipeline])
+        present paginate(schedules), with: Entities::PipelineSchedule
+      end
+
+      desc 'Get a single pipeline schedule' do
+        success Entities::PipelineScheduleDetails
+      end
+      params do
+        requires :pipeline_schedule_id, type: Integer,  desc: 'The pipeline schedule id'
+      end
+      get ':id/pipeline_schedules/:pipeline_schedule_id' do
+        authorize! :read_pipeline_schedule, user_project
+
+        not_found!('PipelineSchedule') unless pipeline_schedule
+
+        present pipeline_schedule, with: Entities::PipelineScheduleDetails
+      end
+
+      desc 'Create a new pipeline schedule' do
+        success Entities::PipelineScheduleDetails
+      end
+      params do
+        requires :description, type: String, desc: 'The description of pipeline schedule'
+        requires :ref, type: String, desc: 'The branch/tag name will be triggered'
+        requires :cron, type: String, desc: 'The cron'
+        optional :cron_timezone, type: String, default: 'UTC', desc: 'The timezone'
+        optional :active, type: Boolean, default: true, desc: 'The activation of pipeline schedule'
+      end
+      post ':id/pipeline_schedules' do
+        authorize! :create_pipeline_schedule, user_project
+
+        pipeline_schedule = Ci::CreatePipelineScheduleService
+          .new(user_project, current_user, declared_params(include_missing: false))
+          .execute
+
+        if pipeline_schedule.persisted?
+          present pipeline_schedule, with: Entities::PipelineScheduleDetails
+        else
+          render_validation_error!(pipeline_schedule)
+        end
+      end
+
+      desc 'Edit a pipeline schedule' do
+        success Entities::PipelineScheduleDetails
+      end
+      params do
+        requires :pipeline_schedule_id, type: Integer,  desc: 'The pipeline schedule id'
+        optional :description, type: String, desc: 'The description of pipeline schedule'
+        optional :ref, type: String, desc: 'The branch/tag name will be triggered'
+        optional :cron, type: String, desc: 'The cron'
+        optional :cron_timezone, type: String, desc: 'The timezone'
+        optional :active, type: Boolean, desc: 'The activation of pipeline schedule'
+      end
+      put ':id/pipeline_schedules/:pipeline_schedule_id' do
+        authorize! :update_pipeline_schedule, user_project
+
+        not_found!('PipelineSchedule') unless pipeline_schedule
+
+        if pipeline_schedule.update(declared_params(include_missing: false))
+          present pipeline_schedule, with: Entities::PipelineScheduleDetails
+        else
+          render_validation_error!(pipeline_schedule)
+        end
+      end
+
+      desc 'Take ownership of a pipeline schedule' do
+        success Entities::PipelineScheduleDetails
+      end
+      params do
+        requires :pipeline_schedule_id, type: Integer,  desc: 'The pipeline schedule id'
+      end
+      post ':id/pipeline_schedules/:pipeline_schedule_id/take_ownership' do
+        authorize! :update_pipeline_schedule, user_project
+
+        not_found!('PipelineSchedule') unless pipeline_schedule
+
+        if pipeline_schedule.own!(current_user)
+          present pipeline_schedule, with: Entities::PipelineScheduleDetails
+        else
+          render_validation_error!(pipeline_schedule)
+        end
+      end
+
+      desc 'Delete a pipeline schedule' do
+        success Entities::PipelineScheduleDetails
+      end
+      params do
+        requires :pipeline_schedule_id, type: Integer,  desc: 'The pipeline schedule id'
+      end
+      delete ':id/pipeline_schedules/:pipeline_schedule_id' do
+        authorize! :admin_pipeline_schedule, user_project
+
+        not_found!('PipelineSchedule') unless pipeline_schedule
+
+        status :accepted
+        present pipeline_schedule.destroy, with: Entities::PipelineScheduleDetails
+      end
+    end
+
+    helpers do
+      def pipeline_schedule
+        @pipeline_schedule ||=
+          user_project.pipeline_schedules
+                      .preload(:owner, :last_pipeline)
+                      .find_by(id: params.delete(:pipeline_schedule_id))
+      end
+    end
+  end
+end
diff --git a/spec/fixtures/api/schemas/pipeline_schedule.json b/spec/fixtures/api/schemas/pipeline_schedule.json
new file mode 100644
index 0000000000000000000000000000000000000000..f6346bd0fb6f21c3f45dd5fd975b53ecbeeb333a
--- /dev/null
+++ b/spec/fixtures/api/schemas/pipeline_schedule.json
@@ -0,0 +1,41 @@
+{
+  "type": "object",
+  "properties" : {
+    "id": { "type": "integer" },
+    "description": { "type": "string" },
+    "ref": { "type": "string" },
+    "cron": { "type": "string" },
+    "cron_timezone": { "type": "string" },
+    "next_run_at": { "type": "date" },
+    "active": { "type": "boolean" },
+    "created_at": { "type": "date" },
+    "updated_at": { "type": "date" },
+    "last_pipeline": { 
+      "type": ["object", "null"],
+      "properties": {
+        "id": { "type": "integer" },
+        "sha": { "type": "string" },
+        "ref": { "type": "string" },
+        "status": { "type": "string" }
+      },
+      "additionalProperties": false
+    },
+    "owner": {
+      "type": "object",
+      "properties": {
+        "name": { "type": "string" },
+        "username": { "type": "string" },
+        "id": { "type": "integer" },
+        "state": { "type": "string" },
+        "avatar_url": { "type": "uri" },
+        "web_url": { "type": "uri" }
+      },
+      "additionalProperties": false
+    }
+  },
+  "required": [
+    "id", "description", "ref", "cron", "cron_timezone", "next_run_at", 
+    "active", "created_at", "updated_at", "owner"
+  ],
+  "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/pipeline_schedules.json b/spec/fixtures/api/schemas/pipeline_schedules.json
new file mode 100644
index 0000000000000000000000000000000000000000..173a28d25053a89758278b3d0f8a76096b7ba745
--- /dev/null
+++ b/spec/fixtures/api/schemas/pipeline_schedules.json
@@ -0,0 +1,4 @@
+{
+  "type": "array",
+  "items": { "$ref": "pipeline_schedule.json" }
+}
diff --git a/spec/requests/api/pipeline_schedules_spec.rb b/spec/requests/api/pipeline_schedules_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..85d11deb26f2b0888a9166b50d8d1eccd7a8ef66
--- /dev/null
+++ b/spec/requests/api/pipeline_schedules_spec.rb
@@ -0,0 +1,297 @@
+require 'spec_helper'
+
+describe API::PipelineSchedules do
+  set(:developer) { create(:user) }
+  set(:user) { create(:user) }
+  set(:project) { create(:project) }
+
+  before do
+    project.add_developer(developer)
+  end
+
+  describe 'GET /projects/:id/pipeline_schedules' do
+    context 'authenticated user with valid permissions' do
+      let(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: developer) }
+
+      before do
+        pipeline_schedule.pipelines << build(:ci_pipeline, project: project)
+      end
+
+      it 'returns list of pipeline_schedules' do
+        get api("/projects/#{project.id}/pipeline_schedules", developer)
+
+        expect(response).to have_http_status(:ok)
+        expect(response).to include_pagination_headers
+        expect(response).to match_response_schema('pipeline_schedules')
+      end
+
+      it 'avoids N + 1 queries' do
+        control_count = ActiveRecord::QueryRecorder.new do
+          get api("/projects/#{project.id}/pipeline_schedules", developer)
+        end.count
+
+        create_list(:ci_pipeline_schedule, 10, project: project)
+          .each do |pipeline_schedule|
+          create(:user).tap do |user|
+            project.add_developer(user)
+            pipeline_schedule.update_attributes(owner: user)
+          end
+          pipeline_schedule.pipelines << build(:ci_pipeline, project: project)
+        end
+
+        expect do
+          get api("/projects/#{project.id}/pipeline_schedules", developer)
+        end.not_to exceed_query_limit(control_count)
+      end
+
+      %w[active inactive].each do |target|
+        context "when scope is #{target}" do
+          before do
+            create(:ci_pipeline_schedule, project: project, active: active?(target))
+          end
+
+          it 'returns matched pipeline schedules' do
+            get api("/projects/#{project.id}/pipeline_schedules", developer), scope: target
+
+            expect(json_response.map{ |r| r['active'] }).to all(eq(active?(target)))
+          end
+        end
+
+        def active?(str)
+          (str == 'active') ? true : false
+        end
+      end
+    end
+
+    context 'authenticated user with invalid permissions' do
+      it 'does not return pipeline_schedules list' do
+        get api("/projects/#{project.id}/pipeline_schedules", user)
+
+        expect(response).to have_http_status(:not_found)
+      end
+    end
+
+    context 'unauthenticated user' do
+      it 'does not return pipeline_schedules list' do
+        get api("/projects/#{project.id}/pipeline_schedules")
+
+        expect(response).to have_http_status(:unauthorized)
+      end
+    end
+  end
+
+  describe 'GET /projects/:id/pipeline_schedules/:pipeline_schedule_id' do
+    let(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: developer) }
+
+    before do
+      pipeline_schedule.pipelines << build(:ci_pipeline, project: project)
+    end
+
+    context 'authenticated user with valid permissions' do
+      it 'returns pipeline_schedule details' do
+        get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer)
+
+        expect(response).to have_http_status(:ok)
+        expect(response).to match_response_schema('pipeline_schedule')
+      end
+
+      it 'responds with 404 Not Found if requesting non-existing pipeline_schedule' do
+        get api("/projects/#{project.id}/pipeline_schedules/-5", developer)
+
+        expect(response).to have_http_status(:not_found)
+      end
+    end
+
+    context 'authenticated user with invalid permissions' do
+      it 'does not return pipeline_schedules list' do
+        get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user)
+
+        expect(response).to have_http_status(:not_found)
+      end
+    end
+
+    context 'unauthenticated user' do
+      it 'does not return pipeline_schedules list' do
+        get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}")
+
+        expect(response).to have_http_status(:unauthorized)
+      end
+    end
+  end
+
+  describe 'POST /projects/:id/pipeline_schedules' do
+    let(:params) { attributes_for(:ci_pipeline_schedule) }
+
+    context 'authenticated user with valid permissions' do
+      context 'with required parameters' do
+        it 'creates pipeline_schedule' do
+          expect do
+            post api("/projects/#{project.id}/pipeline_schedules", developer),
+              params
+          end.to change { project.pipeline_schedules.count }.by(1)
+
+          expect(response).to have_http_status(:created)
+          expect(response).to match_response_schema('pipeline_schedule')
+          expect(json_response['description']).to eq(params[:description])
+          expect(json_response['ref']).to eq(params[:ref])
+          expect(json_response['cron']).to eq(params[:cron])
+          expect(json_response['cron_timezone']).to eq(params[:cron_timezone])
+          expect(json_response['owner']['id']).to eq(developer.id)
+        end
+      end
+
+      context 'without required parameters' do
+        it 'does not create pipeline_schedule' do
+          post api("/projects/#{project.id}/pipeline_schedules", developer)
+
+          expect(response).to have_http_status(:bad_request)
+        end
+      end
+
+      context 'when cron has validation error' do
+        it 'does not create pipeline_schedule' do
+          post api("/projects/#{project.id}/pipeline_schedules", developer),
+            params.merge('cron' => 'invalid-cron')
+
+          expect(response).to have_http_status(:bad_request)
+          expect(json_response['message']).to have_key('cron')
+        end
+      end
+    end
+
+    context 'authenticated user with invalid permissions' do
+      it 'does not create pipeline_schedule' do
+        post api("/projects/#{project.id}/pipeline_schedules", user), params
+
+        expect(response).to have_http_status(:not_found)
+      end
+    end
+
+    context 'unauthenticated user' do
+      it 'does not create pipeline_schedule' do
+        post api("/projects/#{project.id}/pipeline_schedules"), params
+
+        expect(response).to have_http_status(:unauthorized)
+      end
+    end
+  end
+
+  describe 'PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id' do
+    let(:pipeline_schedule) do
+      create(:ci_pipeline_schedule, project: project, owner: developer)
+    end
+
+    context 'authenticated user with valid permissions' do
+      it 'updates cron' do
+        put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer),
+          cron: '1 2 3 4 *'
+
+        expect(response).to have_http_status(:ok)
+        expect(response).to match_response_schema('pipeline_schedule')
+        expect(json_response['cron']).to eq('1 2 3 4 *')
+      end
+
+      context 'when cron has validation error' do
+        it 'does not update pipeline_schedule' do
+          put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer),
+            cron: 'invalid-cron'
+
+          expect(response).to have_http_status(:bad_request)
+          expect(json_response['message']).to have_key('cron')
+        end
+      end
+    end
+
+    context 'authenticated user with invalid permissions' do
+      it 'does not update pipeline_schedule' do
+        put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user)
+
+        expect(response).to have_http_status(:not_found)
+      end
+    end
+
+    context 'unauthenticated user' do
+      it 'does not update pipeline_schedule' do
+        put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}")
+
+        expect(response).to have_http_status(:unauthorized)
+      end
+    end
+  end
+
+  describe 'POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/take_ownership' do
+    let(:pipeline_schedule) do
+      create(:ci_pipeline_schedule, project: project, owner: developer)
+    end
+
+    context 'authenticated user with valid permissions' do
+      it 'updates owner' do
+        post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/take_ownership", developer)
+
+        expect(response).to have_http_status(:created)
+        expect(response).to match_response_schema('pipeline_schedule')
+      end
+    end
+
+    context 'authenticated user with invalid permissions' do
+      it 'does not update owner' do
+        post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/take_ownership", user)
+
+        expect(response).to have_http_status(:not_found)
+      end
+    end
+
+    context 'unauthenticated user' do
+      it 'does not update owner' do
+        post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/take_ownership")
+
+        expect(response).to have_http_status(:unauthorized)
+      end
+    end
+  end
+
+  describe 'DELETE /projects/:id/pipeline_schedules/:pipeline_schedule_id' do
+    let(:master) { create(:user) }
+
+    let!(:pipeline_schedule) do
+      create(:ci_pipeline_schedule, project: project, owner: developer)
+    end
+
+    before do
+      project.add_master(master)
+    end
+
+    context 'authenticated user with valid permissions' do
+      it 'deletes pipeline_schedule' do
+        expect do
+          delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", master)
+        end.to change { project.pipeline_schedules.count }.by(-1)
+
+        expect(response).to have_http_status(:accepted)
+        expect(response).to match_response_schema('pipeline_schedule')
+      end
+
+      it 'responds with 404 Not Found if requesting non-existing pipeline_schedule' do
+        delete api("/projects/#{project.id}/pipeline_schedules/-5", master)
+
+        expect(response).to have_http_status(:not_found)
+      end
+    end
+
+    context 'authenticated user with invalid permissions' do
+      it 'does not delete pipeline_schedule' do
+        delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", developer)
+
+        expect(response).to have_http_status(:forbidden)
+      end
+    end
+
+    context 'unauthenticated user' do
+      it 'does not delete pipeline_schedule' do
+        delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}")
+
+        expect(response).to have_http_status(:unauthorized)
+      end
+    end
+  end
+end