Every endpoint of the Attachment Manager REST API, with examples. Set up access first in REST API — setup and usage; error codes and limits are in REST API errors and limits.
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 |
|---|---|
|
Any call |
The Attachment Manager - Use App permission in Jira. Jira administrators always pass. |
|
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
POST, PUT and DELETE return the same attachment fields as the read endpoint, with the complete resulting label set. Labels live in Attachment Manager only: they are not written to the Jira work item, so they cannot be searched with JQL. Use Search attachments below to find files by label.
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.