diff --git a/services/bitrise/bitrise.service.js b/services/bitrise/bitrise.service.js
index abe3c64c9396b2b4c3c31d1b02caa08b29885f09..4df98eb7d452aa38601fa328dfa6b3e4e9584989 100644
--- a/services/bitrise/bitrise.service.js
+++ b/services/bitrise/bitrise.service.js
@@ -1,15 +1,18 @@
 'use strict'
 
-const LegacyService = require('../legacy-service')
-const { makeBadgeData: getBadgeData } = require('../../lib/badge-data')
+const Joi = require('joi')
+const { BaseJsonService } = require('..')
 
-// This legacy service should be rewritten to use e.g. BaseJsonService.
-//
-// Tips for rewriting:
-// https://github.com/badges/shields/blob/master/doc/rewriting-services.md
-//
-// Do not base new services on this code.
-module.exports = class Bitrise extends LegacyService {
+// https://devcenter.bitrise.io/api/app-status-badge/
+const schema = Joi.object({
+  status: Joi.equal('success', 'error', 'unknown'),
+}).required()
+
+const queryParamSchema = Joi.object({
+  token: Joi.string().required(),
+}).required()
+
+module.exports = class Bitrise extends BaseJsonService {
   static get category() {
     return 'build'
   }
@@ -17,7 +20,8 @@ module.exports = class Bitrise extends LegacyService {
   static get route() {
     return {
       base: 'bitrise',
-      pattern: ':appId/:branch',
+      pattern: ':appId/:branch?',
+      queryParamSchema,
     }
   }
 
@@ -27,56 +31,51 @@ module.exports = class Bitrise extends LegacyService {
         title: 'Bitrise',
         namedParams: { appId: 'cde737473028420d', branch: 'master' },
         queryParams: { token: 'GCIdEzacE4GW32jLVrZb7A' },
-        staticPreview: {
-          label: 'bitrise',
-          message: 'success',
-          color: 'brightgreen',
-        },
+        staticPreview: this.render({ status: 'success' }),
       },
     ]
   }
 
-  static registerLegacyRouteHandler({ camp, cache }) {
-    camp.route(
-      /^\/bitrise\/([^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/,
-      cache({
-        queryParams: ['token'],
-        handler: (data, match, sendBadge, request) => {
-          const appId = match[1]
-          const branch = match[2]
-          const format = match[3]
-          const token = data.token
-          const badgeData = getBadgeData('bitrise', data)
-          let apiUrl = `https://app.bitrise.io/app/${appId}/status.json?token=${token}`
-          if (typeof branch !== 'undefined') {
-            apiUrl += `&branch=${branch}`
-          }
+  static get defaultBadgeData() {
+    return {
+      label: 'bitrise',
+    }
+  }
+
+  async fetch({ appId, branch, token }) {
+    return this._requestJson({
+      url: `https://app.bitrise.io/app/${encodeURIComponent(
+        appId
+      )}/status.json`,
+      options: { qs: { token, branch } },
+      schema,
+      errorMessages: {
+        403: 'app not found or invalid token',
+      },
+    })
+  }
 
-          const statusColorScheme = {
-            success: 'brightgreen',
-            error: 'red',
-            unknown: 'lightgrey',
-          }
+  static render({ status }) {
+    const color = {
+      success: 'brightgreen',
+      error: 'red',
+      unknown: 'lightgrey',
+    }[status]
 
-          request(apiUrl, { json: true }, (err, res, data) => {
-            try {
-              if (!res || err !== null || res.statusCode !== 200) {
-                badgeData.text[1] = 'inaccessible'
-                sendBadge(format, badgeData)
-                return
-              }
+    let message
+    if (status === 'unknown') {
+      // This is the only case mentioned in the API docs. If we get feedback
+      // it's often wrong we can update this.
+      message = 'branch not found'
+    } else {
+      message = status
+    }
 
-              badgeData.text[1] = data.status
-              badgeData.colorscheme = statusColorScheme[data.status]
+    return { message, color }
+  }
 
-              sendBadge(format, badgeData)
-            } catch (e) {
-              badgeData.text[1] = 'invalid'
-              sendBadge(format, badgeData)
-            }
-          })
-        },
-      })
-    )
+  async handle({ appId, branch }, { token }) {
+    const { status } = await this.fetch({ appId, branch, token })
+    return this.constructor.render({ status })
   }
 }
diff --git a/services/bitrise/bitrise.tester.js b/services/bitrise/bitrise.tester.js
index 24b357f82d41d310b13a5e0bcaf1d034ac016844..b31ba9f78ea99b6704a78d04bc8c95ddce607ca5 100644
--- a/services/bitrise/bitrise.tester.js
+++ b/services/bitrise/bitrise.tester.js
@@ -1,53 +1,30 @@
 'use strict'
 
-const Joi = require('joi')
-const { ServiceTester } = require('../tester')
-
-const t = (module.exports = new ServiceTester({
-  id: 'bitrise',
-  title: 'Bitrise',
-}))
+const { isBuildStatus } = require('../build-status')
+const t = (module.exports = require('../tester').createServiceTester())
 
 t.create('deploy status')
-  .get('/cde737473028420d/master.json?token=GCIdEzacE4GW32jLVrZb7A')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'bitrise',
-      value: Joi.equal('success', 'error', 'unknown'),
-    })
-  )
-
-t.create('deploy status without branch')
   .get('/cde737473028420d.json?token=GCIdEzacE4GW32jLVrZb7A')
-  .expectJSONTypes(
-    Joi.object().keys({
-      name: 'bitrise',
-      value: Joi.equal('success', 'error', 'unknown'),
-    })
-  )
+  .expectBadge({
+    label: 'bitrise',
+    message: isBuildStatus,
+  })
+
+t.create('deploy status with branch')
+  .get('/cde737473028420d/master.json?token=GCIdEzacE4GW32jLVrZb7A')
+  .expectBadge({
+    label: 'bitrise',
+    message: isBuildStatus,
+  })
 
 t.create('unknown branch')
   .get('/cde737473028420d/unknown.json?token=GCIdEzacE4GW32jLVrZb7A')
-  .expectJSON({ name: 'bitrise', value: 'unknown' })
+  .expectBadge({ label: 'bitrise', message: 'branch not found' })
 
 t.create('invalid token')
   .get('/cde737473028420d/unknown.json?token=token')
-  .expectJSON({ name: 'bitrise', value: 'inaccessible' })
+  .expectBadge({ label: 'bitrise', message: 'app not found or invalid token' })
 
 t.create('invalid App ID')
   .get('/invalid/master.json?token=GCIdEzacE4GW32jLVrZb7A')
-  .expectJSON({ name: 'bitrise', value: 'inaccessible' })
-
-t.create('server error')
-  .get('/AppID/branch.json?token=token')
-  .intercept(nock =>
-    nock('https://app.bitrise.io')
-      .get('/app/AppID/status.json?token=token&branch=branch')
-      .reply(500, 'Something went wrong')
-  )
-  .expectJSON({ name: 'bitrise', value: 'inaccessible' })
-
-t.create('connection error')
-  .get('/AppID/branch.json?token=token')
-  .networkOff()
-  .expectJSON({ name: 'bitrise', value: 'inaccessible' })
+  .expectBadge({ label: 'bitrise', message: 'app not found or invalid token' })