Forums

Articles
Create
cancel
Showing results for 
Search instead for 
Did you mean: 

Getting 401 error when accessing bitbucket api

wailoktam July 22, 2025

I want to retrieve all pull request ids from a repository but I get stuck with log in.

I have read the following

Getting 401 error for a valid Bitbucket user account, when trying to access API from Jira cloud

Not sure what account I get. I dont know the distinction between service account and user account. I signed up with bitbucket ages ago . 

Below is my code, copied from answer of AI. 

 

import requests
import os
from typing import List, Optional, Dict, Any


def get_all_pr_ids(
workspace: str,
repo_slug: str,
username: str,
app_password: str,
state: Optional[str] = None # 'OPEN', 'MERGED', 'DECLINED', 'SUPERSEDED'
) -> Optional[List[int]]:
"""
Retrieves the IDs of all pull requests in a Bitbucket Cloud repository.

Args:
workspace (str): The Bitbucket workspace ID (e.g., 'your-company-workspace').
repo_slug (str): The repository slug (e.g., 'my-awesome-repo').
username (str): Your Bitbucket username (email or username).
app_password (str): Your Bitbucket app password with 'pullrequests:read' permission.
state (Optional[str]): Filter PRs by state. Can be 'OPEN', 'MERGED', 'DECLINED', 'SUPERSEDED'.
If None, retrieves all PRs regardless of state.

Returns:
Optional[List[int]]: A list of pull request IDs, or None if an error occurs.
"""
base_url = f"https://api.bitbucket.org/2.0/repositories/{workspace}/{repo_slug}/pull-requests"
# base_url = f"https://bitbucket.org/{workspace}/{repo_slug}/pull-requests"
params: Dict[str, Any] = {'pagelen': 100} # Fetch 100 PRs per page (max is 100)
if state:
params['state'] = state.upper() # Ensure state is uppercase for API

all_pr_ids: List[int] = []
next_page_url: Optional[str] = base_url

while next_page_url:
print(f"Fetching PRs from: {next_page_url}")
try:
response = requests.get(next_page_url, auth=(username, app_password),
params=params if next_page_url == base_url else None)
response.raise_for_status() # Raise an HTTPError for bad responses (4xx or 5xx)

data = response.json()

for pr in data.get('values', []):
all_pr_ids.append(pr['id'])

next_page_url = data.get('next') # Get URL for the next page, if any

# For subsequent pages, the 'next' URL already contains the necessary parameters,
# so we don't need to pass 'params' again. Reset params for the loop.
params = {}

except requests.exceptions.HTTPError as e:
print(f"HTTP error occurred: {e}")
print(f"Response content: {e.response.text}")
if e.response.status_code == 404:
print(f"Repository {workspace}/{repo_slug} not found or no pull requests.")
elif e.response.status_code == 401:
print("Authentication failed. Check your username and app password.")
return None
except requests.exceptions.ConnectionError as e:
print(f"Connection error occurred: {e}")
return None
except requests.exceptions.Timeout as e:
print(f"Timeout error occurred: {e}")
return None
except requests.exceptions.RequestException as e:
print(f"An unexpected error occurred: {e}")
return None
except Exception as e:
print(f"An unexpected error occurred during JSON parsing or data access: {e}")
return None

return all_pr_ids


if __name__ == "__main__":
# --- Configuration (Replace with your actual details) ---
# It's highly recommended to use environment variables for sensitive information
# rather than hardcoding them directly in the script.

BITBUCKET_USERNAME = os.getenv("BITBUCKET_USERNAME", "correctid")
BITBUCKET_APP_PASSWORD = os.getenv("BITBUCKET_APP_PASSWORD", "correctpw")
BITBUCKET_WORKSPACE = os.getenv("BITBUCKET_WORKSPACE", "correctworkspace") # e.g., 'my-company'
BITBUCKET_REPO_SLUG = os.getenv("BITBUCKET_REPO_SLUG", "correctrepo") # e.g., 'project-x'

# Optional: Filter by PR state. Uncomment and set to one of:
# 'OPEN', 'MERGED', 'DECLINED', 'SUPERSEDED'
PR_STATE_FILTER: Optional[str] = None # Set to 'OPEN' to get only open PRs, etc.

# --- Check if default values are still present ---
if "your_bitbucket_username" in BITBUCKET_USERNAME or \
"your_bitbucket_app_password" in BITBUCKET_APP_PASSWORD or \
"your-workspace-id" in BITBUCKET_WORKSPACE or \
"your-repository-slug" in BITBUCKET_REPO_SLUG:
print(
"WARNING: Please update the placeholder values for BITBUCKET_USERNAME, BITBUCKET_APP_PASSWORD, BITBUCKET_WORKSPACE, and BITBUCKET_REPO_SLUG.")
print("You can set them as environment variables or directly in the script for testing.")
# For a real scenario, you'd stop here.
# exit(1) # Uncomment this to force exit if not configured

print(f"Retrieving all pull request IDs for {BITBUCKET_WORKSPACE}/{BITBUCKET_REPO_SLUG}...")
if PR_STATE_FILTER:
print(f"Filtering by state: {PR_STATE_FILTER}")

pr_ids = get_all_pr_ids(
workspace=BITBUCKET_WORKSPACE,
repo_slug=BITBUCKET_REPO_SLUG,
username=BITBUCKET_USERNAME,
app_password=BITBUCKET_APP_PASSWORD,
state=PR_STATE_FILTER
)

if pr_ids is not None:
if pr_ids:
print(f"\nFound {len(pr_ids)} pull request(s):")
for pr_id in pr_ids:
print(f"- PR ID: {pr_id}")
else:
print("\nNo pull requests found for the specified criteria in this repository.")
else:
print("\nFailed to retrieve pull request IDs due to an error.")

The original base_url ends with pullrequests without a hyphen. No difference whether I add back a hypen or not

 

curl -u correctid:correctpw https://api.bitbucket.org/2.0/repositories/correctsworkpace

gives nothing back. 

 

Can anyone tell me what is wrong?

2 answers

0 votes
Theodora Boudale
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
July 23, 2025

Hi @wailoktam and welcome to the community!

While basic authentication with username and app password still works for our APIs, we are deprecating app app passwords and replacing them with API tokens:

  • On September 9, 2025, the creation of app passwords will be discontinued.
  • On June 9, 2026, any existing app passwords will become inactive.

So, if you want to use Basic authentication, I recommend switching to email and API token (email instead of username and API token instead of app password).

You can then check if they work by running:

curl -u {email}:{API_TOKEN} --request GET \
--url 'https://api.bitbucket.org/2.0/repositories/{workspace_id}/{repo_slug}/pullrequests' \
--header 'Accept: application/json'

Replace the placeholders (including curly brackets) with your values.

If the API call with curl succeeds and returns what you expect, then you'll need to look into your script.

If there's an issue with the curl command, please add the -v option to the curl command for verbose output, and let me know what status code the command returns and any errors.

The service account mentioned in the post you linked is just a term people use for a Bitbucket account they create only for CI/CD purposes. It's not a separate type of account.

Kind regards,
Theodora

wailoktam August 6, 2025

Hi, the curl command does not return anything after pressing enter. Any clue?

Theodora Boudale
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
August 8, 2025

Hi,

As a first step, I suggest adding the -v argument to the curl command in order to find out the status code. You can add this right before the user details, so the request will look like this:

curl -v -u {email}:{API_TOKEN} --request GET \
--url 'https://api.bitbucket.org/2.0/repositories/{workspace_id}/{repo_slug}/pullrequests' \
--header 'Accept: application/json'

Then, look at the output of the command. Right after the line "Request completely sent off" in the output, there should be a status code, something like in the following two examples:

HTTP/2 401
HTTP/2 200

What do you see in the output of your call?

Additionally, can you please let me know if the repository that you run this call for has any open (not merged or declined) pull requests?

Kind regards,
Theodora

wailoktam August 12, 2025

Thanks. I am getting a 401 error after adding the v switch

* Using Stream ID: 1 (easy handle 0x55a609253e80)
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
> GET /2.0/repositories/wailoktam/jobSearchDemo/pullrequests HTTP/2
> Host: api.bitbucket.org
> authorization: Basic <redacted>
> user-agent: curl/7.81.0
> accept: application/json
>
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* Connection state changed (MAX_CONCURRENT_STREAMS == 64)!
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
< HTTP/2 401

Parts of the command and message that worries me:

1. > authorization: Basic

Does the basic mean using id/pw?

2. 

{workspace_id}

Is workspace what is shown under recent workspaces when clicking the top right icon?

3.

{repo_slug}

Is it just the name of the repository?

Theodora Boudale
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
August 12, 2025

Hi,

Thank you for the info.

Before I go into the details, I would like to ask that you please revoke the API token that you used to make this call, because you accidentally exposed it in the output. The string after > authorization: Basic is a base64 string of the credentials you used in the API call. If you ever share output publicly, please ensure that this string is redacted.

 

1. > authorization: Basic

Does the basic mean using id/pw?

Basic authorization with Bitbucket Cloud APIs can be either username and app password, or Atlassian account email and API token.

  • Please ensure that you are using your Atlassian account email address that is listed here: https://bitbucket.org/account/settings/email/ after the text "Your Atlassian account address is:". Do not use any of the aliases listed below.

  • Double check the email for typos. You can copy-paste the Atlassian account email from this page I shared, to ensure it is correct.

  • Create a new API token that has at least the scope read:pullrequest:bitbucket.

  • When substituting the values in {email}:{API_TOKEN}, do not use curly braces around. This should look like email@example.com:ABCDEFG, and and NOT {email@example.com}:{ABCDEFG}.

{workspace_id}

Is workspace what is shown under recent workspaces when clicking the top right icon?

Not necessarily. The Recent workspaces show workspace names, and not IDs. The name may be the same as the ID, but it may also be different.

Select the workspace from the list, and look at the URL on your browser's address bar. It should look like this:

https://bitbucket.org/workspace-id/workspace/overview/

The part workspace-id in this URL is the workspace's ID.

Do not enclose it in curly braces in the url of the API call.

 

{repo_slug}

Is it just the name of the repository?

Not necessarily. In many cases it's the same as the name, but if the repo name has spaces or special characters like $, @, etc, then the repo slug is a URL-friendly version of the repository name and these special characters are replaced.

You can find out the repo slug if you open the repository on Bitbucket Cloud's website, and then look at the URL on your browser's address bar. If the landing page is the Source page, the URL will look like this:

https://bitbucket.org/workspace-id/repo-slug/src/main/

repo-slug in the URL is the repo's slug.

Do not enclose it in curly braces in the url of the API call.


Please also keep in mind that by default, only open pull requests are returned.

If you want to retrieve pull requests in other states, you can use the state query parameter and repeat it for each individual state. Valid values for state are OPEN, MERGED, DECLINED, SUPERSEDED. This info is mentioned in the documentation of this endpoint as well: https://developer.atlassian.com/cloud/bitbucket/rest/api-group-pullrequests/#api-repositories-workspace-repo-slug-pullrequests-get

Please feel free to let me know if you have any questions.

Kind regards,
Theodora

wailoktam August 13, 2025

Hi, I am getting this error message

 

{"type": "error", "error": {"message": "Token is invalid, expired, or not supported for this endpoint."}}

 

Before getting this error, I do the following based on your advice. 

1. I find a typo in my email and fix it

2. I change the token following your advice . I did not choose a token with scope. I suppose it means it has the largest scope and can do everything. 

3. I check the namespace using the url method you mention and it is correct

4. I check the repo-slug using the url method but this time I notice it becomes all lower case and I so I change it to all lower case. 

Theodora Boudale
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
August 13, 2025

Hi,

2. I change the token following your advice . I did not choose a token with scope. I suppose it means it has the largest scope and can do everything. 

A token without a scope is not going to work with Bitbucket Cloud APIs or Git commands.

You need to create a token with scopes, and for this specific API endpoint the token needs to have at least the scope read:pullrequest:bitbucket.

Can you please try this, and let me know if the call works?

Kind regards,
Theodora

0 votes
Aron Gombas _Midori_
Community Champion
July 23, 2025

It seems that you are using BASIC auth.

Not sure, but I think it will not work with your email address and password. It should work with your email address and a valid API token though!

wailoktam July 28, 2025

Thanks I create an api token but it is still not working, giving me a 401 error. There is no where for me to input my email address. I can only use my username since it is part of the url for getting the pull requests.

 

import requests
import os
from typing import List, Dict, Optional

def list_pull_requests(
bitbucket_url: str,
repository_owner: str,
repository_slug: str,
bitbucket_api_token: str
) -> Optional[List[Dict]]:
"""
Lists all pull requests in a Bitbucket repository and provides their IDs
using the Bitbucket REST API with an API token (Bearer Token authentication).

Args:
bitbucket_url (str): The base URL of your Bitbucket instance (e.g., "https://api.bitbucket.org/2.0" for Cloud,
or your self-hosted URL like "https://your-bitbucket-server.com").
repository_owner (str): The owner of the repository (e.g., "my_team" for Cloud, or project key for Server).
repository_slug (str): The slug of the repository (e.g., "my_repo").
bitbucket_api_token (str): Your Bitbucket API token (Bearer token).

Returns:
Optional[List[Dict]]: A list of dictionaries, where each dictionary represents a pull request
and contains at least 'id', 'title', and 'state'.
Returns None if an error occurs.
"""

# Determine the API endpoint based on Bitbucket Cloud or Server
if "api.bitbucket.org" in bitbucket_url:
# Bitbucket Cloud API
# https://developer.atlassian.com/cloud/bitbucket/rest/api-group-pullrequests/#api-repositories-workspace-repo-slug-pullrequests-get
url = f"{bitbucket_url}/repositories/{repository_owner}/{repository_slug}/pullrequests"
else:
# Bitbucket Server/Data Center API
# https://developer.atlassian.com/server/bitbucket/rest/v800/api-group-pull-requests/#api-api-latest-projects-projectkey-repos-repositoryslug-pullrequests-get
url = f"{bitbucket_url}/rest/api/latest/projects/{repository_owner}/repos/{repository_slug}/pull-requests"
print(f"Using Bitbucket Server/Data Center endpoint: {url}")

headers = {
"Authorization": f"Bearer {bitbucket_api_token}",
"Accept": "application/json"
}

all_pull_requests = []
next_page_url = url

while next_page_url:
try:
response = requests.get(next_page_url, headers=headers)
response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)

data = response.json()

if "api.bitbucket.org" in bitbucket_url:
# Bitbucket Cloud response structure
pull_requests_on_page = data.get('values', [])
for pr in pull_requests_on_page:
all_pull_requests.append({
'id': pr.get('id'),
'title': pr.get('title'),
'state': pr.get('state'), # e.g., "OPEN", "MERGED", "DECLINED"
'url': pr.get('links', {}).get('html', {}).get('href')
})
next_page_url = data.get('next') # URL for the next page
else:
# Bitbucket Server/Data Center response structure
pull_requests_on_page = data.get('values', [])
for pr in pull_requests_on_page:
all_pull_requests.append({
'id': pr.get('id'),
'title': pr.get('title'),
'state': pr.get('state'), # e.g., "OPEN", "MERGED", "DECLINED"
'url': pr.get('links', {}).get('self', [])[0].get('href') if pr.get('links') else None
})
# Bitbucket Server uses 'isLastPage' and 'nextPageStart' for pagination
if data.get('isLastPage'):
next_page_url = None
else:
# Append start parameter for the next page
next_page_url = f"{url}?start={data.get('nextPageStart')}"
# Preserve existing query parameters if any (though unlikely for this base URL)
if '?' in url and not url.endswith('?'):
next_page_url = f"{url}&start={data.get('nextPageStart')}"


except requests.exceptions.RequestException as e:
print(f"Error listing pull requests: {e}")
if response.status_code == 404:
print(f"Repository '{repository_owner}/{repository_slug}' or the endpoint might not exist.")
return None # Indicate an error occurred

return all_pull_requests


if __name__ == '__main__':
# --- Configuration ---
# Use environment variables for security.
# For Bitbucket Cloud: BITBUCKET_URL = "https://api.bitbucket.org/2.0"
# For Bitbucket Server/Data Center: BITBUCKET_URL = "https://your-bitbucket-server.com"
# BITBUCKET_URL = os.environ.get("BITBUCKET_URL", "https://api.bitbucket.org/2.0")
BITBUCKET_URL = "https://api.bitbucket.org/2.0"
# For Bitbucket Cloud: This is the workspace ID (e.g., "your-workspace-id")
# For Bitbucket Server/Data Center: This is the project key (e.g., "PROJ")
REPOSITORY_OWNER = "wailoktam"

# Repository slug (e.g., "my-repo")
REPOSITORY_SLUG = "jobSearchDemo"

# Your Bitbucket API Token (Bearer Token)
BITBUCKET_API_TOKEN = "ATATT3xFfGF06efGbWAiGJ8uUMwevtIelCRPPyHMl5WfN2Kis3h_gkprsTcrr_8cqRySgfdw9Cs18RpQ9sUZKrODUoSn5FE0o16ASyK_puWybH37BaYLSSb45MWnd8e4NxYWAm9ibFcE2B_CaqSmxcFIYi1pcDD1I9_t8L_uL6mHD6HOrN9qU14 = CEFEFD99"

# --- Example Usage ---
pull_requests = list_pull_requests(
BITBUCKET_URL,
REPOSITORY_OWNER,
REPOSITORY_SLUG,
BITBUCKET_API_TOKEN
)

if pull_requests:
print(f"\nPull Requests in {REPOSITORY_OWNER}/{REPOSITORY_SLUG}:")
if not pull_requests:
print("No pull requests found.")
else:
for pr in pull_requests:
print(f" ID: {pr.get('id')}, Title: '{pr.get('title')}', State: {pr.get('state')}, URL: {pr.get('url')}")
else:
print(f"Failed to retrieve pull requests for {REPOSITORY_OWNER}/{REPOSITORY_SLUG}.")

Suggest an answer

Log in or Sign up to answer
DEPLOYMENT TYPE
CLOUD
PERMISSIONS LEVEL
Product Admin Site Admin
TAGS
AUG Leaders

Atlassian Community Events