diff --git a/lib/all-badge-examples.js b/lib/all-badge-examples.js
index c50fce816880e657bcb9d7a41c088444cff836b5..d4df4312478684608a05a1604a3b30ecd4274f05 100644
--- a/lib/all-badge-examples.js
+++ b/lib/all-badge-examples.js
@@ -137,10 +137,6 @@ const allBadgeExamples = [
         previewUri: '/travis/com/ivandelabeldad/rackian-gateway/master.svg',
         exampleUri: '/travis/com/USER/REPO/BRANCH.svg',
       },
-      {
-        title: 'Wercker',
-        previewUri: '/wercker/ci/wercker/docs.svg',
-      },
       {
         title: 'TeamCity CodeBetter',
         previewUri: '/teamcity/codebetter/bt428.svg',
diff --git a/server.js b/server.js
index 97bd2ae30e45af23389a2366b5c4664095fa69df..57da0ae722c3b5a638290124dd0a3b969e395167 100644
--- a/server.js
+++ b/server.js
@@ -576,87 +576,6 @@ cache(function (data, match, sendBadge, request) {
   });
 }));
 
-// Wercker integration
-camp.route(/^\/wercker\/ci\/([a-fA-F0-9]+)\.(svg|png|gif|jpg|json)$/,
-cache(function(data, match, sendBadge, request) {
-  var projectId = match[1];  // eg, `54330318b4ce963d50020750`
-  var format = match[2];
-  var options = {
-    method: 'GET',
-    json: true,
-    uri: 'https://app.wercker.com/getbuilds/' + projectId + '?limit=1',
-  };
-  var badgeData = getBadgeData('build', data);
-  request(options, function(err, res, json) {
-    if (err != null) {
-      badgeData.text[1] = 'inaccessible';
-      sendBadge(format, badgeData);
-      return;
-    }
-    try {
-      var build = json[0];
-
-      if (build.status === 'finished') {
-        if (build.result === 'passed') {
-          badgeData.colorscheme = 'brightgreen';
-          badgeData.text[1] = 'passing';
-        } else {
-          badgeData.colorscheme = 'red';
-          badgeData.text[1] = build.result;
-        }
-      } else {
-        badgeData.text[1] = build.status;
-      }
-      sendBadge(format, badgeData);
-
-    } catch(e) {
-      badgeData.text[1] = 'invalid';
-      sendBadge(format, badgeData);
-    }
-  });
-}));
-
-// Wercker V3 integration
-camp.route(/^\/wercker\/ci\/(.+)\/(.+)\.(svg|png|gif|jpg|json)$/,
-cache(function(data, match, sendBadge, request) {
-  var owner = match[1];
-  var application = match[2];
-  var format = match[3];
-  var options = {
-    method: 'GET',
-    json: true,
-    uri: 'https://app.wercker.com/api/v3/applications/' + owner + '/' + application + '/builds?limit=1',
-  };
-  var badgeData = getBadgeData('build', data);
-  request(options, function(err, res, json) {
-    if (err != null) {
-      badgeData.text[1] = 'inaccessible';
-      sendBadge(format, badgeData);
-      return;
-    }
-    try {
-      var build = json[0];
-
-      if (build.status === 'finished') {
-        if (build.result === 'passed') {
-          badgeData.colorscheme = 'brightgreen';
-          badgeData.text[1] = 'passing';
-        } else {
-          badgeData.colorscheme = 'red';
-          badgeData.text[1] = build.result;
-        }
-      } else {
-        badgeData.text[1] = build.status;
-      }
-      sendBadge(format, badgeData);
-
-    } catch(e) {
-      badgeData.text[1] = 'invalid';
-      sendBadge(format, badgeData);
-    }
-  });
-}));
-
 // Rust download and version integration
 camp.route(/^\/crates\/(d|v|dv|l)\/([A-Za-z0-9_-]+)(?:\/([0-9.]+))?\.(svg|png|gif|jpg|json)$/,
 cache(function (data, match, sendBadge, request) {
diff --git a/services/wercker/wercker.service.js b/services/wercker/wercker.service.js
new file mode 100644
index 0000000000000000000000000000000000000000..26bc9caf58fe28f77abbe9908887d27d84830620
--- /dev/null
+++ b/services/wercker/wercker.service.js
@@ -0,0 +1,73 @@
+'use strict'
+
+const Joi = require('joi')
+const BaseJsonService = require('../base-json')
+
+const werckerSchema = Joi.array()
+  .items(
+    Joi.object({
+      status: Joi.string().required(),
+      result: Joi.string().required(),
+    })
+  )
+  .min(1)
+  .required()
+
+module.exports = class Wercker extends BaseJsonService {
+  async fetch({ applicationName, projectId, branch }) {
+    const url = applicationName
+      ? `https://app.wercker.com/api/v3/applications/${applicationName}/builds?limit=1`
+      : `https://app.wercker.com/getbuilds/${projectId}?limit=1`
+    return this._requestJson({
+      schema: werckerSchema,
+      url,
+      options: { qs: { branch } },
+      errorMessages: {
+        401: 'private application not supported',
+        404: 'application not found',
+      },
+    })
+  }
+
+  static render({ status, result }) {
+    if (status === 'finished') {
+      if (result === 'passed') {
+        return { message: 'passing', color: 'brightgreen' }
+      } else {
+        return { message: result, color: 'red' }
+      }
+    }
+    return { message: status }
+  }
+
+  async handle({ applicationName, projectId, branch }) {
+    const json = await this.fetch({ applicationName, projectId, branch })
+    const { status, result } = json[0]
+    return this.constructor.render({ status, result })
+  }
+
+  // Metadata
+  static get category() {
+    return 'build'
+  }
+
+  static get url() {
+    return {
+      base: 'wercker/ci',
+      format: '(?:([^/]+/[^/]+)|([a-fA-F0-9]+))(?:/(.+))?',
+      capture: ['applicationName', 'projectId', 'branch'],
+    }
+  }
+
+  static get examples() {
+    return [
+      {
+        previewUrl: 'wercker/go-wercker-api',
+      },
+      {
+        title: `${this.name} branch`,
+        previewUrl: 'wercker/go-wercker-api/master',
+      },
+    ]
+  }
+}
diff --git a/services/wercker/wercker.tester.js b/services/wercker/wercker.tester.js
new file mode 100644
index 0000000000000000000000000000000000000000..0ff38c4fc35af6d9bdefc2e8f7c16f6d36705bf1
--- /dev/null
+++ b/services/wercker/wercker.tester.js
@@ -0,0 +1,29 @@
+'use strict'
+
+const Joi = require('joi')
+const ServiceTester = require('../service-tester')
+
+const { isBuildStatus } = require('../test-validators')
+
+const t = new ServiceTester({ id: 'wercker', title: 'Wercker' })
+module.exports = t
+
+t.create('CI build status')
+  .get('/ci/wercker/go-wercker-api.json')
+  .expectJSONTypes(Joi.object().keys({ name: 'build', value: isBuildStatus }))
+
+t.create('CI build status (branch)')
+  .get('/ci/wercker/go-wercker-api/master.json')
+  .expectJSONTypes(Joi.object().keys({ name: 'build', value: isBuildStatus }))
+
+t.create('CI build status (old v1 API)')
+  .get('/ci/559e33c8e982fc615500b357.json')
+  .expectJSONTypes(Joi.object().keys({ name: 'build', value: isBuildStatus }))
+
+t.create('CI application not found')
+  .get('/ci/somerandomproject/thatdoesntexits.json')
+  .expectJSON({ name: 'build', value: 'application not found' })
+
+t.create('CI private application')
+  .get('/ci/wercker/blueprint.json')
+  .expectJSON({ name: 'build', value: 'private application not supported' })