Bitbucket Cloud retired app passwords in favour of scoped API tokens. If a Jenkins multibranch pipeline authenticates to Bitbucket with an app password, it doesn’t fail loudly the day the deprecation lands - it fails quietly, and only in specific spots, which makes it confusing to diagnose. This is the exact failure, why it shows up where it does, and the fix.


Problem

A multibranch pipeline job that had been running fine for months suddenly failed on a manual “Build Now”:

com.cloudbees.jenkins.plugins.bitbucket.api.BitbucketRequestException: HTTP request error.
Status: Unauthorized HTTP 401
Response: {"type": "error", "error": {"message": "App passwords are deprecated and must be replaced with API tokens." ...}}
	at ...BitbucketCloudApiClient.getBranch(BitbucketCloudApiClient.java:381)
	at ...BitbucketSCMSource.retrieve(BitbucketSCMSource.java:639)
	at ...SCMSource.fetch(SCMSource.java:577)
	at ...SCMBinder.create(SCMBinder.java:104)
	at ...WorkflowRun.run(WorkflowRun.java:320)

The confusing part: webhook-triggered builds on the same job kept working. Only manual triggers failed.


Root cause

Why manual triggers fail but webhook builds don’t

A push webhook from Bitbucket carries the new commit SHA in its payload. Jenkins’ branch indexing consumes that and queues the build with the revision already resolved - the build never needs to call back to Bitbucket’s API to ask “what’s HEAD right now.”

A manual “Build Now” has no pre-resolved revision attached. So SCMBinder.create() - the lightweight “Pipeline script from SCM” step that fetches just the Jenkinsfile before running anything - has to call SCMSource.fetch()BitbucketSCMSource.retrieve()getBranch() to resolve the branch head itself. That live API call is what hits the dead credential.

This matters beyond manual builds: branch indexing scans (the periodic “Scan Multibranch Pipeline” that discovers new branches) call the same API with the same credential. If the credential is broken, those are failing too - just less visibly, since a failed scan doesn’t show up as a red build.

Two separate root causes, not one

Fixing this took two passes, because there are two independent things app passwords used to paper over:

  1. Basic Auth identity - Bitbucket’s REST API requires the Atlassian account email as the username when authenticating with a scoped API token. The Bitbucket handle (which worked fine with app passwords, and still works for plain git clone/git fetch over HTTPS) does not work for REST API calls.
  2. Token scopes - a scoped token only grants what you explicitly select. The legacy app password’s effective permissions (webhook, pullrequest, team) need to be mapped to the new scope model - missing scopes don’t always 403; some endpoints come back as a bare 401 with no explanation.

Steps to fix

1. Create a scoped API token

In Bitbucket: Personal settings → API tokens → Create API token with scopes. Select Bitbucket as the app, set an expiry, and grant:

  • read:repository:bitbucket, write:repository:bitbucket - git operations, branch listing, commit/build status
  • read:pullrequest:bitbucket, write:pullrequest:bitbucket - PR discovery and status
  • read:webhook:bitbucket, write:webhook:bitbucket - if Jenkins auto-manages the webhook
  • read:account, read:workspace:bitbucket - needed to enumerate repos under a workspace/owner; this is the closest equivalent to the legacy team scope, and the one most likely to be missing if you only mapped repository + pullrequest

Copy the token immediately - it’s shown once.

2. Validate with curl before touching Jenkins

This step saved several rounds of trial-and-error. Test the exact calls Jenkins makes, with the Atlassian email as username:

# Owner/workspace repo listing - powers the "Repository Name" dropdown in the job config
curl -u "[email protected]:ATATT...<token>" "https://api.bitbucket.org/2.0/repositories/<workspace>" -v

# Branch listing for the specific repo
curl -u "[email protected]:ATATT...<token>" "https://api.bitbucket.org/2.0/repositories/<workspace>/<repo>/refs/branches" -v

# Pull request listing
curl -u "[email protected]:ATATT...<token>" "https://api.bitbucket.org/2.0/repositories/<workspace>/<repo>/pullrequests" -v

A few gotchas hit along the way:

  • Using the Bitbucket username instead of the email gives a 401 with no useful body - easy to mistake for a token problem when it’s actually a username problem.
  • New tokens can take up to a minute to start working. A 401 immediately after creation isn’t necessarily wrong scopes - wait and retry before changing anything.
  • Don’t paste a live token into a chat log, ticket, or shared doc to “show someone the error” - if it happens, revoke and regenerate immediately rather than leaving it live.

Only move to Jenkins once all three calls return 200.

3. Update the Jenkins credential

Manage Jenkins → Credentials → find the credential bound to the job’s Branch Source (check Configure → Branch Sources → Credentials on the job to identify which one) → Update:

  • Username: the verified email
  • Password: the new token

Keep the same credential ID if it’s shared across multiple jobs - every job referencing it is fixed in one place, no per-job edits needed.

4. Re-verify

  • Reopen the job’s Branch Source config - the Repository Name field (an owner→repo lookup dropdown) should populate without a validation error. If it stays empty, the workspace-read scope is still missing.
  • Run Scan Multibranch Pipeline Now - confirms branch/PR/tag discovery works end to end.
  • Manually trigger a build - confirms the original 401 is gone.

Notes

  1. Don’t “fix” this by switching the Branch Source type from Bitbucket to plain Git. Plain git over HTTPS only needs the token to authenticate - it doesn’t touch the REST API, so it’ll work even with a partially-broken/under-scoped token. But you lose native PR and tag discovery: PR-derived branches just get dumped into the flat branch list with no PR awareness. It looks like a fix and isn’t one - revert to the Bitbucket source type once the actual credential is corrected.
  2. If the Branch Source’s tool config shows an “Edited” marker or was recently changed, check it - an unrelated config edit can coincide with the deprecation cutover and make the timeline confusing.
  3. A shared credential across many multibranch jobs is good news once you find it - one fix in Credentials propagates everywhere without touching individual job configs.
  4. Audit Manage Jenkins → Credentials for any other apppassword-type entries once you’re done - Bitbucket’s cutover affects every job using one, not just the one that happened to fail first.