---
name: bitbucket-oauth-app
description: Registers a Bitbucket Cloud OAuth 2.0 consumer to obtain OAuth2 credentials — the consumer Key and Secret, the single callback URL, and the permission checkboxes that become the token's scopes — for a platform that connects many customers' Bitbucket workspaces. Use when asked to get Bitbucket OAuth credentials, create or edit a Bitbucket OAuth consumer, choose between an OAuth consumer and a workspace/project/repository access token or an API token, migrate off deprecated Bitbucket app passwords, rotate a Bitbucket consumer secret, or fix a Bitbucket authorization error such as an invalid redirect, `invalid_scope`, `unauthorized_client`, or a token that dies after two hours. Covers Bitbucket **Cloud** only — Bitbucket Data Center registers OAuth clients inside the customer's own instance. For any other vendor's developer portal, use that vendor's skill instead.
---

# Bitbucket Cloud OAuth 2.0 Consumer Registration

Get a working Bitbucket Cloud OAuth2 client for a platform that connects many customers' Bitbucket workspaces. A
successful run produces: an OAuth consumer created inside a workspace you control, one callback URL per consumer,
a ticked permission set, and a **Key** and **Secret** pair.

**Read this first: Bitbucket Cloud is an Atlassian product, but its OAuth is not Atlassian 3LO.** There is no
developer console app, no `auth.atlassian.com`, no `audience` parameter, no `offline_access` scope, no cloudid, and
no `/ex/{product}/{cloudid}/…` addressing. The consumer is created in **workspace settings**, the endpoints are on
`bitbucket.org`, and the API is `https://api.bitbucket.org/2.0`. If you arrived here from an Atlassian-console
skill, put it down — nothing in it applies except the company name. See §1.

Four things are expensive to get wrong here, and three of them are invisible at registration time:

1. **Scopes are checkboxes on the consumer, not parameters on the authorize URL.** Consent is all-or-nothing: every
   customer grants the consumer's whole permission set, and changing the set changes what every future
   authorization grants (§6).
2. **One consumer takes one callback URL, and a `redirect_uri` sent in a request must sit *under* it.** Four
   callback hosts means four consumers, four Key/Secret pairs (§5).
3. **Access tokens are short — one to two hours — and refresh tokens rotate and idle out after three months.** A
   client that does not persist the rotated refresh token passes its smoke test and fails days later (§8).
4. **The consumer lives in a workspace, so every admin of that workspace can read its Secret** — and deleting the
   consumer instantly kills every customer token issued from it (§3, §4).

## Inputs to collect before you start

Ask in one batch. Never invent.

| Input | Notes |
| --- | --- |
| **Which workspace will own the consumer** | It is a workspace object, not a personal app (§3) |
| **Consumer name** and description | Name must be unique within the account; shown on the consent dialog |
| **Callback URL** — exactly one per consumer | Blocking. See §5 before promising anything |
| **Permission set** to tick | Becomes the scope set for every customer (§6) |
| **Private consumer or not** | Decides whether `client_credentials` is available (§9) |
| **Does the client persist rotated refresh tokens?** | Blocking — see §8 |
| **Is a user-delegated token actually the right credential?** | Access tokens and API tokens are real alternatives (§10) |
| **New consumer, or an edit to an existing one** | A new Key orphans every existing customer connection (§2) |
| **Cloud or Data Center** | This document is Cloud only |

## Quick Start

1. Confirm this is Bitbucket **Cloud**, and that an OAuth consumer — not an access token — is the right
   credential (§1, §10).
2. Confirm a **new** consumer is needed; an edit keeps every existing customer connected (§2).
3. Sign in as an admin of the workspace that should own the consumer (§3).
4. **Settings cog → Workspace settings → Apps and features → OAuth consumers → Add consumer** (§4).
5. Enter the name, description and the **single Callback URL**; decide the private-consumer setting (§4, §5).
6. Tick the **Permissions** the connector actually needs — this is the scope set (§6).
7. **Save**, then expand the consumer row to reveal the generated **Key** and **Secret** (§7).
8. Confirm the client stores the **rotated** refresh token on every refresh (§8).
9. Verify with a real authorize → callback → API read → double refresh on a different account (§12).
10. Hand the credentials over; never commit them (§13).

## Platform state (verified 2026-09-20 — re-verify before trusting)

- **Consumers are created in workspace settings**, not a developer console: avatar → pick the workspace →
  **Settings cog → Workspace settings → Apps and features → OAuth consumers → Add consumer**. Atlassian's support
  docs add: *"If you're giving access to a workspace, make sure you have administrative access."*
- **The endpoints are on `bitbucket.org`**: `https://bitbucket.org/site/oauth2/authorize` and
  `https://bitbucket.org/site/oauth2/access_token`. The API base is `https://api.bitbucket.org/2.0`.
- **As of May 4th, 2026, all OAuth 2.0 authenticated API requests must be directed at `https://api.bitbucket.org`**
  with the token as a Bearer token, per Atlassian's authentication reference. A client still calling the API
  through `bitbucket.org` is on borrowed time.
- **Three grant flows plus a custom JWT flow.** Authorization Code (4.1), Client Credentials (4.4), and the
  Bitbucket JWT grant `urn:bitbucket:oauth2:jwt` for Connect add-ons. **Resource Owner Password Credentials (4.3)
  is no longer supported.**
- **Implicit grant: the two Atlassian pages disagree.** The developer reference says *"Note that Implicit Grant
  (4.2) and Resource Owner Password Credentials Grant (4.3) are no longer supported"*; the support article still
  documents implicit grant as a working flow. The developer reference is the newer page (it carries the May 2026
  endpoint change). Do not build on implicit grant either way.
- **Access token lifetime: the same two pages disagree.** The developer reference says *"Our access tokens expire
  in one hour"*; the support article says *"Our access tokens expire in two hours."* **Read `expires_in` from the
  token response and never hardcode either number** (§8).
- **Refresh tokens rotate and idle out at three months.** *"Once a refresh token has been used to issue a new
  access token, a new refresh token will be issued as part of the response. The existing refresh token will expire
  shortly after and should not be used again."* And: *"A refresh token which is not used to generate an access
  token will expire after 3 months."*
- **The scope parameter is not how you ask for scopes.** *"Scopes are defined on the client/consumer instance.
  Bitbucket Cloud does not currently support the use of the optional scope parameter on the individual grant
  requests."* (§6).
- **App passwords are gone or going.** Atlassian's authentication reference now says flatly: *"App passwords are
  deprecated. Use API tokens."* Per the changelog, new app passwords could no longer be created from **9 September
  2025**, brownouts run from **9 June 2026**, and app passwords are **fully deprecated on 28 July 2026** — during
  brownouts, API requests fail with **401** and Git-over-HTTPS with **410**. **API tokens** are the replacement:
  Atlassian account → Security → *Create and manage API tokens* → *Create API token with scopes*, select Bitbucket
  as the app, **expiry required, maximum one year**, used as Basic auth (Atlassian email + token) or, since
  **18 August 2026**, as a Bearer token.
- **The native Bitbucket issue tracker is removed.** Per the changelog entry of **20 August 2026**, *"the API
  endpoints that support Issue Tracker are now fully removed."* The `issue` and `issue:write` scopes still exist in
  the scope list but no longer have endpoints behind them on Bitbucket's own tracker.
- **There is no cross-workspace `GET /workspaces` collection any more.** Atlassian's Workspaces reference lists
  `GET /user/workspaces` and `GET /workspaces/{workspace}`; the cross-workspace listing is deprecated. Anything
  resolving "which workspaces can this token see" must use `/user/workspaces`.
- **No review, approval, install cap or marketplace gate is documented for a plain OAuth consumer.** The only
  lifecycle operations Atlassian documents are create, reveal Key/Secret, and delete. That is genuinely simpler
  than most portals — and it also means nothing stops a customer's admin deleting the consumer (§4).
- **Rate limits are counted per user, not per app**, and the scaled-limit tier is explicitly *not* available to
  OAuth (§11).

If the workspace settings pages do not look like this, stop and report what you actually see rather than clicking on.

## 1. This is not the Atlassian Developer Console — confirm you are in the right place

A reader coming from Jira or Confluence will expect an Atlassian 3LO app. Bitbucket Cloud does not use one. The
differences are total, not cosmetic:

| | **Atlassian 3LO (Jira, Confluence)** | **Bitbucket Cloud OAuth 2.0** |
| --- | --- | --- |
| Where the app lives | Atlassian Developer Console, owned by an Atlassian account | **Workspace settings → OAuth consumers**, owned by a workspace |
| Authorize / token host | `auth.atlassian.com` | **`bitbucket.org/site/oauth2/…`** |
| API host | `api.atlassian.com/ex/{product}/{cloudid}` | **`api.bitbucket.org/2.0`** — no cloudid, no indirection |
| Asking for scopes | `scope` on the authorize URL | **Checkboxes on the consumer**; the request cannot widen them |
| Refresh token opt-in | `offline_access` scope | None — refresh tokens come with the authorization_code grant |
| Extra required authorize params | `audience`, `prompt=consent` | None documented |
| Two-legged app token | Not part of 3LO | **`client_credentials`**, for private consumers (§9) |

Two Atlassian app platforms *do* sit over Bitbucket and are not this: **Atlassian Connect** (heading for end of
support — the changelog has been steadily removing Connect APIs through 2026) and **Forge**, which has its own
scope family spelled `read:repository:bitbucket`, `write:pullrequest:bitbucket` and so on. Those Forge/API-token
scopes are a different list with different rules — Atlassian states that *"Forge app and API token scopes do not
implicitly grant access to other scopes, for example, write:repository:bitbucket does not implicitly grant access
to read:repository:bitbucket"*, which is the opposite of how OAuth 2.0 scopes behave here (§6). **Never copy a
scope string from one family into the other.**

**Bitbucket Data Center is a different product for this purpose.** It has its own OAuth 2.0 provider, configured
inside the customer's own instance, with endpoints on the customer's host. If the customer's URL is not
`bitbucket.org`, you are in the wrong document.

## 2. Reuse the existing consumer, or register a new one

A new consumer means a **new Key, and every stored token is bound to the old one** — every customer re-authorizes.
Worse, if you delete the old consumer to tidy up, you revoke them immediately (§4).

**Edit the existing consumer** for: changing the permission set (read §6 first — it is not free either), rotating
a compromised secret, correcting the callback URL, or diagnosing a failure.

**Register a new consumer** only when the user asks for one: a second callback host that the one-URL rule forces
into its own consumer (§5), a replacement for a compromised consumer, or a move to a different owning workspace.
Say which path you are taking before you start clicking.

## 3. Account, workspace, and who can read the secret

The consumer is **a workspace object**. That has three consequences worth stating out loud before you create it:

- **You need workspace admin rights** on the owning workspace to reach the OAuth consumers page at all.
- **Anyone who administers that workspace can read the Secret.** Atlassian's own instructions are: *"Toggle the
  consumer name to see the generated Key and Secret value for your consumer."* There is no show-once screen and no
  separate reveal permission. **Do not create the production consumer in a shared customer-facing workspace, or in
  an individual's personal workspace.** Use a workspace whose admin list is the set of people you would hand the
  secret to anyway, and treat the admin list as part of the credential's blast radius.
- **Anyone who administers that workspace can delete the consumer**, and Atlassian is explicit that deleting
  *"also removes all existing tokens linked to that consumer"* — i.e. instantly disconnects every customer.

A free Bitbucket account and workspace are enough to create a consumer and to test end-to-end. **Workspace access
tokens are a Premium feature** (§10) — if the plan matters, it matters there, not here.

Hand control back to the user for anything only a human can do: Atlassian account signup, email verification, 2SV,
accepting terms, creating a workspace. Do not retry a blocked step in a loop.

**If this session has no browser automation** — the usual case for a CLI or cloud run — do not pretend to click.
Give the user the exact ordered click path from §4 with the literal callback URL (§5) and the literal permission
ticks (§6), then continue once they report back with the Key.

## 4. Create the consumer

1. Select your avatar, then open the **workspace** that will own the consumer.
2. **Settings cog** (top navigation) → **Workspace settings**.
3. Sidebar → **Apps and features** → **OAuth consumers** → **Add consumer**.
4. Fill in the form:
   - **Name** — *"The display name for your consumer. This must be unique within your account. This is required."*
     It is what customers see on the grant dialog, so use the product name, not a ticket number.
   - **Description** — optional.
   - **Callback URL** — *"Required for OAuth 2.0 consumers."* One field. See §5; this is the field that decides
     your architecture.
   - **URL** — optional, informational ("where the curious can go to learn more").
   - **This is a private consumer** — a checkbox. Tick it for a server-side connector that keeps its secret on a
     server. Atlassian's Pipelines guide instructs *"Select the This is a private consumer checkbox"* as the
     prerequisite for the `client_credentials` flow (§9), and a consumer marked public cannot use that grant. The
     public setting is for a distributed application that **ships its credentials inside the app**, which a
     multi-tenant server-side connector is not. Atlassian's published docs describe this checkbox only in passing,
     so read the in-product help text next to it before deciding, and record what it said.
   - **Permissions** — the checkboxes that become the scope set (§6).
5. **Save.** The system generates the Key and Secret (§7).

**Deleting a consumer** (Workspace settings → OAuth consumers → the row's menu → Delete) *"also removes all
existing tokens linked to that consumer."* There is no soft delete and no grace period. If the reason for deleting
is a compromised credential, Atlassian points at its app security incident management guidance for Marketplace
Partners rather than a self-service rotation flow — so plan a rotation as a deploy-then-delete, never a
delete-then-panic.

## 5. The callback URL — you get one field, and it is a prefix

`redirect_uri` handling here is unusual and catches people who assume GitHub-style exact matching against a list.
Atlassian documents two rules:

- *"If you don't include the URL in the request we redirect to the callback URL in the consumer."*
- *"If you do include the URL in a request it must be appended to the same URL configured in the consumer. So if
  your consumer callback URL is `example.com/add-on` the URL in your request must be something similar to
  `example.com/add-on/function`."*

So the registered value is a **prefix**, and a per-request `redirect_uri` may only extend it with a deeper path. It
cannot change the host. That settles the multi-region question: **different callback hosts cannot share one
consumer.**

For Unified.to the callbacks are one per data center; confirm the current list with the platform owner rather than
assuming, because data centers get added:

```
https://api.unified.to/oauth/code          # us (default)
https://api-eu.unified.to/oauth/code       # eu
https://api-au.unified.to/oauth/code       # au
https://api-dev.unified.to/oauth/code      # dev
```

Four hosts, four consumers, four Key/Secret pairs — and the credential store must be able to hold them separately.
Say so explicitly in your handoff. (There is one alternative, and it is a platform change rather than a console
setting: a single callback host that fans out internally. Do not assume it exists.)

Other notes:

- Send `redirect_uri` explicitly on the authorize request anyway, and send the identical value on the token
  exchange. Relying on the consumer's default is what makes a region mix-up silent instead of loud.
- A mismatch surfaces at connect time, not at save time.
- The callback is your platform's URL. Nothing about it varies per customer or per workspace.

## 6. Permissions are checkboxes — consent is all-or-nothing

**This is the single biggest conceptual difference from every other portal in this collection.** Bitbucket states
it twice:

> *"Scopes are defined on the client/consumer instance. Bitbucket Cloud does not currently support the use of the
> optional scope parameter on the individual grant requests."*
>
> *"When the scope parameter is provided, Bitbucket will validate that it contains no scopes that were not already
> present on the client/consumer and fail if additional scopes are requested, but asking for fewer scopes will not
> affect the resulting access token."*

Read that second sentence carefully, because it has three separate consequences:

1. **Sending `scope` can only fail you, never help you.** A scope on the authorize URL that is not ticked on the
   consumer **fails the request**. A subset succeeds — and is ignored.
2. **Every token carries the consumer's entire permission set.** There is no per-customer, per-object or
   least-privilege authorization. A customer who only wants repository reads still grants everything ticked.
3. **Changing the checkboxes changes the deal for everyone.** Every subsequent authorization carries the new set,
   and a connector that adds a permission is asking all future customers for more. Treat a permission change as a
   customer-visible decision, not a config tweak — and re-run the consent dialog yourself to see what it now says.

The consumer form groups permissions per resource, with read/write ticks inside each group — Atlassian's Pipelines
walkthrough instructs *"Under Permissions, go to Repositories. Tick both Read and Write."* Its older support
walkthrough describes the coarse shape as **Email** (*"Permission to read your account's primary email address"*),
**Read**, **Write** and **Admin** across account, workspace membership and repositories. The live form is
finer-grained than that older page and maps onto the scope strings below; **read the actual checkboxes in the UI
and map them to the table** rather than trusting either description. Tick what the connector calls, and nothing
more.

### The scope strings, and what implies what

These are the OAuth 2.0 scope strings Bitbucket publishes. The implication rules are the non-obvious part: some
scopes grant others for free, and — importantly — **the admin scopes do not**.

| Scope | Grants | Implies |
| --- | --- | --- |
| `repository` | Read a repo: source, clone over HTTPS, file-browsing API, zip archives | — |
| `repository:write` | Push over HTTPS, fork | **implies `repository`** |
| `repository:admin` | Repo admin features only — deploy keys, permissions, branch permissions, delete the repo | **does *not* imply `repository` or `repository:write`** |
| `repository:delete` | Delete repositories | — |
| `pullrequest` | See, list and comment on PRs; create/resolve tasks | **implies `repository`** (read on the destination repo) |
| `pullrequest:write` | Create, merge, decline, approve PRs | **implies `pullrequest` and `repository:write`** |
| `project` | View projects | **implies `repository`** across the project's repos |
| `project:write` | **Deprecated**, superseded by `project:admin` | — |
| `project:admin` | Create, update, delete projects | **does *not* imply `project` or `repository:write`** |
| `issue` | View, create, comment, watch, vote on issues | nothing, and **no implicit repo access** |
| `issue:write` | Transition and delete issues | **implies `issue`**, still no repo access |
| `wiki` | Wikis, read *and* write — *"no distinction is made"* | nothing |
| `snippet` / `snippet:write` | Read / create, edit, delete snippets | `snippet:write` **implies `snippet`** |
| `webhook` | Any webhook operation; **reads existing subscriptions on anything the token can reach, without the resource's own scope** | — |
| `email` | The user's primary email address | — |
| `account` | Read account info; on workspace APIs, view users, permissions and projects | — |
| `account:write` | Change account properties — **including delete the authorizing user's account** | — |
| `pipeline`, `pipeline:write`, `pipeline:variable` | Read pipelines / control runs / create variables | — |
| `runner`, `runner:write` | Read / manage pipeline runners | — |

Two traps in that table:

- **The admin scopes are not supersets.** `repository:admin` gives you branch permissions and deletion but not the
  source; a connector that needs to read code *and* administer must tick both. Atlassian notes the sharp edge:
  `repository:admin` *"can be used or misused to grant read access to other users, who can then clone the repo"* —
  so it is a heavier ask than it looks on the dialog.
- **`webhook` is quietly broad on reads.** It *"gives read access to existing webhook subscriptions on all
  resources the authorization mechanism can access, without needing further scopes."* Creating a webhook, though,
  needs the resource scope too — `issue:created` needs `webhook` **and** `issue`.

### Scopes never exceed the user's own permissions

Atlassian puts it bluntly: *"Anything you could do when logged into Bitbucket, your application can do also. So for
example, if they have read/write access to all of the workspace, your application does as well."* That cuts both
ways, and both directions produce support tickets:

- A token with `repository` sees **only the repositories the authorizing user can see** — so two customers on the
  same consumer get wildly different views, and "the connector is missing repos" is usually an account-permissions
  question, not a scope question.
- Conversely there is **no repository picker**. Unlike an installation-style model, the customer cannot narrow the
  grant to three repositories. Whatever the user can reach, the consumer can reach. Customer security reviews
  notice this; have an answer ready, and note that **access tokens (§10) are the per-repository answer** when a
  customer insists.

## 7. Capture the credentials

From **Workspace settings → OAuth consumers**, expand the consumer row: *"Toggle the consumer name to see the
generated Key and Secret value for your consumer."*

- **Key** — the OAuth `client_id`.
- **Secret** — the OAuth `client_secret`. Re-readable by any workspace admin, forever (§3).
- **Authorization endpoint** — `https://bitbucket.org/site/oauth2/authorize`
- **Token endpoint** — `https://bitbucket.org/site/oauth2/access_token`
- **API base** — `https://api.bitbucket.org/2.0`, Bearer token in the `Authorization` header (required since
  4 May 2026).
- Which data center this consumer's single callback serves (§5).

The authorize request Atlassian documents is minimal:

```
https://bitbucket.org/site/oauth2/authorize?client_id={client_id}&response_type=code
```

No `audience`, no `prompt`, no `offline_access`. Add `state` — Bitbucket does not require it and does not document
it, but you need it, and you need to cope if it does not come back (see the symptom table in §12). The exchange
passes the client credentials as **HTTP Basic**, not in the body:

```
curl -X POST -u "client_id:secret" \
  https://bitbucket.org/site/oauth2/access_token \
  -d grant_type=authorization_code -d code={code}
```

**PKCE is not mentioned anywhere in Atlassian's Bitbucket Cloud OAuth documentation** — not in the developer
reference, not in the support article. Do not assume `code_challenge` is honoured, and do not rely on it as a
security control here; the confidential-client path (private consumer + Basic auth secret) is the documented one.

Rotating the Secret invalidates the old one for new exchanges, so deploy the new value before you rely on it.
Report the secret once so the user can paste it into their secret store, say plainly that it is now in the
transcript and can be rotated, then move on.

## 8. Token lifetimes and rotating refresh tokens

| Thing | Value | Source |
| --- | --- | --- |
| Access token lifetime | **One hour** per the developer reference, **two hours** per the support article — **read `expires_in`** | Both Atlassian pages, in conflict |
| Expired access token | 401 responses | Both |
| Refresh token issued | With `authorization_code` and with each `refresh_token` response | Developer reference |
| Refresh token behavior | **Rotating** — *"a new refresh token will be issued… The existing refresh token will expire shortly after and should not be used again"* | Developer reference |
| Unused refresh token | **Expires after 3 months**, then the user must redo the full authorization_code flow | Developer reference |
| Absolute maximum lifetime | Not documented | — |

The refresh call, with credentials again as Basic auth:

```
curl -X POST -u "client_id:secret" \
  https://bitbucket.org/site/oauth2/access_token \
  -d grant_type=refresh_token -d refresh_token={refresh_token}
```

**The failure mode that passes your smoke test.** A client that does not store the *new* refresh token refreshes
successfully the first time — it still holds a valid token — and fails the second time, hours later. So ask two
questions, and note a platform can answer yes to the first and no to the second:

1. Does the client read `refresh_token` out of the refresh response at all?
2. Does it **persist** it, atomically, before the next refresh, including under concurrent refreshes of the same
   connection?

Note the phrase *"will expire shortly after"* — the old token is not killed instantly, which gives a racing client
a small undocumented window of apparent success. Do not design around it.

The quieter killer is **idleness**: a connection nobody refreshes for three months is dead, and the customer must
re-authorize from scratch. With a one-to-two-hour access token, any live connector refreshes constantly — but
*paused*, *disabled* and *trial* connections do not, and those are exactly the ones that break on resumption.

## 9. `client_credentials` — the two-legged flow, and what it actually reaches

Bitbucket supports the Client Credentials grant (RFC 6749 §4.4):

```
curl -X POST -u "client_id:secret" \
  https://bitbucket.org/site/oauth2/access_token \
  -d grant_type=client_credentials
```

What it gives you is specific: a token that *"represents not an end user, but the owner of the client/consumer"* —
that is, the workspace the consumer lives in, as the consumer's owner sees it. Practically:

- **It is useless for multi-tenant customer access.** It cannot reach a customer's workspace; it reaches yours. It
  is the right tool for your own automation against your own workspace (CI pushing back to a repo is Atlassian's
  own worked example), and the wrong tool for a connector.
- **The consumer must be marked private.** A consumer marked public is rejected for this grant; Atlassian's
  worked example ticks *"This is a private consumer"* as a prerequisite.
- It inherits the same ceiling as everything else: the consumer's ticked permissions, no more.
- Atlassian's example passes `-d scopes="repository"` alongside the grant. Given §6, treat that as cosmetic — the
  consumer's checkboxes decide the token.

If someone proposes `client_credentials` as a way to avoid asking customers to authorize, that is a
misunderstanding worth correcting on the spot.

## 10. The alternatives: access tokens, API tokens, and the app-password removal

An OAuth consumer is not always the right credential. Bitbucket offers three non-OAuth options and one dead one,
and the differences are about *what the credential is attached to*:

- **Repository / Project / Workspace access tokens.** *"Access tokens are linked to a repository, project, or
  workspace, not a user account."* They survive the employee who created them leaving, they are the only way to
  hand a customer a genuinely repository-scoped credential, and they are the only credential that qualifies for
  scaled rate limits (§11). Key properties: created by an admin of that resource with a chosen scope set; **cannot
  be viewed or edited after creation** (replace, don't recover); can carry an expiry, and workspaces can be made
  to *require* one; can be **rotated** (refresh the secret and expiry without redefining scopes); appear in the UI
  as a pseudo-user named after the token; and are **deactivated when the resource is deleted** (repository tokens
  also die when the repo is transferred). Their scope lists are narrower than OAuth's — workspace tokens get
  `project`/`project:admin`, `repository*`, `pullrequest*`, `webhook`, `account`, `pipeline*`, `runner*`; repo
  tokens drop the project scopes. They cannot manage permissions, cannot log into the website, and a few deploy-key
  endpoints are closed to them. **Workspace access tokens are a Premium feature.**
- **API tokens** (Atlassian-account level): *"personal access tokens that users can create to authenticate with
  Bitbucket's REST APIs or interact with Git… designed as a long term replacement for app passwords."* Basic auth
  with the Atlassian email as username, or Bearer since 18 August 2026. **Expiry is mandatory, maximum one year**,
  scopes are chosen at creation and cannot be edited afterwards. Tied to an individual, so they inherit the
  offboarding problem OAuth also has.
- **App passwords: deprecated, and being removed.** No new ones since 9 September 2025; brownouts from 9 June 2026
  (401 on the API, 410 on Git-over-HTTPS); fully removed 28 July 2026. If a platform still offers "username + app
  password" as a connection method, that is a migration item with a deadline, not a supported alternative.

**Choose the OAuth consumer when** the platform must act *as each customer's user*, across whatever that user can
see, with a browser consent step — the normal multi-tenant connector. **Point a customer at an access token when**
they refuse a workspace-wide user-delegated grant and want the credential pinned to one repository or workspace,
or when what you are connecting is really CI automation rather than a person.

## 11. Rate limits — counted per user, and OAuth cannot buy more

Atlassian's request-limits page is explicit about the counter: *"Unauthenticated calls: measured against a specific
IP address. Authenticated calls: measured against the user ID."* So a connector's budget is **per authorizing
customer user**, which is friendlier than a per-app cap — until one customer runs a heavy sync.

| Limit | Value |
| --- | --- |
| Anonymous | **60 requests/hour** across all API resources |
| Repository data (`/2.0/repositories/*`) | **1,000–10,000 per hour** (1,000 default; scaled tier below) |
| Git operations (HTTPS and SSH) | 60,000/hour |
| Raw file downloads | 5,000/hour |
| Archive (.zip/.gz) downloads | 1,500 files/hour |
| Webhook list/add/remove | 1,000/hour |
| Application properties | 2,000/hour |
| Invitations | 100/minute |

The window is *"a one-hour rolling window"*, not a fixed reset.

**The scaled tier is closed to OAuth.** Qualifying for more than 1,000/hour on repository data requires a Standard
or Premium plan, 100+ paid users in the workspace, **and** that you *"must use workspace, project, repository
access tokens for authentication or make requests through programmatic access via a Forge app."* The formula is
`1,000 + (paid users − 100) × 10`, capped at **10,000/hour**. A user-delegated OAuth token does not qualify, at any
customer size. If a large customer is throttling, the answer is a change of credential type (§10), not a support
ticket — say that plainly rather than promising headroom you cannot get.

When the scaled limits apply, Bitbucket returns `X-RateLimit-Limit` (total permitted per hour — *not* remaining),
`X-RateLimit-Resource`, and `X-RateLimit-NearLimit` (boolean, true under 20% remaining). Those headers are only
documented for scaled-limit requests, so an OAuth client cannot count on seeing them and must handle throttling
from the status code alone.

## Product state — Unified.to (verified 2026-09-20; confirm with the connector's owner)

Dated observation of how the Bitbucket connector behaves today, not contract:

- **It is an OAuth 2.0 consumer integration** against `https://bitbucket.org/site/oauth2/authorize` and
  `.../access_token`, with the API at `https://api.bitbucket.org/2.0` — the correct hosts, including the post-May-2026
  API host requirement. It also offers a non-OAuth option taking an **email address plus an API token**, which is the
  right successor to app passwords (§10).
- **It sends `scope` on the authorize URL**, space-delimited, built per object type from a map: `repository`
  (+ `repository:write`, `repository:delete`) for repository, branch, commit and file objects; `pullrequest`
  (+ `pullrequest:write`); `issue`/`issue:write` for task objects; `project`/`project:admin`; and
  `account`/`account:write`/`email` for the people and organization objects. `account` and `email` are the login
  scopes. Given §6, this only matters in one direction: **any scope in that map that is not ticked on the consumer
  will fail the authorize request**, so the consumer must be ticked for the union of everything the workspace might
  enable — and every customer's token then carries that union regardless.
- **It exchanges the code with the client credentials as HTTP Basic**, which matches Atlassian's documented
  `-u "client_id:secret"` form, and uses the standard refresh grant. **No PKCE parameters are sent** — consistent
  with Atlassian documenting none (§7).
- **It flags Bitbucket as not returning `state`** on the callback and compensates internally. Worth knowing when
  debugging a connect flow, and worth confirming against current Bitbucket behaviour rather than assuming.
- **It picks the customer's workspace automatically at connect time by taking the first entry of a
  cross-workspace workspace listing.** Two problems, both real (§12): the cross-workspace collection endpoint is
  the deprecated one — the connector's own most recent read test recorded an error naming that deprecation — and
  "first workspace" is arbitrary for any customer who belongs to more than one. Flag both to the connector's owner.
- **Its task objects target the native Bitbucket issue tracker**, whose API endpoints Atlassian recorded as fully
  removed on 20 August 2026. Those objects, and the `issue`/`issue:write` scopes backing them, should be expected
  to fail; confirm before ticking those permissions on a new consumer.
- **Platform-shared OAuth credentials appear to be configured for this connector**, meaning workspaces may not each
  need their own Key and Secret — confirm which mode is live before registering anything, because it changes whether
  this run is needed at all.

## 12. Verify end-to-end

Authorizing as the workspace admin who owns the consumer proves almost nothing — that account can see everything
anyway. Test the path a customer takes.

1. Run a real **authorize → callback** round trip through the platform's connect flow, from a **different**
   Bitbucket account in a **different** workspace, sending `redirect_uri` explicitly.
2. Read the **consent dialog** as that customer would and check it against §6 — it shows the consumer's whole
   permission set, and that is what you are actually asking every customer for.
3. Confirm the token response's **`expires_in`** and that a **`refresh_token`** is present.
4. Make one real read against **`https://api.bitbucket.org/2.0/…`** with a Bearer header.
5. Check what the token can *see*: list the user's workspaces via `/user/workspaces` and confirm the connector
   picks the intended one rather than the first (§10, Product state).
6. Force a **refresh**, then force a **second refresh using the token the first returned**. This is the step that
   catches a client ignoring rotation, and it is the step most often skipped.
7. If the consumer will serve more than one region, repeat against each region's consumer — they are separate
   credentials with separate callbacks (§5).

| Symptom | Cause |
| --- | --- |
| Authorize request rejected over scopes | A scope in the request is not ticked on the consumer — the request cannot widen the set (§6) |
| Requested fewer scopes, token came back with more | Expected: *"asking for fewer scopes will not affect the resulting access token"* (§6) |
| Redirect rejected / lands somewhere unexpected | `redirect_uri` is not the registered callback or a deeper path under it; a different host never matches (§5) |
| EU/AU customers land on the US callback | Wrong consumer's Key in play, or `redirect_uri` omitted so the consumer default was used (§5) |
| `unauthorized_client` / invalid client credentials | Wrong Key/Secret pair, or credentials sent in the body instead of HTTP Basic; check for stray whitespace or newlines in the stored values (§7) |
| `client_credentials` rejected | The consumer is not marked private (§9) |
| Everything 401s after about an hour | Normal expiry — read `expires_in`, do not hardcode (§8) |
| First refresh works, later ones fail | Rotated refresh token not persisted (§8) |
| Connection dead after a quiet few months | Three-month idle refresh-token expiry; the customer must re-authorize (§8) |
| Customer sees fewer repos than expected | The token is bounded by the authorizing user's own access — a permissions question, not a scope one (§6) |
| Customer wants only some repositories connected | Not possible with an OAuth consumer; access tokens are the per-resource credential (§10) |
| Repo admin calls fail although `repository:admin` is ticked | `repository:admin` does not imply `repository`/`repository:write` — tick them too (§6) |
| Webhook creation fails although `webhook` is ticked | Creating a subscription also needs the resource's own scope (§6) |
| Issue-tracker calls 404 | The native issue tracker API was removed on 20 Aug 2026 (Platform state) |
| Cross-workspace workspace listing errors | The cross-workspace collection is deprecated; use `/user/workspaces` (Platform state) |
| Large customer constantly throttled | OAuth cannot reach the scaled tier — it needs an access token or Forge (§11) |
| Auth suddenly broke for every customer at once | The consumer was deleted (revokes all its tokens) or the Secret was rotated without a deploy (§4, §7) |
| Git/API auth broke for a customer using "username + app password" | App-password brownout/removal — 401 on API, 410 on Git (Platform state) |
| Nothing resembles this document; you are on `auth.atlassian.com` | That is Atlassian 3LO, not Bitbucket Cloud (§1) |
| The instance is not `bitbucket.org` | Bitbucket Data Center — different product, different OAuth provider (§1) |

## 13. Hand off — never commit the secret

- **Do not** write the consumer Secret into source control, a test, a fixture, a committed `.env`, a ticket, a PR
  body, or a chat channel. Values go to the user, for the secret store or the console.
- Remember the Secret is **permanently re-readable by every admin of the owning workspace** (§3). If that admin
  list is wrong, fixing it is part of this job, not a follow-up.
- If a code change is needed (a callback host, a permission, workspace selection), keep it credential-free and say
  what the human must set out of band.
- Close with: consumer name and the **owning workspace**; the **Key**; where the Secret was delivered; the
  authorize/token endpoints and API base; the single callback URL and which data center it serves, plus how many
  further consumers the remaining regions need (§5); the exact permission set ticked and the fact that every
  customer grants all of it (§6); whether the consumer is marked private; the observed `expires_in`; the state of
  rotated-refresh-token persistence and who owns any remaining work; and anything left for the user to do.

## Stop and ask

Hand back to a human rather than guessing when: the platform needs more callback hosts than one consumer allows
and nobody has decided how many consumers to create (§5); the workspace that would own the consumer has an admin
list broader than the people who should hold the secret (§3); a permission change would alter what every future
customer grants (§6); the client does not persist rotated refresh tokens — report it, do not register and hope
(§8); someone proposes `client_credentials` as a way to reach customers' workspaces (§9); a customer's security
review requires per-repository scoping that an OAuth consumer cannot give (§6, §10); a customer is still on app
passwords and the removal date is close (§10); a large customer needs rate-limit headroom OAuth cannot reach
(§11); the target is Bitbucket Data Center rather than Cloud (§1); or the workspace settings pages do not match
the **Platform state** section above.

**No browser automation:** if the session cannot drive a browser, do not simulate clicks or claim a step was done.
Produce the exact click path from §4 and the literal values for the user, and resume when they report back.

## References

Atlassian-owned pages only. Every link verified to return HTTP 200 on 2026-09-20.

- Authentication methods — OAuth 2.0, grants, token and refresh lifetimes, access tokens, API tokens, the full
  scope list — https://developer.atlassian.com/cloud/bitbucket/oauth-2/
- Bitbucket Cloud REST API scopes (per-scope detail and implication rules) — https://developer.atlassian.com/cloud/bitbucket/bitbucket-cloud-rest-api-scopes/
- The Bitbucket Cloud REST API (intro, base URL) — https://developer.atlassian.com/cloud/bitbucket/rest/intro/
- Bitbucket Cloud changelog (app-password brownouts, issue-tracker removal, Bearer for API tokens) — https://developer.atlassian.com/cloud/bitbucket/changelog/
- Workspaces REST reference (`/user/workspaces`, `/workspaces/{workspace}`) — https://developer.atlassian.com/cloud/bitbucket/rest/api-group-workspaces/
- Use OAuth on Bitbucket Cloud (create a consumer, callback rules, delete a consumer) — https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/
- Integrate another application through OAuth (the click path, the Permissions checkboxes) — https://support.atlassian.com/bitbucket-cloud/docs/integrate-another-application-through-oauth/
- OAuth consumer examples — https://support.atlassian.com/bitbucket-cloud/docs/oauth-consumer-examples/
- Push back to your repository (private-consumer checkbox, `client_credentials` worked example) — https://support.atlassian.com/bitbucket-cloud/docs/push-back-to-your-repository/
- API request limits (per-user counting, scaled tier, rate-limit headers) — https://support.atlassian.com/bitbucket-cloud/docs/api-request-limits/
- Access tokens for a workspace (Premium; features and limitations) — https://support.atlassian.com/bitbucket-cloud/docs/workspace-access-tokens/
- Access tokens for a project — https://support.atlassian.com/bitbucket-cloud/docs/project-access-tokens/
- Access tokens for a repository — https://support.atlassian.com/bitbucket-cloud/docs/repository-access-tokens/
- Rotate an access token for a workspace — https://support.atlassian.com/bitbucket-cloud/docs/rotate-an-access-token-for-a-workspace/
- Require access token expiry — https://support.atlassian.com/bitbucket-cloud/docs/require-access-token-expiry/
- API tokens — https://support.atlassian.com/bitbucket-cloud/docs/api-tokens/
- Bitbucket Cloud transitions to API tokens (app-password deprecation rationale and phases) — https://www.atlassian.com/blog/bitbucket/bitbucket-cloud-transitions-to-api-tokens-enhancing-security-with-app-password-deprecation
- 'Invalid OAuth client credentials' troubleshooting — https://support.atlassian.com/bitbucket-cloud/kb/invalid-oauth-client-credentials-while-setting-up-autoscaler-for-runners-on-kubernetes/
- Grant access to a workspace (workspace membership, groups, and Read/Write/Admin permissions) — https://support.atlassian.com/bitbucket-cloud/docs/grant-access-to-a-workspace/
