diff --git a/CHANGELOG b/CHANGELOG
index 528e33a91d673c9ba6c65914040d2d0b8c4ddd5c..13ec1bb885fb9eb2363cbcd9114c4586b29ec727 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -23,6 +23,7 @@ v 8.12.0 (unreleased)
   - Remove Gitorious import
   - Fix inconsistent background color for filter input field (ClemMakesApps)
   - Add Sentry logging to API calls
+  - Add BroadcastMessage API
   - Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling)
   - Remove unused mixins (ClemMakesApps)
   - Add search to all issue board lists
diff --git a/doc/api/broadcast_messages.md b/doc/api/broadcast_messages.md
new file mode 100644
index 0000000000000000000000000000000000000000..c3a9207a3ae51ecec296abe3162830cc8f19eee5
--- /dev/null
+++ b/doc/api/broadcast_messages.md
@@ -0,0 +1,158 @@
+# Broadcast Messages
+
+> **Note:** This feature was introduced in GitLab 8.12.
+
+The broadcast message API is only accessible to administrators. All requests by
+guests will respond with `401 Unauthorized`, and all requests by normal users
+will respond with `403 Forbidden`.
+
+## Get all broadcast messages
+
+```
+GET /broadcast_messages
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages
+```
+
+Example response:
+
+```json
+[
+    {
+        "message":"Example broadcast message",
+        "starts_at":"2016-08-24T23:21:16.078Z",
+        "ends_at":"2016-08-26T23:21:16.080Z",
+        "color":"#E75E40",
+        "font":"#FFFFFF",
+        "id":1,
+        "active": false
+    }
+]
+```
+
+## Get a specific broadcast message
+
+```
+GET /broadcast_messages/:id
+```
+
+| Attribute   | Type     | Required | Description               |
+| ----------- | -------- | -------- | ------------------------- |
+| `id`        | integer  | yes      | Broadcast message ID      |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1
+```
+
+Example response:
+
+```json
+{
+    "message":"Deploy in progress",
+    "starts_at":"2016-08-24T23:21:16.078Z",
+    "ends_at":"2016-08-26T23:21:16.080Z",
+    "color":"#cecece",
+    "font":"#FFFFFF",
+    "id":1,
+    "active":false
+}
+```
+
+## Create a broadcast message
+
+Responds with `400 Bad request` when the `message` parameter is missing or the
+`color` or `font` values are invalid, and `201 Created` when the broadcast
+message was successfully created.
+
+```
+POST /broadcast_messages
+```
+
+| Attribute   | Type     | Required | Description                                          |
+| ----------- | -------- | -------- | ---------------------------------------------------- |
+| `message`   | string   | yes      | Message to display                                   |
+| `starts_at` | datetime | no       | Starting time (defaults to current time)             |
+| `ends_at`   | datetime | no       | Ending time (defaults to one hour from current time) |
+| `color`     | string   | no       | Background color hex code                            |
+| `font`      | string   | no       | Foreground color hex code                            |
+
+```bash
+curl --data "message=Deploy in progress&color=#cecece" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages
+```
+
+Example response:
+
+```json
+{
+    "message":"Deploy in progress",
+    "starts_at":"2016-08-26T00:41:35.060Z",
+    "ends_at":"2016-08-26T01:41:35.060Z",
+    "color":"#cecece",
+    "font":"#FFFFFF",
+    "id":1,
+    "active": true
+}
+```
+
+## Update a broadcast message
+
+```
+PUT /broadcast_messages/:id
+```
+
+| Attribute   | Type     | Required | Description               |
+| ----------- | -------- | -------- | ------------------------- |
+| `id`        | integer  | yes      | Broadcast message ID      |
+| `message`   | string   | no       | Message to display        |
+| `starts_at` | datetime | no       | Starting time             |
+| `ends_at`   | datetime | no       | Ending time               |
+| `color`     | string   | no       | Background color hex code |
+| `font`      | string   | no       | Foreground color hex code |
+
+```bash
+curl --request PUT --data "message=Update message&color=#000" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1
+```
+
+Example response:
+
+```json
+{
+    "message":"Update message",
+    "starts_at":"2016-08-26T00:41:35.060Z",
+    "ends_at":"2016-08-26T01:41:35.060Z",
+    "color":"#000",
+    "font":"#FFFFFF",
+    "id":1,
+    "active": true
+}
+```
+
+## Delete a broadcast message
+
+```
+DELETE /broadcast_messages/:id
+```
+
+| Attribute   | Type     | Required | Description               |
+| ----------- | -------- | -------- | ------------------------- |
+| `id`        | integer  | yes      | Broadcast message ID      |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/broadcast_messages/1
+```
+
+Example response:
+
+```json
+{
+    "message":"Update message",
+    "starts_at":"2016-08-26T00:41:35.060Z",
+    "ends_at":"2016-08-26T01:41:35.060Z",
+    "color":"#000",
+    "font":"#FFFFFF",
+    "id":1,
+    "active": true
+}
+```
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 4602e627fdb71b39c1f3b5ee3ccd54eb4c17aaf4..e14464c1b0d05847a67c8c22cd1181fb6a9f9e6d 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -31,6 +31,7 @@ module API
     mount ::API::AccessRequests
     mount ::API::AwardEmoji
     mount ::API::Branches
+    mount ::API::BroadcastMessages
     mount ::API::Builds
     mount ::API::CommitStatuses
     mount ::API::Commits
diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fb2a41480113d71390b2ca2d5f18a192e541ec4d
--- /dev/null
+++ b/lib/api/broadcast_messages.rb
@@ -0,0 +1,99 @@
+module API
+  class BroadcastMessages < Grape::API
+    before { authenticate! }
+    before { authenticated_as_admin! }
+
+    resource :broadcast_messages do
+      helpers do
+        def find_message
+          BroadcastMessage.find(params[:id])
+        end
+      end
+
+      desc 'Get all broadcast messages' do
+        detail 'This feature was introduced in GitLab 8.12.'
+        success Entities::BroadcastMessage
+      end
+      params do
+        optional :page,     type: Integer, desc: 'Current page number'
+        optional :per_page, type: Integer, desc: 'Number of messages per page'
+      end
+      get do
+        messages = BroadcastMessage.all
+
+        present paginate(messages), with: Entities::BroadcastMessage
+      end
+
+      desc 'Create a broadcast message' do
+        detail 'This feature was introduced in GitLab 8.12.'
+        success Entities::BroadcastMessage
+      end
+      params do
+        requires :message,   type: String,   desc: 'Message to display'
+        optional :starts_at, type: DateTime, desc: 'Starting time', default: -> { Time.zone.now }
+        optional :ends_at,   type: DateTime, desc: 'Ending time',   default: -> { 1.hour.from_now }
+        optional :color,     type: String,   desc: 'Background color'
+        optional :font,      type: String,   desc: 'Foreground color'
+      end
+      post do
+        create_params = declared(params, include_missing: false).to_h
+        message = BroadcastMessage.create(create_params)
+
+        if message.persisted?
+          present message, with: Entities::BroadcastMessage
+        else
+          render_validation_error!(message)
+        end
+      end
+
+      desc 'Get a specific broadcast message' do
+        detail 'This feature was introduced in GitLab 8.12.'
+        success Entities::BroadcastMessage
+      end
+      params do
+        requires :id, type: Integer, desc: 'Broadcast message ID'
+      end
+      get ':id' do
+        message = find_message
+
+        present message, with: Entities::BroadcastMessage
+      end
+
+      desc 'Update a broadcast message' do
+        detail 'This feature was introduced in GitLab 8.12.'
+        success Entities::BroadcastMessage
+      end
+      params do
+        requires :id,        type: Integer,  desc: 'Broadcast message ID'
+        optional :message,   type: String,   desc: 'Message to display'
+        optional :starts_at, type: DateTime, desc: 'Starting time'
+        optional :ends_at,   type: DateTime, desc: 'Ending time'
+        optional :color,     type: String,   desc: 'Background color'
+        optional :font,      type: String,   desc: 'Foreground color'
+      end
+      put ':id' do
+        message = find_message
+        update_params = declared(params, include_missing: false).to_h
+
+        if message.update(update_params)
+          present message, with: Entities::BroadcastMessage
+        else
+          render_validation_error!(message)
+        end
+      end
+
+      desc 'Delete a broadcast message' do
+        detail 'This feature was introduced in GitLab 8.12.'
+        success Entities::BroadcastMessage
+      end
+      params do
+        requires :id, type: Integer, desc: 'Broadcast message ID'
+      end
+      delete ':id' do
+        message = find_message
+
+        present message.destroy, with: Entities::BroadcastMessage
+      end
+    end
+  end
+end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index e3a8ff6de806a06bdfe7decf6cb334a1aa5f651f..fe7468dd68133db03bf814e9f7dcd20d25a6997c 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -575,5 +575,10 @@ module API
     class Template < Grape::Entity
       expose :name, :content
     end
+
+    class BroadcastMessage < Grape::Entity
+      expose :id, :message, :starts_at, :ends_at, :color, :font
+      expose :active?, as: :active
+    end
   end
 end
diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7c9078b28642cd1d44b9675368ffc7152659c40a
--- /dev/null
+++ b/spec/requests/api/broadcast_messages_spec.rb
@@ -0,0 +1,180 @@
+require 'spec_helper'
+
+describe API::BroadcastMessages, api: true do
+  include ApiHelpers
+
+  let(:user)  { create(:user) }
+  let(:admin) { create(:admin) }
+
+  describe 'GET /broadcast_messages' do
+    it 'returns a 401 for anonymous users' do
+      get api('/broadcast_messages')
+
+      expect(response).to have_http_status(401)
+    end
+
+    it 'returns a 403 for users' do
+      get api('/broadcast_messages', user)
+
+      expect(response).to have_http_status(403)
+    end
+
+    it 'returns an Array of BroadcastMessages for admins' do
+      create(:broadcast_message)
+
+      get api('/broadcast_messages', admin)
+
+      expect(response).to have_http_status(200)
+      expect(json_response).to be_kind_of(Array)
+      expect(json_response.first.keys)
+        .to match_array(%w(id message starts_at ends_at color font active))
+    end
+  end
+
+  describe 'GET /broadcast_messages/:id' do
+    let!(:message) { create(:broadcast_message) }
+
+    it 'returns a 401 for anonymous users' do
+      get api("/broadcast_messages/#{message.id}")
+
+      expect(response).to have_http_status(401)
+    end
+
+    it 'returns a 403 for users' do
+      get api("/broadcast_messages/#{message.id}", user)
+
+      expect(response).to have_http_status(403)
+    end
+
+    it 'returns the specified message for admins' do
+      get api("/broadcast_messages/#{message.id}", admin)
+
+      expect(response).to have_http_status(200)
+      expect(json_response['id']).to eq message.id
+      expect(json_response.keys)
+        .to match_array(%w(id message starts_at ends_at color font active))
+    end
+  end
+
+  describe 'POST /broadcast_messages' do
+    it 'returns a 401 for anonymous users' do
+      post api('/broadcast_messages'), attributes_for(:broadcast_message)
+
+      expect(response).to have_http_status(401)
+    end
+
+    it 'returns a 403 for users' do
+      post api('/broadcast_messages', user), attributes_for(:broadcast_message)
+
+      expect(response).to have_http_status(403)
+    end
+
+    context 'as an admin' do
+      it 'requires the `message` parameter' do
+        attrs = attributes_for(:broadcast_message)
+        attrs.delete(:message)
+
+        post api('/broadcast_messages', admin), attrs
+
+        expect(response).to have_http_status(400)
+        expect(json_response['error']).to eq 'message is missing'
+      end
+
+      it 'defines sane default start and end times' do
+        time = Time.zone.parse('2016-07-02 10:11:12')
+        travel_to(time) do
+          post api('/broadcast_messages', admin), message: 'Test message'
+
+          expect(response).to have_http_status(201)
+          expect(json_response['starts_at']).to eq '2016-07-02T10:11:12.000Z'
+          expect(json_response['ends_at']).to   eq '2016-07-02T11:11:12.000Z'
+        end
+      end
+
+      it 'accepts a custom background and foreground color' do
+        attrs = attributes_for(:broadcast_message, color: '#000000', font: '#cecece')
+
+        post api('/broadcast_messages', admin), attrs
+
+        expect(response).to have_http_status(201)
+        expect(json_response['color']).to eq attrs[:color]
+        expect(json_response['font']).to eq attrs[:font]
+      end
+    end
+  end
+
+  describe 'PUT /broadcast_messages/:id' do
+    let!(:message) { create(:broadcast_message) }
+
+    it 'returns a 401 for anonymous users' do
+      put api("/broadcast_messages/#{message.id}"),
+        attributes_for(:broadcast_message)
+
+      expect(response).to have_http_status(401)
+    end
+
+    it 'returns a 403 for users' do
+      put api("/broadcast_messages/#{message.id}", user),
+        attributes_for(:broadcast_message)
+
+      expect(response).to have_http_status(403)
+    end
+
+    context 'as an admin' do
+      it 'accepts new background and foreground colors' do
+        attrs = { color: '#000000', font: '#cecece' }
+
+        put api("/broadcast_messages/#{message.id}", admin), attrs
+
+        expect(response).to have_http_status(200)
+        expect(json_response['color']).to eq attrs[:color]
+        expect(json_response['font']).to eq attrs[:font]
+      end
+
+      it 'accepts new start and end times' do
+        time = Time.zone.parse('2016-07-02 10:11:12')
+        travel_to(time) do
+          attrs = { starts_at: Time.zone.now, ends_at: 3.hours.from_now }
+
+          put api("/broadcast_messages/#{message.id}", admin), attrs
+
+          expect(response).to have_http_status(200)
+          expect(json_response['starts_at']).to eq '2016-07-02T10:11:12.000Z'
+          expect(json_response['ends_at']).to   eq '2016-07-02T13:11:12.000Z'
+        end
+      end
+
+      it 'accepts a new message' do
+        attrs = { message: 'new message' }
+
+        put api("/broadcast_messages/#{message.id}", admin), attrs
+
+        expect(response).to have_http_status(200)
+        expect { message.reload }.to change { message.message }.to('new message')
+      end
+    end
+  end
+
+  describe 'DELETE /broadcast_messages/:id' do
+    let!(:message) { create(:broadcast_message) }
+
+    it 'returns a 401 for anonymous users' do
+      delete api("/broadcast_messages/#{message.id}"),
+        attributes_for(:broadcast_message)
+
+      expect(response).to have_http_status(401)
+    end
+
+    it 'returns a 403 for users' do
+      delete api("/broadcast_messages/#{message.id}", user),
+        attributes_for(:broadcast_message)
+
+      expect(response).to have_http_status(403)
+    end
+
+    it 'deletes the broadcast message for admins' do
+      expect { delete api("/broadcast_messages/#{message.id}", admin) }
+        .to change { BroadcastMessage.count }.by(-1)
+    end
+  end
+end