界面已本地化;技能介绍与说明文档按所选语言由 AI 自动翻译,代码、命令、路径和专业术语保留原文,以原作者文档为准。
探索技能
数据与研究 · SKILL.md

confluence-rest-api

confluence-rest-api

【数据与研究】Use when interacting with Confluence programmatically via REST API — API v1 and v2 endpoints, authentication (API tokens, OAuth, PAT), CRUD operations on pages/spaces/attachments, CQL (Confluence Query Language), content properties, labels, comments, user/group management, bulk operations, pagination, rate limiting, and error handling. Works with bot…

独立 GitHub 快照 · 自动分类 · 以原作者文档为准

技能说明

GitHub

name: confluence-rest-api description: Use when interacting with Confluence programmatically via REST API — API v1 and v2 endpoints, authentication (API tokens, OAuth, PAT), CRUD operations on pages/spaces/attachments, CQL (Confluence Query Language), content properties, labels, comments, user/group management, bulk operations, pagination, rate limiting, and error handling. Works with both Confluence Cloud and Data Center. family: confluence disambiguation: Confluence's own REST surface — spaces, pages, content CRUD. Jira issues, JQL and workflows are jira-rest-api, a different product with a different API.

Confluence REST API

Companion skill: confluence-content-creator — higher-level page authoring workflows built on these API primitives.

1. Authentication

API Tokens (Cloud) — Basic Auth with email + token

export CONFLUENCE_BASE="https://yoursite.atlassian.net/wiki"
export CONFLUENCE_USER="you@company.com"
export CONFLUENCE_TOKEN="your-api-token"
curl -s -u "${CONFLUENCE_USER}:${CONFLUENCE_TOKEN}" "${CONFLUENCE_BASE}/api/v2/spaces?limit=5"
import os, requests
BASE = os.environ["CONFLUENCE_BASE"]
AUTH = (os.environ["CONFLUENCE_USER"], os.environ["CONFLUENCE_TOKEN"])
resp = requests.get(f"{BASE}/api/v2/spaces", params={"limit": 5}, auth=AUTH)

Personal Access Tokens (Data Center 7.14+) — Bearer token, no username

curl -s -H "Authorization: Bearer ${CONFLUENCE_PAT}" "${CONFLUENCE_BASE}/rest/api/content?limit=5"

OAuth 2.0 (Cloud 3LO) — for Connect/Forge apps

# Exchange authorization code for token
token_resp = requests.post("https://auth.atlassian.com/oauth/token", json={
    "grant_type": "authorization_code", "client_id": os.environ["OAUTH_CLIENT_ID"],
    "client_secret": os.environ["OAUTH_CLIENT_SECRET"], "code": auth_code,
    "redirect_uri": "https://yourapp.com/callback"})
access_token = token_resp.json()["access_token"]
# Get cloud_id, then call: /ex/confluence/{cloud_id}/wiki/api/v2/pages
Credential StorageUse Case
Environment variablesLocal dev, CI/CD
HashiCorp Vault / AWS Secrets ManagerProduction
.env + python-dotenv (in .gitignore)Local dev

2. API Versions

v2 = Cloud default. v1 = Data Center default.

v2 (/api/v2/... under the /wiki context, i.e. https://yoursite.atlassian.net/wiki/api/v2/pages) — the current API for Confluence Cloud: pages, spaces, blog posts, comments, attachments. Cleaner responses, cursor pagination (limit + cursor). NOT available on Data Center. v1 (/rest/api/...) — the API for Data Center. On Cloud, v1 still serves CQL search (/rest/api/search, no announced deprecation) and some endpoints without v2 equivalents, but v1 content CRUD (/rest/api/content) is deprecated for Cloud (see note below). Offset pagination (start+limit).

Deprecation note (verified 2026-06-10): Atlassian has deprecated many Confluence Cloud v1 endpoints that have v2 equivalents — including /rest/api/content page CRUD. Announced removal deadlines were extended repeatedly (2024-12-02 → 2025-03-31 → 2025-04-30, then per-endpoint dates into late 2026, e.g. Convert content body to 2026-08-05); removal is rolling out endpoint-by-endpoint, NOT as a single cut-off, and some v1 groups (CQL search) have no deprecation plans. Do NOT build new Cloud integrations on v1 content CRUD; check the Confluence Cloud changelog for the current per-endpoint status before relying on any deprecated v1 endpoint. This deprecation does NOT apply to Data Center, where v1 remains the supported API.

Use v2 when (Cloud)Use v1 when
Any Cloud pages/spaces CRUD (default)Targeting Data Center (v2 unavailable there)
Building new Cloud integrationsCQL search — no v2 equivalent
Need cursor pagination on large result setsLegacy Cloud scripts pending migration (deprecated — migrate)

Key v2 endpoints: GET/POST/PUT/DELETE /api/v2/pages, GET /api/v2/spaces, GET /api/v2/pages/{id}/children, GET /api/v2/pages/{id}/versions. With CONFLUENCE_BASE ending in /wiki (as in §1), call them as ${CONFLUENCE_BASE}/api/v2/....

3. Pages CRUD

Create

# v2 — Cloud (default). v2 takes a numeric spaceId, NOT a space key —
# resolve once: GET {BASE}/api/v2/spaces?keys=DEV → results[0]["id"]
payload = {"spaceId": "98765", "status": "current", "title": "New Page",
           "body": {"representation": "storage", "value": "<p>Content here.</p>"}}
resp = requests.post(f"{BASE}/api/v2/pages", json=payload, auth=AUTH)

# Child page — add parentId
payload["parentId"] = "123456"  # parent page ID
# v1 — Data Center
payload = {"type": "page", "title": "New Page", "space": {"key": "DEV"},
           "body": {"storage": {"value": "<p>Content here.</p>", "representation": "storage"}}}
resp = requests.post(f"{BASE}/rest/api/content", json=payload, auth=AUTH)

# Child page — add ancestors
payload["ancestors"] = [{"id": "123456"}]  # parent page ID

Get by ID / Title

# v2 — Cloud: by ID with body
curl -s -u "${CONFLUENCE_USER}:${CONFLUENCE_TOKEN}" \
  "${CONFLUENCE_BASE}/api/v2/pages/123456?body-format=storage"
# v2 — Cloud: by title (optionally scoped with space-id)
curl -s -u "${CONFLUENCE_USER}:${CONFLUENCE_TOKEN}" \
  "${CONFLUENCE_BASE}/api/v2/pages?title=Meeting+Notes&space-id=98765"
# v1 — Data Center: by ID with expanded fields
curl -s -u "${CONFLUENCE_USER}:${CONFLUENCE_TOKEN}" \
  "${CONFLUENCE_BASE}/rest/api/content/123456?expand=body.storage,version,ancestors"
# v1 — Data Center: by title in a space
curl -s -u "${CONFLUENCE_USER}:${CONFLUENCE_TOKEN}" \
  "${CONFLUENCE_BASE}/rest/api/content?title=Meeting+Notes&spaceKey=DEV&expand=version"

Update (MUST increment version)

# v2 — Cloud (id is required in the body and must match the URL)
page = requests.get(f"{BASE}/api/v2/pages/{page_id}",
                    params={"body-format": "storage"}, auth=AUTH).json()
update = {"id": str(page_id), "status": "current", "title": page["title"],
          "version": {"number": page["version"]["number"] + 1},
          "body": {"representation": "storage", "value": "<p>Updated.</p>"}}
resp = requests.put(f"{BASE}/api/v2/pages/{page_id}", json=update, auth=AUTH)
# v1 — Data Center
page = requests.get(f"{BASE}/rest/api/content/{page_id}",
                    params={"expand": "body.storage,version"}, auth=AUTH).json()
update = {"version": {"number": page["version"]["number"] + 1}, "title": page["title"],
          "type": "page", "body": {"storage": {"value": "<p>Updated.</p>", "representation": "storage"}}}
resp = requests.put(f"{BASE}/rest/api/content/{page_id}", json=update, auth=AUTH)

Delete / Archive / Move / Copy

# v2 — Cloud
curl -X DELETE "${CONFLUENCE_BASE}/api/v2/pages/123456"             # trash
curl -X DELETE "${CONFLUENCE_BASE}/api/v2/pages/123456?purge=true"  # purge (page must already be trashed)
# v1 — Data Center
curl -X DELETE "${CONFLUENCE_BASE}/rest/api/content/123456"           # trash
curl -X DELETE "${CONFLUENCE_BASE}/rest/api/content/123456?status=trashed"  # purge
# Move page under new parent — v1 only (no v2 equivalent as of 2026-06; works on Cloud + Data Center, verify current Cloud status)
requests.put(f"{BASE}/rest/api/content/{page_id}/move/append/{target_parent_id}", auth=AUTH)
# Copy page — v1 only (same caveat)
requests.post(f"{BASE}/rest/api/content/{page_id}/copy", auth=AUTH, json={
    "copyAttachments": True, "copyLabels": True,
    "destination": {"type": "parent_page", "value": "789012"}})
# Page version history — v2 (Cloud)
resp = requests.get(f"{BASE}/api/v2/pages/{page_id}/versions", params={"limit": 10}, auth=AUTH)
# Page version history — v1 (Data Center)
resp = requests.get(f"{BASE}/rest/api/content/{page_id}/version", params={"limit": 10}, auth=AUTH)

4. Content Body — Storage Format

Confluence uses XHTML-based storage format. Key macros:

<!-- Code block -->
<ac:structured-macro ac:name="code">
  <ac:parameter ac:name="language">python</ac:parameter>
  <ac:plain-text-body><![CDATA[print("hello")]]></ac:plain-text-body>
</ac:structured-macro>

<!-- Info/Warning panels -->
<ac:structured-macro ac:name="info">
  <ac:rich-text-body><p>Info text.</p></ac:rich-text-body>
</ac:structured-macro>

<!-- TOC, Status lozenge, Expand -->
<ac:structured-macro ac:name="toc"><ac:parameter ac:name="maxLevel">3</ac:parameter></ac:structured-macro>
<ac:structured-macro ac:name="status">
  <ac:parameter ac:name="title">DONE</ac:parameter><ac:parameter ac:name="colour">Green</ac:parameter>
</ac:structured-macro>
<ac:structured-macro ac:name="expand">
  <ac:parameter ac:name="title">Details</ac:parameter>
  <ac:rich-text-body><p>Hidden content.</p></ac:rich-text-body>
</ac:structured-macro>

<!-- Link to page, mention user -->
<ac:link><ri:page ri:content-title="Target Page" ri:space-key="DEV" /></ac:link>
<ac:link><ri:user ri:account-id="5a1234bc567890def" /></ac:link>
# Convert wiki markup to storage format
resp = requests.post(f"{BASE}/rest/api/contentbody/convert/storage",
    json={"value": "h2. Heading\nText here.", "representation": "wiki"}, auth=AUTH)
storage_html = resp.json()["value"]

5. Spaces

# List spaces
resp = requests.get(f"{BASE}/rest/api/space", params={"limit": 25, "type": "global"}, auth=AUTH)

# Create space
requests.post(f"{BASE}/rest/api/space", auth=AUTH, json={
    "key": "NEW", "name": "New Space", "type": "global",
    "description": {"plain": {"value": "Description.", "representation": "plain"}}})

# Get space with homepage
resp = requests.get(f"{BASE}/rest/api/space/DEV", params={"expand": "homepage"}, auth=AUTH)
homepage_id = resp.json()["homepage"]["id"]

# Space permissions (Cloud)
requests.post(f"{BASE}/rest/api/space/DEV/permission", auth=AUTH, json={
    "subject": {"type": "group", "identifier": "developers"},
    "operation": {"key": "read", "targetType": "space"}})

6. CQL (Confluence Query Language)

Syntax: field operator value [AND|OR ...] [ORDER BY field ASC|DESC]

def cql_search(cql, limit=25):
    resp = requests.get(f"{BASE}/rest/api/search", params={"cql": cql, "limit": limit}, auth=AUTH)
    resp.raise_for_status()
    return resp.json()["results"]

cql_search('space = "DEV" AND type = "page"')                          # pages in space
cql_search('text ~ "deployment guide"')                                 # full-text search
cql_search('label = "architecture" AND type = "page"')                  # by label
cql_search('creator = "john.doe" AND type = "page"')                    # by creator
cql_search('lastModified >= now("-7d") AND space = "DEV"')              # recent changes
cql_search('title ~ "Sprint*" AND type = "page" ORDER BY lastModified DESC')  # title match
cql_search('ancestor = 123456 AND type = "page"')                      # children of page
cql_search('type = "blogpost" AND space = "ENG" ORDER BY created DESC') # blog posts
curl -s -u "${CONFLUENCE_USER}:${CONFLUENCE_TOKEN}" --get "${CONFLUENCE_BASE}/rest/api/search" \
  --data-urlencode 'cql=space = "DEV" AND type = "page"' --data-urlencode 'limit=10'
FieldOperatorsExample
space=, !=, INspace IN ("DEV","OPS")
type=type = "page"
title=, ~title ~ "API"
text~text ~ "keyword"
label=, INlabel = "reviewed"
creator=creator = currentUser()
lastModified/created>=, <=, >, <lastModified >= now("-30d")
ancestor/parent=ancestor = 123456

7. Attachments

# Upload
requests.put(f"{BASE}/rest/api/content/{page_id}/child/attachment", auth=AUTH,
    headers={"X-Atlassian-Token": "nocheck"},
    files={"file": ("report.pdf", open("report.pdf", "rb"), "application/pdf")},
    data={"comment": "Initial upload"})

# List attachments
resp = requests.get(f"{BASE}/rest/api/content/{page_id}/child/attachment",
                    params={"limit": 50}, auth=AUTH)

# Download
download_path = resp.json()["results"][0]["_links"]["download"]
content = requests.get(f"{BASE}{download_path}", auth=AUTH).content

# Update existing attachment
requests.post(f"{BASE}/rest/api/content/{page_id}/child/attachment/{att_id}/data", auth=AUTH,
    headers={"X-Atlassian-Token": "nocheck"},
    files={"file": ("report.pdf", open("report_v2.pdf", "rb"), "application/pdf")})
curl -u "${CONFLUENCE_USER}:${CONFLUENCE_TOKEN}" -X PUT \
  "${CONFLUENCE_BASE}/rest/api/content/123456/child/attachment" \
  -H "X-Atlassian-Token: nocheck" -F "file=@report.pdf"

8. Labels

# Add labels
requests.post(f"{BASE}/rest/api/content/{page_id}/label", auth=AUTH,
              json=[{"prefix": "global", "name": "api-docs"}, {"prefix": "global", "name": "v2"}])

# Get labels
labels = requests.get(f"{BASE}/rest/api/content/{page_id}/label", auth=AUTH).json()["results"]

# Remove label
requests.delete(f"{BASE}/rest/api/content/{page_id}/label/{label_name}", auth=AUTH)

# Search by label (via CQL)
cql_search('label = "architecture" AND space = "DEV"')

9. Comments

# Get comments on a page
resp = requests.get(f"{BASE}/rest/api/content/{page_id}/child/comment",
                    params={"expand": "body.storage,version", "limit": 50}, auth=AUTH)

# Create page-level comment
requests.post(f"{BASE}/rest/api/content", auth=AUTH, json={
    "type": "comment", "container": {"id": page_id, "type": "page"},
    "body": {"storage": {"value": "<p>LGTM!</p>", "representation": "storage"}}})

# Inline comment (Cloud v2)
requests.post(f"{BASE}/api/v2/inline-comments", auth=AUTH, json={
    "pageId": page_id, "body": {"representation": "storage", "value": "<p>Typo here.</p>"},
    "inlineCommentProperties": {"textSelection": "exact text", "textSelectionMatchCount": 1,
                                 "textSelectionMatchIndex": 0}})

# Update comment (must increment version)
comment = requests.get(f"{BASE}/rest/api/content/{comment_id}",
                       params={"expand": "version"}, auth=AUTH).json()
requests.put(f"{BASE}/rest/api/content/{comment_id}", auth=AUTH, json={
    "version": {"number": comment["version"]["number"] + 1}, "type": "comment",
    "body": {"storage": {"value": "<p>Updated.</p>", "representation": "storage"}}})

# Delete comment
requests.delete(f"{BASE}/rest/api/content/{comment_id}", auth=AUTH)

10. Content Properties

Custom key-value metadata on pages — useful for sync state, build numbers, integration data.

# Set property
requests.post(f"{BASE}/rest/api/content/{page_id}/property", auth=AUTH, json={
    "key": "deploy-status",
    "value": {"environment": "production", "build": "2026.3.42"}})

# Get property
prop = requests.get(f"{BASE}/rest/api/content/{page_id}/property/deploy-status", auth=AUTH).json()

# Update property (must increment version)
requests.put(f"{BASE}/rest/api/content/{page_id}/property/deploy-status", auth=AUTH, json={
    "key": "deploy-status", "value": {"environment": "production", "build": "2026.3.43"},
    "version": {"number": prop["version"]["number"] + 1}})

# Delete property
requests.delete(f"{BASE}/rest/api/content/{page_id}/property/deploy-status", auth=AUTH)

11. User and Group Management

# Current user
user = requests.get(f"{BASE}/rest/api/user/current", auth=AUTH).json()

# User by account ID (Cloud) or key (Data Center)
requests.get(f"{BASE}/rest/api/user", params={"accountId": "5a1234..."}, auth=AUTH)

# List group members
resp = requests.get(f"{BASE}/rest/api/group/developers/member", params={"limit": 200}, auth=AUTH)

# Add/remove user from group (Data Center)
requests.post(f"{BASE}/rest/api/group/developers/member", json={"name": "john.doe"}, auth=AUTH)
requests.delete(f"{BASE}/rest/api/group/developers/member", params={"name": "john.doe"}, auth=AUTH)

12. Bulk Operations and Pagination

v1 offset pagination (start + limit)

def get_all_pages_v1(space_key):
    all_pages, start, limit = [], 0, 100
    while True:
        data = requests.get(f"{BASE}/rest/api/content", auth=AUTH,
            params={"spaceKey": space_key, "type": "page", "limit": limit, "start": start}).json()
        all_pages.extend(data["results"])
        if data["size"] < limit or "next" not in data.get("_links", {}):
            break
        start += limit
    return all_pages

v2 cursor pagination (Cloud)

def get_all_pages_v2(space_id):
    all_pages, url = [], f"{BASE}/api/v2/pages"
    params = {"space-id": space_id, "limit": 250}
    while url:
        data = requests.get(url, params=params, auth=AUTH).json()
        all_pages.extend(data["results"])
        url = data.get("_links", {}).get("next")
        params = {}  # cursor URL includes params
    return all_pages

Rate-limit-aware bulk creation

import time
def bulk_create_pages(pages_data, space_key, delay=0.5):
    created = []
    for page_data in pages_data:
        payload = {"type": "page", "title": page_data["title"], "space": {"key": space_key},
                   "body": {"storage": {"value": page_data["body"], "representation": "storage"}}}
        resp = requests.post(f"{BASE}/rest/api/content", json=payload, auth=AUTH)
        if resp.status_code == 429:
            time.sleep(int(resp.headers.get("Retry-After", 60)))
            resp = requests.post(f"{BASE}/rest/api/content", json=payload, auth=AUTH)
        resp.raise_for_status()
        created.append(resp.json()["id"])
        time.sleep(delay)
    return created

Session with automatic retry

from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

def confluence_session():
    s = requests.Session()
    s.mount("https://", HTTPAdapter(max_retries=Retry(
        total=5, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504],
        allowed_methods=["GET","POST","PUT","DELETE"], respect_retry_after_header=True)))
    s.auth = AUTH
    return s

13. Webhooks

Register (Data Center)

requests.post(f"{BASE}/rest/webhooks/1.0/webhook", auth=AUTH, json={
    "name": "Page Notifier", "url": "https://yourapp.com/webhooks/confluence",
    "events": ["page_created", "page_updated", "page_removed", "comment_created"],
    "active": True})

Event types

page_created, page_updated, page_removed, page_restored, comment_created, comment_updated, comment_removed, attachment_created, space_created, space_removed, label_added, label_removed.

Payload structure

{"timestamp": 1711267200000, "event": "page_updated",
 "userAccountId": "5a1234...",
 "page": {"id": 123456, "title": "API Guide", "spaceKey": "DEV", "version": 5}}

Cloud webhooks are registered via Atlassian Connect app descriptors (atlassian-connect.json) or Forge event triggers, not via REST.

14. Error Handling

CodeMeaningCause
400Bad RequestMalformed JSON, invalid fields
401UnauthorizedInvalid/expired credentials
403ForbiddenInsufficient permissions
404Not FoundWrong ID or content trashed
409ConflictStale version number on update
413Payload Too LargeAttachment exceeds limit
429Too Many RequestsRate limited — check Retry-After
5xxServer ErrorRetry with backoff
def safe_api_call(method, url, max_retries=3, **kwargs):
    kwargs.setdefault("auth", AUTH)
    for attempt in range(max_retries):
        resp = requests.request(method, url, **kwargs)
        if resp.status_code == 429:
            time.sleep(int(resp.headers.get("Retry-After", 2 ** attempt * 10)))
            continue
        if resp.status_code >= 500:
            time.sleep(2 ** attempt * 5)
            continue
        if resp.status_code == 409:
            raise Exception(f"Version conflict: {resp.json().get('message')}")
        resp.raise_for_status()
        return resp
    raise Exception(f"Failed after {max_retries} retries: {method} {url}")

Security — XML / XHTML parsing

For HTML/XHTML rendering of downstream output (storage format → display), sanitise with bleach or nh3 BEFORE inserting into a browser context — never raw-render API-returned XHTML. See llm-security SKILL.md §4.4 for context-appropriate escaping rules.

Anti-Patterns

Don'tWhy
Hardcode API tokens in scriptsCredential leak via source control
Skip version number on updates409 Conflict every time
Tight-loop API calls without delayRate limited and potentially blocked
Use DELETE without confirming page IDNo undo for purged pages
Ignore expand parameter on GETsResponse bodies are minimal by default
Assume v2 endpoints exist on Data CenterMany v2 endpoints are Cloud-only
Send raw HTML instead of storage formatUse <ac:structured-macro> for macros
Use offset pagination for large Cloud datasetsCursor-based (v2) is more reliable
我的收藏