Attachment Manager for Jira

REST API — setup and usage

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

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

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

  3. Set a callback URL, use the console-generated authorization URL to obtain consent, and exchange the returned code for an access token.

  4. Call GET /v1/labels with Authorization: Bearer <access token>. Use the label IDs from that response in later requests.

rest-api-setup.png

Attachment Manager settings: the REST API tab explains site enablement and OAuth setup.

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

  1. Open the Atlassian Developer Console and choose Create → OAuth 2.0 integration. The person connecting it must be a member of the target site.

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

  3. Also add the Jira product scope read:forge-app:jira.

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

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

Atlassian Developer Console

Exchange the code for tokens

Bash
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

Bash
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

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.

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

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.

Errors and retries

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

Contact VIEW26 support

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.

Last updated: