Attachment Manager for Jira

REST API reference

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

Bash
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.

Bash
curl --url "$BASE/v1/attachments/10104/labels" \
  --header "Authorization: Bearer $ACCESS_TOKEN" --header "Accept: application/json"
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.

Bash
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.

Bash
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

Bash
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

Bash
# 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

JSON
{
  "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.

Python
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.

Last updated: