diff --git a/services/bitbucket/bitbucket-last-commit.service.js b/services/bitbucket/bitbucket-last-commit.service.js
new file mode 100644
index 0000000000000000000000000000000000000000..ca6f4c241b5d1154de08804481b918b070427d8e
--- /dev/null
+++ b/services/bitbucket/bitbucket-last-commit.service.js
@@ -0,0 +1,81 @@
+import Joi from 'joi'
+import { age as ageColor } from '../color-formatters.js'
+import { BaseJsonService, NotFound, pathParam, queryParam } from '../index.js'
+import { formatDate } from '../text-formatters.js'
+import { relativeUri } from '../validators.js'
+
+const schema = Joi.object({
+  values: Joi.array().items({
+    date: Joi.string().isoDate().required(),
+  }),
+}).required()
+
+const queryParamSchema = Joi.object({
+  path: relativeUri,
+}).required()
+
+export default class BitbucketLastCommit extends BaseJsonService {
+  static category = 'activity'
+  static route = {
+    base: 'bitbucket/last-commit',
+    pattern: ':user/:repo/:branch+',
+    queryParamSchema,
+  }
+
+  static openApi = {
+    '/bitbucket/last-commit/{user}/{repo}/{branch}': {
+      get: {
+        summary: 'Bitbucket last commit',
+        parameters: [
+          pathParam({ name: 'user', example: 'shields-io' }),
+          pathParam({ name: 'repo', example: 'test-repo' }),
+          pathParam({ name: 'branch', example: 'main' }),
+          queryParam({
+            name: 'path',
+            example: 'README.md',
+            schema: { type: 'string' },
+            description: 'File path to resolve the last commit for.',
+          }),
+        ],
+      },
+    },
+  }
+
+  static defaultBadgeData = { label: 'last commit' }
+
+  static render({ commitDate }) {
+    return {
+      message: formatDate(commitDate),
+      color: ageColor(Date.parse(commitDate)),
+    }
+  }
+
+  async fetch({ user, repo, branch, path }) {
+    // https://developer.atlassian.com/cloud/bitbucket/rest/api-group-commits/#api-repositories-workspace-repo-slug-commits-get
+    return this._requestJson({
+      url: `https://bitbucket.org/api/2.0/repositories/${user}/${repo}/commits/${branch}`,
+      options: {
+        searchParams: {
+          path,
+          pagelen: 1,
+          fields: 'values.date',
+        },
+      },
+      schema,
+      httpErrors: {
+        403: 'private repo',
+        404: 'user, repo or branch not found',
+      },
+    })
+  }
+
+  async handle({ user, repo, branch }, queryParams) {
+    const { path } = queryParams
+    const data = await this.fetch({ user, repo, branch, path })
+    const [commit] = data.values
+
+    if (!commit) throw new NotFound({ prettyMessage: 'no commits found' })
+
+    return this.constructor.render({ commitDate: commit.date })
+  }
+}
diff --git a/services/bitbucket/bitbucket-last-commit.tester.js b/services/bitbucket/bitbucket-last-commit.tester.js
new file mode 100644
index 0000000000000000000000000000000000000000..8c75524173f45187252c55e9bd50e697b236b5dd
--- /dev/null
+++ b/services/bitbucket/bitbucket-last-commit.tester.js
@@ -0,0 +1,41 @@
+import { isFormattedDate } from '../test-validators.js'
+import { ServiceTester } from '../tester.js'
+
+export const t = new ServiceTester({
+  id: 'BitbucketLastCommit',
+  title: 'Bitbucket last commit',
+  pathPrefix: '/bitbucket/last-commit',
+})
+
+t.create('last commit')
+  .get('/shields-io/test-repo/main.json')
+  .expectBadge({ label: 'last commit', message: isFormattedDate })
+
+t.create('last commit (path)')
+  .get('/shields-io/test-repo/main.json?path=README.md')
+  .expectBadge({ label: 'last commit', message: isFormattedDate })
+
+t.create('last commit (user not found)')
+  .get('/not-a-user/test-repo/main.json')
+  .expectBadge({
+    label: 'last commit',
+    message: 'user, repo or branch not found',
+  })
+
+t.create('last commit (repo not found)')
+  .get('/shields-io/not-a-repo/main.json')
+  .expectBadge({
+    label: 'last commit',
+    message: 'user, repo or branch not found',
+  })
+
+t.create('last commit (branch not found)')
+  .get('/shields-io/test-repo/not-a-branch.json')
+  .expectBadge({
+    label: 'last commit',
+    message: 'user, repo or branch not found',
+  })
+
+t.create('last commit (path not found)')
+  .get('/shields-io/test-repo/main.json?path=not/a/dir')
+  .expectBadge({ label: 'last commit', message: 'no commits found' })