Use the Attachment Manager REST API to list labels, read and change the labels on individual Jira attachments, and find attachments across projects. This guide covers API v1 in Attachment Manager 2.0.0 for Jira Cloud and Jira Service Management Cloud.
Atlassian Forge app REST APIs are a Preview feature. Availability depends on your app edition and the site’s App REST APIs setting. Check Settings → REST API for your installation.
Quick start
-
Ask a site or organization admin to enable App REST APIs for Attachment Manager in Atlassian Administration → Apps → Sites → your site → Connected apps → View app details → Details. Copy the base URL shown there.
-
Create an OAuth 2.0 integration in the Atlassian Developer Console. Add Attachment Manager and the required scopes under Permissions, plus read:forge-app:jira.
-
Set a callback URL, use the console-generated authorization URL to obtain consent, and exchange the returned code for an access token.
-
Call GET /v1/labels with Authorization: Bearer <access token>. Use the label IDs from that response in later requests.
Attachment Manager settings: the REST API tab explains site enablement and OAuth setup.
# Set BASE to the URL copied from Connected apps (no trailing slash).
export BASE="https://YOUR-SITE.atlassian.net/gateway/api/svc/jira/apps/APP-ID_ENVIRONMENT-ID"
# Supply ACCESS_TOKEN from your integration’s secure token store.
curl --request GET --url "$BASE/v1/labels" \
--header "Authorization: Bearer $ACCESS_TOKEN" \
--header "Accept: application/json"
Authentication and authorization
Choose the correct base URL
All paths below are relative to BASE. Both forms below reach the same app installation. Prefer copying the complete base URL from Connected apps after enabling the API.
https://api.atlassian.com/svc/jira/{cloudId}/apps/{appId}_{environmentId}
https://{your-site}.atlassian.net/gateway/api/svc/jira/apps/{appId}_{environmentId}
|
Part |
Value |
|---|---|
|
appId |
80543892-0cc9-47a0-87c6-edba20d8c232 |
|
environmentId |
9306beda-25f0-4700-a1ae-dbc9f9de8f80 for the Marketplace release described in the v1 reference. |
|
cloudId |
Your Jira site’s cloud ID; available at https://{your-site}.atlassian.net/_edge/tenant_info. |
Use the app and environment selected for your integration. A staging installation has a different environment ID from the Marketplace release. Do not copy a staging settings URL and assume it is the production API base URL.
Enable the API for the site
A site or organization admin enables App REST APIs under Attachment Manager’s Connected apps details. App REST APIs are disabled by default. Disabling the setting immediately blocks external API calls for that site; it does not disable the app’s Jira interface.
Configure an OAuth 2.0 integration
-
Open the Atlassian Developer Console and choose Create → OAuth 2.0 integration. The person connecting it must be a member of the target site.
-
Under Permissions, choose Add Marketplace or custom app; select the site, Attachment Manager, and the correct environment. Add the app scopes required for the operations below.
-
Also add the Jira product scope read:forge-app:jira.
-
Under Authorization, configure your callback URL. Start from the authorization URL generated by the console. Keep its sns parameter, which associates custom scopes with Attachment Manager.
-
For unattended renewal, include offline_access in the scope parameter (space-separated; URL-encode spaces as %20). Complete consent, then exchange the code returned to the callback.
Atlassian: access REST APIs exposed by a Forge app
Exchange the code for tokens
curl --request POST --url https://auth.atlassian.com/oauth/token \
--header "Content-Type: application/json" \
--data '{
"grant_type": "authorization_code",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"code": "CODE_FROM_CALLBACK",
"redirect_uri": "YOUR_CALLBACK_URL"
}'
The response includes access_token, token_type, expires_in and scope. When offline_access was granted, it also includes refresh_token. The reference describes one-hour access tokens; use expires_in to schedule renewal. Keep client secrets and tokens in your integration’s secure storage.
Renew access
curl --request POST --url https://auth.atlassian.com/oauth/token \
--header "Content-Type: application/json" \
--data '{
"grant_type": "refresh_token",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"refresh_token": "YOUR_LATEST_REFRESH_TOKEN"
}'
Refresh tokens rotate: store the new refresh token returned by each renewal. The reference specifies expiry after 90 days without use. Reuse the access token across requests; do not request a new token for every API call.
Scopes and Jira permissions
|
Scope |
Purpose |
|---|---|
|
read:attachment-label:custom |
Read the site label catalogue, attachment labels, and attachment search. |
|
write:attachment-label:custom |
Add, replace, and remove attachment labels; unknown label names may be created. |
|
read:forge-app:jira |
Required Atlassian scope for every call. |
|
offline_access |
Optional OAuth scope to obtain a refresh token. |
Write scope does not grant search/read scope. An integration that finds attachments and then labels them needs both app scopes. Calls run as the person who consented, and label changes are attributed to that account.
|
Action |
Permission |
|---|---|
|
Read label catalogue |
Site membership. The catalogue and its usage counts are site-wide. |
|
Read attachment labels or search |
Browse Projects and visibility of the work item under issue security. |
|
Add, replace, or remove labels |
Browse Projects and Edit Issues on the work item. |
Content the caller cannot see is treated as not found. For unattended work, use a dedicated Atlassian account with the project access the integration requires.
Endpoint reference
|
Operation |
Method and path |
App scope |
|---|---|---|
|
List label catalogue |
GET /v1/labels |
read:attachment-label:custom |
|
Read attachment labels |
GET /v1/attachments/{attachmentId}/labels |
read:attachment-label:custom |
|
Add labels |
POST /v1/attachments/{attachmentId}/labels |
write:attachment-label:custom |
|
Replace all labels |
PUT /v1/attachments/{attachmentId}/labels |
write:attachment-label:custom |
|
Remove one label |
DELETE /v1/attachments/{attachmentId}/labels/{label} |
write:attachment-label:custom |
|
Search attachments |
GET /v1/attachments |
read:attachment-label:custom |
All successful operations return 200 OK and JSON, including writes and DELETE. Send Accept: application/json on requests, and Content-Type: application/json on POST and PUT. IDs are integers; timestamps are ISO 8601 UTC; sizes are bytes; colorHex is a six-digit palette value without #.
Label references
In JSON, a number is a label ID and a string is a name: {"labels":[100002,"Documentation"]}. A digits-only JSON string such as "2026" is still a name. In URL paths and query parameters, digits-only values are IDs; other values are names. Name matching ignores case, trims outer spaces, and collapses repeated spaces. Use IDs for names consisting only of digits or containing commas or percent signs. URL-encode names used in paths or query strings.
1. List the label catalogue
curl --url "$BASE/v1/labels" \
--header "Authorization: Bearer $ACCESS_TOKEN" --header "Accept: application/json"
Returns {"labels":[...]} sorted by name. Each label includes labelId, name, colorHex, needsReview, usageCount, usageBytes, usageAsOf and createdAt. usageAsOf may be null; counts are refreshed in the background, rather than on every write. needsReview identifies recovered placeholder labels an admin should rename. An optional job field appears during admin deletion or merging: {"type":"delete"|"merge","jobId":"..."}.
2. Read an attachment’s labels
Use a positive Jira attachment ID from attachment search or Jira’s GET /rest/api/3/issue/{issueIdOrKey}?fields=attachment. The following IDs and response are illustrative; replace them with values from your site.
curl --url "$BASE/v1/attachments/10104/labels" \
--header "Authorization: Bearer $ACCESS_TOKEN" --header "Accept: application/json"
{
"attachmentId": 10104,
"issueId": 10003,
"issueKey": "DOC-3",
"projectId": 10000,
"projectKey": "DOC",
"filename": "user-guide.pdf",
"labels": [
{
"labelId": 100003,
"name": "Documentation",
"colorHex": "CCE0FF"
}
]
}
The labels array is sorted by labelId and may be empty. A 404 can mean the attachment does not exist, the work item is hidden, the project is not tracked, or the attachment has not been indexed yet.
3. Add labels without removing existing ones
POST accepts 1–50 label references. Existing assignments and duplicate references are skipped. An unknown name creates a site-wide label with a palette colour derived from its name; an unknown numeric ID returns 404. Use IDs when you want to avoid creating labels accidentally.
curl --request POST --url "$BASE/v1/attachments/10104/labels" \
--header "Authorization: Bearer $ACCESS_TOKEN" \
--header "Accept: application/json" --header "Content-Type: application/json" \
--data '{"labels":[100003,"Reviewed"]}'
4. Replace the complete label set
PUT accepts 0–50 references. Any label omitted from the request is removed from this attachment, and unknown names are created. Read the current labels first if you need to preserve any of them. Use POST when you only intend to add labels.
curl --request PUT --url "$BASE/v1/attachments/10104/labels" \
--header "Authorization: Bearer $ACCESS_TOKEN" \
--header "Accept: application/json" --header "Content-Type: application/json" \
--data '{"labels":[100003]}'
To clear all labels, send {"labels":[]}. This removes assignments; it does not delete label definitions or the Jira attachment.
5. Remove one label
curl --request DELETE --url "$BASE/v1/attachments/10104/labels/100003" \
--header "Authorization: Bearer $ACCESS_TOKEN" --header "Accept: application/json"
DELETE takes no body. A URL-encoded label name can replace the numeric label ID. An existing catalogue label that is not assigned to this attachment produces a successful unchanged response. A label that does not exist returns 404; DELETE never creates labels.
Write responses and Jira search
POST, PUT and DELETE return the same attachment fields as the read endpoint, with the complete resulting label set and propertyState:
|
propertyState |
Meaning |
|---|---|
|
WRITTEN |
Labels were saved and the work-item property used by JQL was updated. |
|
QUEUED_FOR_REPAIR |
Labels were saved. The JQL property update was deferred to a background repair, usually completed within minutes. |
|
UNCHANGED |
The attachment already had the requested label state. |
Use Attachmentlabels in JQL to find work items, and the REST API to obtain individual attachments. For example:
Attachmentlabels = "100003"
6. Search attachments
GET /v1/attachments combines different filters with AND. Multiple values within a list parameter match any of those values. Lists accept repeated parameters or comma-separated values. Search covers only indexed projects and work items the caller can see.
|
Parameter |
Type / default |
Behaviour |
|---|---|---|
|
label |
List of IDs or names |
Matches any specified label. Unknown labels return 404. |
|
projectId |
List of integers |
At most 50. Inaccessible/nonexistent requested projects appear in deniedProjectIds. |
|
q |
String |
Case-sensitive filename substring, minimum 2 characters. % and _ are literal. |
|
type |
List of media families |
Matches any requested family; see the list below. |
|
uploader |
List of Atlassian account IDs |
Matches uploads by any listed account. |
|
from |
ISO 8601 date/time |
Inclusive lower upload-time bound. |
|
to |
ISO 8601 date/time |
Exclusive upper upload-time bound. |
|
minBytes |
Nonnegative integer |
Inclusive minimum size. |
|
maxBytes |
Nonnegative integer |
Inclusive maximum, at least minBytes. |
|
sort |
UPLOADED_DESC by default |
UPLOADED_DESC, UPLOADED_ASC, SIZE_DESC, SIZE_ASC, FILENAME_ASC. Filename sorting requires exactly one project. |
|
pageSize |
50 by default |
Minimum 1; values over 100 are capped at 100. |
|
cursor |
Opaque string |
Use nextCursor from the previous response; preserve the filters and sort. |
Media families: image, pdf, document, spreadsheet, presentation, archive, video, audio, text, code, data, other. The family is derived from MIME type, with a filename-extension fallback for generic MIME types.
Use explicit UTC offsets for dates. A date alone means midnight UTC. For example, from=2026-03-01 and to=2026-04-01 covers March. Without projectId, only the first 50 browsable projects are searched; use explicit batches of up to 50 for larger sites.
Search examples
# Find labelled files in one project, largest first.
curl --get --url "$BASE/v1/attachments" \
--header "Authorization: Bearer $ACCESS_TOKEN" --header "Accept: application/json" \
--data-urlencode "projectId=10000" \
--data-urlencode "label=Documentation,Reviewed" \
--data-urlencode "sort=SIZE_DESC"
# Find PDFs and documents uploaded in March, at least 1 MiB.
curl --get --url "$BASE/v1/attachments" \
--header "Authorization: Bearer $ACCESS_TOKEN" --header "Accept: application/json" \
--data-urlencode "projectId=10000" \
--data-urlencode "type=pdf,document" \
--data-urlencode "from=2026-03-01T00:00:00Z" \
--data-urlencode "to=2026-04-01T00:00:00Z" \
--data-urlencode "minBytes=1048576"
# Search by filename and uploader. Replace the example account ID.
curl --get --url "$BASE/v1/attachments" \
--header "Authorization: Bearer $ACCESS_TOKEN" --header "Accept: application/json" \
--data-urlencode "projectId=10000" \
--data-urlencode "q=guide" \
--data-urlencode "uploader=YOUR_ATLASSIAN_ACCOUNT_ID" \
--data-urlencode "sort=FILENAME_ASC" \
--data-urlencode "pageSize=50"
To require every one of several labels, search by one label and retain rows whose labels array also contains all other required IDs. A comma-separated label filter alone means ANY, not ALL.
Search response and pagination
{
"rows": [
{
"attachmentId": 10104,
"issueId": 10003,
"issueKey": "DOC-3",
"projectId": 10000,
"projectKey": "DOC",
"filename": "user-guide.pdf",
"sizeBytes": 1048576,
"mediaType": "pdf",
"uploaderAccountId": "EXAMPLE_ACCOUNT_ID",
"uploadedAt": "2026-03-02T09:14:07.000Z",
"labels": [
{
"labelId": 100003,
"name": "Documentation",
"colorHex": "CCE0FF"
}
]
}
],
"nextCursor": null,
"deniedProjectIds": []
}
Every row includes all its labels, not just matching labels. deniedProjectIds lists inaccessible or nonexistent explicitly requested projects; it is empty if projectId was omitted. Untracked projects return no rows and are not included in deniedProjectIds.
Continue until nextCursor is null, even when rows is empty. Issue-security filtering can shorten or empty a page while more pages remain. Treat cursors as opaque and repeat the same filters and sort.
import requests
def search_all(base, access_token, filters):
headers = {"Authorization": f"Bearer {access_token}",
"Accept": "application/json"}
params = dict(filters)
params.pop("cursor", None)
while True:
response = requests.get(f"{base.rstrip('/')}/v1/attachments",
headers=headers, params=params, timeout=30)
response.raise_for_status()
page = response.json()
yield from page["rows"]
if page["nextCursor"] is None:
break
params["cursor"] = page["nextCursor"]
This pagination example raises on HTTP errors. A production integration should add token renewal and bounded retries following the rules below.
Errors and retries
{
"error": {
"code": "NOT_FOUND",
"message": "attachment 10999 not found",
"details": {}
},
"inv": "EXAMPLE_INVOCATION_ID"
}
Branch on error.code rather than error.message. details is optional. Keep inv for support. Gateway errors from Atlassian can have a different response shape.
|
HTTP / code |
Meaning and action |
|---|---|
|
400 VALIDATION |
Invalid JSON, parameter, ID, name, range, cursor or limit. Fix the request before retrying. |
|
401 (gateway) |
Missing, expired or invalid token. Refresh access and retry. |
|
403 FORBIDDEN |
Token owner lacks a required Jira permission, such as Edit Issues. Check the project permission scheme. |
|
403 (gateway) |
API is disabled or scopes are missing. Check the site toggle, app environment, read:forge-app:jira, custom scopes and sns; consent again after adding scopes. |
|
404 NOT_FOUND |
Attachment/label does not exist or is not visible/indexed. Check IDs, project tracking, issue security and recent-upload indexing. |
|
404 (gateway route) |
Check base URL, HTTP method and /v1 path. PATCH is not one of the supported operations. |
|
409 LABEL_NAME_EXISTS |
Concurrent creation of the same name. Retry once so the name resolves to the existing label. |
|
429 UPSTREAM_RATE_LIMITED |
Jira limited the request. Wait the Retry-After seconds before retrying. |
|
429 (platform) |
Use exponential backoff; honour Retry-After when supplied. |
|
500 INTERNAL / SQL_ERROR; 502 UPSTREAM_ERROR |
Retry with bounded backoff. Contact support with inv if persistent. |
|
503 MIGRATION_IN_PROGRESS |
Setup or upgrade is still running. Wait Retry-After (60 seconds in the reference). |
|
504 TIMEOUT |
Retry or narrow the search with projects and filters. |
Repeating an identical POST, PUT or DELETE is idempotent with respect to the intended label state. Avoid unbounded retry loops; PUT still replaces the complete set, so coordinate concurrent editors when that matters.
Limits and freshness
|
Limit |
Value |
|---|---|
|
POST label references |
1–50 |
|
PUT label references |
0–50 |
|
Labels per site |
1,000 |
|
Label name length |
100 characters |
|
Search page size |
1–100, default 50 |
|
Projects per search |
Up to 50 |
|
Filename query |
At least 2 characters |
New uploads are usually indexed within seconds. Label changes are available through this API when the write returns; catalogue usage counts update later. JQL synchronization is reported separately by propertyState. The supplied v1 reference states that API access has no separate licence; confirm that your installed edition exposes the REST API tab. Rate limits are controlled by Atlassian; handle 429 and Retry-After instead of relying on a fixed quota.
Troubleshooting
-
“REST APIs are not allowed: installationConfig is missing”: enable App REST APIs for this exact app/environment and site.
-
“Missing required scopes”: preserve the console-generated sns parameter, add the missing custom/product scopes, and obtain fresh consent.
-
Search is incomplete: specify projectId batches, check the case of q, verify tracked projects, and follow nextCursor until null.
-
Labels changed but JQL has not caught up: inspect propertyState; QUEUED_FOR_REPAIR means label changes were saved and synchronization is pending.
-
A visible Jira attachment returns 404: verify its project is tracked and allow time for indexing; also check the consenting account’s access.
When contacting support, provide the method and path, status/error code, timestamp and inv. Remove access tokens, refresh tokens and client secrets from examples.
Versioning and changelog
The version is part of the path: /v1. Clients should tolerate additional optional response fields and new error codes. Breaking changes use a new version path.
|
Date |
Change |
|---|---|
|
14 September 2026 |
v1 reference: label catalogue; read, add, replace and remove attachment labels; attachment search. |