diff --git a/CHANGELOG b/CHANGELOG
index c2e10f62d9b43dceb9f96fae4e03cd306e51b8b3..a0c56837f89645c25c119805dd606ff63f5fbcc9 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -45,6 +45,7 @@ v 8.4.0 (unreleased)
   - Allow subsequent validations in CI Linter
   - Show referenced MRs & Issues only when the current viewer can access them
   - Fix Encoding::CompatibilityError bug when markdown content has some complex URL (Jason Lee)
+  - Add API support for managing build variables of project
   - Allow broadcast messages to be edited
 
 v 8.3.4
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 7f6f497f325dd22723fe7a9e77e4d907e91c9c73..e786bd7dd93c99f9cb4ddc0a588f2c9b0a9d1856 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -18,8 +18,12 @@ module Ci
     
     belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
 
-    validates_presence_of :key
     validates_uniqueness_of :key, scope: :gl_project_id
+    validates :key,
+      presence: true,
+      length: { within: 0..255 },
+      format: { with: /\A[a-zA-Z0-9_]+\z/,
+                message: "can contain only letters, digits and '_'." }
 
     attr_encrypted :value, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base
   end
diff --git a/doc/api/README.md b/doc/api/README.md
index 25a31b235cc77dc195bea89b70e02a7c106b34c9..c3401bcbc9d6d6997b2988e9bb1485517ce40d1a 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -23,6 +23,7 @@
 - [Namespaces](namespaces.md)
 - [Settings](settings.md)
 - [Keys](keys.md)
+- [Build Variables](build_variables.md)
 
 ## Clients
 
diff --git a/doc/api/build_variables.md b/doc/api/build_variables.md
new file mode 100644
index 0000000000000000000000000000000000000000..b96f1bdac8ab0971a5b21573c00555738b6cd85c
--- /dev/null
+++ b/doc/api/build_variables.md
@@ -0,0 +1,128 @@
+# Build Variables
+
+## List project variables
+
+Get list of a project's build variables.
+
+```
+GET /projects/:id/variables
+```
+
+| Attribute | Type    | required | Description         |
+|-----------|---------|----------|---------------------|
+| `id`      | integer | yes      | The ID of a project |
+
+```
+curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables"
+```
+
+```json
+[
+    {
+        "key": "TEST_VARIABLE_1",
+        "value": "TEST_1"
+    },
+    {
+        "key": "TEST_VARIABLE_2",
+        "value": "TEST_2"
+    }
+]
+```
+
+## Show variable details
+
+Get the details of a project's specific build variable.
+
+```
+GET /projects/:id/variables/:key
+```
+
+| Attribute | Type    | required | Description           |
+|-----------|---------|----------|-----------------------|
+| `id`      | integer | yes      | The ID of a project   |
+| `key`     | string  | yes      | The `key` of a variable |
+
+```
+curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/TEST_VARIABLE_1"
+```
+
+```json
+{
+    "key": "TEST_VARIABLE_1",
+    "value": "TEST_1"
+}
+```
+
+## Create variable
+
+Create a new build variable.
+
+```
+POST /projects/:id/variables
+```
+
+| Attribute | Type    | required | Description           |
+|-----------|---------|----------|-----------------------|
+| `id`      | integer | yes      | The ID of a project   |
+| `key`     | string  | yes      | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed |
+| `value`   | string  | yes      | The `value` of a variable |
+
+```
+curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" -F "key=NEW_VARIABLE" -F "value=new value"
+```
+
+```json
+{
+    "key": "NEW_VARIABLE",
+    "value": "new value"
+}
+```
+
+## Update variable
+
+Update a project's build variable.
+
+```
+PUT /projects/:id/variables/:key
+```
+
+| Attribute | Type    | required | Description             |
+|-----------|---------|----------|-------------------------|
+| `id`      | integer | yes      | The ID of a project     |
+| `key`     | string  | yes      | The `key` of a variable   |
+| `value`   | string  | yes      | The `value` of a variable |
+
+```
+curl -X PUT -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/NEW_VARIABLE" -F "value=updated value"
+```
+
+```json
+{
+    "key": "NEW_VARIABLE",
+    "value": "updated value"
+}
+```
+
+## Remove variable
+
+Remove a project's build variable.
+
+```
+DELETE /projects/:id/variables/:key
+```
+
+| Attribute | Type    | required | Description             |
+|-----------|---------|----------|-------------------------|
+| `id`      | integer | yes      | The ID of a project     |
+| `key`     | string  | yes      | The `key` of a variable |
+
+```
+curl -X DELETE -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/VARIABLE_1"
+```
+
+```json
+{
+    "key": "VARIABLE_1",
+    "value": "VALUE_1"
+}
+```
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 7834262d612244d7162eb33f99e9458b4937200c..098dd9758402eb3c1bd7e0617f042f0ab8aa2fdc 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -54,5 +54,6 @@ module API
     mount Keys
     mount Tags
     mount Triggers
+    mount Variables
   end
 end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index e3bc3316ce56cdad022a513c246e532e446b8e5b..e2feb7bb6d9fa7c03b4cae950d40ce60255e09f3 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -366,5 +366,9 @@ module API
     class TriggerRequest < Grape::Entity
       expose :id, :variables
     end
+
+    class Variable < Grape::Entity
+      expose :key, :value
+    end
   end
 end
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d9a055f6c927ad00b1e1843d613ff1391fc76ff3
--- /dev/null
+++ b/lib/api/variables.rb
@@ -0,0 +1,95 @@
+module API
+  # Projects variables API
+  class Variables < Grape::API
+    before { authenticate! }
+    before { authorize_admin_project }
+
+    resource :projects do
+      # Get project variables
+      #
+      # Parameters:
+      #   id (required) - The ID of a project
+      #   page (optional) - The page number for pagination
+      #   per_page (optional) - The value of items per page to show
+      # Example Request:
+      #   GET /projects/:id/variables
+      get ':id/variables' do
+        variables = user_project.variables
+        present paginate(variables), with: Entities::Variable
+      end
+
+      # Get specific variable of a project
+      #
+      # Parameters:
+      #   id (required) - The ID of a project
+      #   key (required) - The `key` of variable
+      # Example Request:
+      #   GET /projects/:id/variables/:key
+      get ':id/variables/:key' do
+        key = params[:key]
+        variable = user_project.variables.find_by(key: key.to_s)
+
+        return not_found!('Variable') unless variable
+
+        present variable, with: Entities::Variable
+      end
+
+      # Create a new variable in project
+      #
+      # Parameters:
+      #   id (required) - The ID of a project
+      #   key (required) - The key of variable
+      #   value (required) - The value of variable
+      # Example Request:
+      #   POST /projects/:id/variables
+      post ':id/variables' do
+        required_attributes! [:key, :value]
+
+        variable = user_project.variables.create(key: params[:key], value: params[:value])
+
+        if variable.valid?
+          present variable, with: Entities::Variable
+        else
+          render_validation_error!(variable)
+        end
+      end
+
+      # Update existing variable of a project
+      #
+      # Parameters:
+      #   id (required) - The ID of a project
+      #   key (optional) - The `key` of variable
+      #   value (optional) - New value for `value` field of variable
+      # Example Request:
+      #   PUT /projects/:id/variables/:key
+      put ':id/variables/:key' do
+        variable = user_project.variables.find_by(key: params[:key].to_s)
+
+        return not_found!('Variable') unless variable
+
+        attrs = attributes_for_keys [:value]
+        if variable.update(attrs)
+          present variable, with: Entities::Variable
+        else
+          render_validation_error!(variable)
+        end
+      end
+
+      # Delete existing variable of a project
+      #
+      # Parameters:
+      #   id (required) - The ID of a project
+      #   key (required) - The ID of a variable
+      # Example Request:
+      #   DELETE /projects/:id/variables/:key
+      delete ':id/variables/:key' do
+        variable = user_project.variables.find_by(key: params[:key].to_s)
+
+        return not_found!('Variable') unless variable
+        variable.destroy
+
+        present variable, with: Entities::Variable
+      end
+    end
+  end
+end
diff --git a/spec/factories/ci/variables.rb b/spec/factories/ci/variables.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8f62d64411bdea433fbc06b6e9d1be79c7cf8355
--- /dev/null
+++ b/spec/factories/ci/variables.rb
@@ -0,0 +1,22 @@
+# == Schema Information
+#
+# Table name: ci_variables
+#
+#  id                   :integer          not null, primary key
+#  project_id           :integer          not null
+#  key                  :string(255)
+#  value                :text
+#  encrypted_value      :text
+#  encrypted_value_salt :string(255)
+#  encrypted_value_iv   :string(255)
+#  gl_project_id        :integer
+#
+
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+  factory :ci_variable, class: Ci::Variable do
+    sequence(:key) { |n| "VARIABLE_#{n}" }
+    value 'VARIABLE_VALUE'
+  end
+end
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9744729ba0c4620166c4564c2b85a1c4e5c0c9e2
--- /dev/null
+++ b/spec/requests/api/variables_spec.rb
@@ -0,0 +1,182 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+  include ApiHelpers
+
+  let(:user) { create(:user) }
+  let(:user2) { create(:user) }
+  let!(:project) { create(:project, creator_id: user.id) }
+  let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
+  let!(:developer) { create(:project_member, user: user2, project: project, access_level: ProjectMember::DEVELOPER) }
+  let!(:variable) { create(:ci_variable, project: project) }
+
+  describe 'GET /projects/:id/variables' do
+    context 'authorized user with proper permissions' do
+      it 'should return project variables' do
+        get api("/projects/#{project.id}/variables", user)
+
+        expect(response.status).to eq(200)
+        expect(json_response).to be_a(Array)
+      end
+    end
+
+    context 'authorized user with invalid permissions' do
+      it 'should not return project variables' do
+        get api("/projects/#{project.id}/variables", user2)
+
+        expect(response.status).to eq(403)
+      end
+    end
+
+    context 'unauthorized user' do
+      it 'should not return project variables' do
+        get api("/projects/#{project.id}/variables")
+
+        expect(response.status).to eq(401)
+      end
+    end
+  end
+
+  describe 'GET /projects/:id/variables/:key' do
+    context 'authorized user with proper permissions' do
+      it 'should return project variable details' do
+        get api("/projects/#{project.id}/variables/#{variable.key}", user)
+
+        expect(response.status).to eq(200)
+        expect(json_response['value']).to eq(variable.value)
+      end
+
+      it 'should respond with 404 Not Found if requesting non-existing variable' do
+        get api("/projects/#{project.id}/variables/non_existing_variable", user)
+
+        expect(response.status).to eq(404)
+      end
+    end
+
+    context 'authorized user with invalid permissions' do
+      it 'should not return project variable details' do
+        get api("/projects/#{project.id}/variables/#{variable.key}", user2)
+
+        expect(response.status).to eq(403)
+      end
+    end
+
+    context 'unauthorized user' do
+      it 'should not return project variable details' do
+        get api("/projects/#{project.id}/variables/#{variable.key}")
+
+        expect(response.status).to eq(401)
+      end
+    end
+  end
+
+  describe 'POST /projects/:id/variables' do
+    context 'authorized user with proper permissions' do
+      it 'should create variable' do
+        expect do
+          post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2'
+        end.to change{project.variables.count}.by(1)
+
+        expect(response.status).to eq(201)
+        expect(json_response['key']).to eq('TEST_VARIABLE_2')
+        expect(json_response['value']).to eq('VALUE_2')
+      end
+
+      it 'should not allow to duplicate variable key' do
+        expect do
+          post api("/projects/#{project.id}/variables", user), key: variable.key, value: 'VALUE_2'
+        end.to change{project.variables.count}.by(0)
+
+        expect(response.status).to eq(400)
+      end
+    end
+
+    context 'authorized user with invalid permissions' do
+      it 'should not create variable' do
+        post api("/projects/#{project.id}/variables", user2)
+
+        expect(response.status).to eq(403)
+      end
+    end
+
+    context 'unauthorized user' do
+      it 'should not create variable' do
+        post api("/projects/#{project.id}/variables")
+
+        expect(response.status).to eq(401)
+      end
+    end
+  end
+
+  describe 'PUT /projects/:id/variables/:key' do
+    context 'authorized user with proper permissions' do
+      it 'should update variable data' do
+        initial_variable = project.variables.first
+        value_before = initial_variable.value
+
+        put api("/projects/#{project.id}/variables/#{variable.key}", user), value: 'VALUE_1_UP'
+
+        updated_variable = project.variables.first
+
+        expect(response.status).to eq(200)
+        expect(value_before).to eq(variable.value)
+        expect(updated_variable.value).to eq('VALUE_1_UP')
+      end
+
+      it 'should responde with 404 Not Found if requesting non-existing variable' do
+        put api("/projects/#{project.id}/variables/non_existing_variable", user)
+
+        expect(response.status).to eq(404)
+      end
+    end
+
+    context 'authorized user with invalid permissions' do
+      it 'should not update variable' do
+        put api("/projects/#{project.id}/variables/#{variable.key}", user2)
+
+        expect(response.status).to eq(403)
+      end
+    end
+
+    context 'unauthorized user' do
+      it 'should not update variable' do
+        put api("/projects/#{project.id}/variables/#{variable.key}")
+
+        expect(response.status).to eq(401)
+      end
+    end
+  end
+
+  describe 'DELETE /projects/:id/variables/:key' do
+    context 'authorized user with proper permissions' do
+      it 'should delete variable' do
+        expect do
+          delete api("/projects/#{project.id}/variables/#{variable.key}", user)
+        end.to change{project.variables.count}.by(-1)
+        expect(response.status).to eq(200)
+      end
+
+      it 'should responde with 404 Not Found if requesting non-existing variable' do
+        delete api("/projects/#{project.id}/variables/non_existing_variable", user)
+
+        expect(response.status).to eq(404)
+      end
+    end
+
+    context 'authorized user with invalid permissions' do
+      it 'should not delete variable' do
+        delete api("/projects/#{project.id}/variables/#{variable.key}", user2)
+
+        expect(response.status).to eq(403)
+      end
+    end
+
+    context 'unauthorized user' do
+      it 'should not delete variable' do
+        delete api("/projects/#{project.id}/variables/#{variable.key}")
+
+        expect(response.status).to eq(401)
+      end
+    end
+  end
+end