Confluence API Doc Sync
Sync API documentation from a multi-file docs/api/ directory to Confluence — one endpoint file = one Confluence page. The directory structure maps directly to a Confluence page tree, so no heading-based splitting is needed.
Uses acli for authentication verification and page reading (to get current version), and Confluence REST API via curl for page updates.
Core Principle: Directory Structure = Confluence Page Tree
The docs/api/ directory (generated by api-doc-gen) already organizes endpoints as individual files grouped by domain. This skill maps that structure directly to Confluence:
docs/api/ Confluence Page Tree
├── index.md → Parent Page (overview + common errors)
├── consent/ → ├── Consent (domain group page)
│ ├── accept-consent.md → │ ├── Accept Consent
│ ├── get-consent.md → │ ├── Get Consent
│ └── revoke-consent.md → │ └── Revoke Consent
├── channel/ → ├── Channel (domain group page)
│ ├── create-channel.md → │ ├── Create Channel
│ └── get-all-channels.md → │ └── Get All Channels
└── purpose/ → └── Purpose (domain group page)
├── create-purpose.md → ├── Create Purpose
└── get-purposes.md → └── Get PurposesEach .md endpoint file becomes exactly one Confluence page. Group directories become parent pages.
Step 1: Gather Required Information
Ask the user for the following (if not already provided):
- API doc directory path — e.g.,
docs/api/(relative to project root, or absolute). Default:docs/api/ - Parent page URL — the Confluence page/folder under which API doc pages live (or will be created), e.g.,
https://company.atlassian.net/wiki/spaces/PROJ/pages/123456789/API+Referenceor a folder URL likehttps://company.atlassian.net/wiki/spaces/PROJ/folder/123456789
Extract the page ID directly from the URL (the numeric segment, e.g., 123456789).
Validate the directory:
index.mdmust exist in the provided path- At least one subdirectory with
.mdfiles must exist
Do NOT ask for Confluence URL, email, or API token — those are resolved automatically in the next step.
Step 2: Verify Authentication and Resolve Credentials
acli auth statusIf not authenticated or acli not found:
- Not installed → tell user:
brew install atlassian/tap/aclior visit https://developer.atlassian.com/cloud/acli/install/ - Not authenticated → guide user to run:
acli auth login
From the output, extract:
- CONFLUENCE_URL —
Site:field prefixed withhttps://, e.g.,company.atlassian.net→https://company.atlassian.net - EMAIL —
Email:field, e.g.,user@company.com
Why REST API for writes?acli confluence pagecurrently only supportsview. For page create/update we use Confluence REST API viacurl. URL and email come fromacli auth status; only the API token needs to be resolved (at write time).
Step 3: Read Directory Structure
Scan the docs/api/ directory to build the page list. No heading-based parsing needed — the file/directory structure is the source of truth.
Scanning Steps
- Read
index.md— extract service name (from H1), overview paragraph, and Common Error Responses section - List subdirectories — each subdirectory = one domain group (e.g.,
consent/→ "Consent"). Skiphealth/— health check endpoints are infrastructure-only and not synced to Confluence. - List
.mdfiles per subdirectory — each file = one API endpoint page - Extract page title per endpoint — read the Method and Path fields from the file, then format as
METHOD: /path(e.g.,POST: /api/v1/consents). This is the Confluence page title — not the H1 heading. - Extract page content per endpoint — use the full file content, but:
- Strip the breadcrumb line (first line starting with >) - Strip the H1 heading (used as in-page heading, not page title)
Page Types
| Source | Page Type | Title | Content |
|---|---|---|---|
| Group directory | Domain group | Directory name → Title Case (e.g., consent → "Consent") | Brief intro or empty |
Endpoint .md file | API page | METHOD: /path from Method + Path fields (e.g., POST: /api/v1/consents) | File content (minus breadcrumb) |
index.md overview | Parent page content | Service name from H1 | Overview + Common Errors (appended to parent page) |
Summary Output
After scanning, show the user a structured summary:
Found N domain groups, M individual API endpoints:
Consent (5 APIs)
POST: /api/v1/consents
GET: /api/v1/consents/:citizen_id
GET: /api/v1/consents/:id
GET: /api/v1/consents/:id/history
DELETE: /api/v1/consents/:id/revoke
Channel (5 APIs)
POST: /api/v1/channels
GET: /api/v1/channels
...
Purpose (11 APIs)
POST: /api/v1/purposes
GET: /api/v1/purposes
...
Total pages to create/update: N (domain groups) + M (APIs) = T pagesAsk the user to confirm or specify which sections to sync (all, specific domains, or specific APIs).
Step 4: Map Sections to Confluence Page Hierarchy
The page structure in Confluence mirrors the directory structure:
Parent page (provided by user)
├── Domain Group pages (directories) ← first-level children
│ └── Individual API pages (files) ← second-level childrenDiscover Existing Pages
First, fetch all children (and grandchildren) under the parent page to find existing pages:
# Get direct children of parent page
curl -s "${CONFLUENCE_URL}/wiki/rest/api/content/${PARENT_PAGE_ID}?expand=space,children.page" \
-u "${EMAIL}:${API_TOKEN}"From the response extract:
space.key→ save asSPACE_KEY(needed for creating new pages)children.page.results[]→ list of{id, title}for direct children
For each direct child that looks like a domain group, also fetch its children:
curl -s "${CONFLUENCE_URL}/wiki/rest/api/content/${DOMAIN_GROUP_PAGE_ID}/child/page" \
-u "${EMAIL}:${API_TOKEN}"Build the Mapping
Match existing page titles against scanned sections to build the mapping:
| Source | Page Title | Type | Matched Page ID | Action |
|---|---|---|---|---|
| consent/ | Consent | Domain group | 456789 | Update |
| consent/accept-consent.md | POST: /api/v1/consents | API page | 567890 | Update |
| consent/revoke-consent.md | DELETE: /api/v1/consents/:id/revoke | API page | — | Create (under 456789) |
| purpose/ | Purpose | Domain group | — | Create (under parent) |
| purpose/create-purpose.md | POST: /api/v1/purposes | API page | — | Create (under new domain group page) |
Important ordering: When creating new pages, domain group pages must be created before their child API pages (because child pages need the parent's page ID as ancestor).
Matching Strategy
- Match by exact page title (e.g.,
POST: /api/v1/consentsmatches existing page with same title) - If ambiguous, show the user and ask them to confirm the mapping
- For unmatched sections → mark as "Create new"
Step 5: Get Current Page Versions
For each page in the mapping, fetch its current version number (required for updates):
acli confluence page view --id <PAGE_ID> --include-version --jsonExtract version.number from the JSON output. Store as CURRENT_VERSION per page.
Step 6: Convert Markdown to Confluence Storage Format
For each endpoint file, convert the Markdown content to Confluence storage format (XHTML-based).
Pre-processing
Before conversion, strip from each endpoint file:
- Breadcrumb line — first line starting with
>(e.g.,> [API Documentation](../index.md) >...) - H1 heading — first
#line (used as page title, not body content)
Conversion Rules
| Markdown | Confluence Storage | |||
|---|---|---|---|---|
``` `lang\ncode\n` ``` | <ac:structured-macro ac:name="code"><ac:parameter ac:name="language">lang</ac:parameter><ac:plain-text-body><![CDATA[code]]></ac:plain-text-body></ac:structured-macro> | |||
**bold** | <strong>bold</strong> | |||
*italic* | <em>italic</em> | |||
[text](url) | <a href="url">text</a> | |||
## Heading | <h2>Heading</h2> | |||
| `\ | col \ | col \ | ` table | <table><tbody><tr><td>...</td></tr></tbody></table> |
For js, javascript, sh, bash, json, yaml code blocks, map to the Confluence language name accordingly (bash for sh, javascript for js).
Step 7: Sync Pages (Create + Update) via REST API
Before writing, resolve the API token:
echo $CONFLUENCE_API_TOKEN- If set → use it silently, no need to ask
- If empty → ask the user once: "ต้องการ API token สำหรับ write ผ่าน Confluence REST API (acli ยังไม่ support page write) — generate ได้ที่ https://id.atlassian.com/manage-profile/security/api-tokens"
Use EMAIL extracted from acli auth status in Step 2.
Execution Order (critical for parent-child hierarchy)
- First pass — Domain group pages: Create or update all domain group pages as children of the parent page. This ensures parent page IDs exist before creating child API pages.
- Second pass — Individual API pages: Create or update all endpoint pages as children of their respective domain group pages.
Creating a New Page
curl -s -X POST \
"${CONFLUENCE_URL}/wiki/rest/api/content" \
-u "${EMAIL}:${API_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"type\": \"page\",
\"title\": \"${PAGE_TITLE}\",
\"ancestors\": [{\"id\": \"${ANCESTOR_PAGE_ID}\"}],
\"space\": {\"key\": \"${SPACE_KEY}\"},
\"body\": {
\"storage\": {
\"value\": \"${ESCAPED_HTML}\",
\"representation\": \"storage\"
}
}
}"- For domain group pages:
ANCESTOR_PAGE_ID= user-provided parent page ID - For individual API pages:
ANCESTOR_PAGE_ID= the domain group page ID (created in first pass)
Extract id from the response to use as ancestor for child pages.
Updating an Existing Page
curl -s -X PUT \
"${CONFLUENCE_URL}/wiki/rest/api/content/${PAGE_ID}" \
-u "${EMAIL}:${API_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"version\": {\"number\": $((CURRENT_VERSION + 1))},
\"title\": \"${PAGE_TITLE}\",
\"type\": \"page\",
\"body\": {
\"storage\": {
\"value\": \"${ESCAPED_HTML}\",
\"representation\": \"storage\"
}
}
}"Check HTTP status — 200 means success.
Content comparison tip: Before updating, compare normalized content (collapse whitespace) to skip pages with no real changes. This avoids unnecessary version bumps.
Step 8: Report Results
Print a summary table after all operations:
| Page Title | Type | Page ID | Status |
|-----------------------------------------|----------------|------------|-------------------------|
| Consent | Domain group | 456789 | Updated (v3 → v4) |
| POST: /api/v1/consents | API page | 567890 | Updated (v2 → v3) |
| DELETE: /api/v1/consents/:id/revoke | API page | 678901 | Created |
| GET: /api/v1/consents/:citizen_id | API page | 567890 | Skipped (no changes) |
| Purpose | Domain group | 789012 | Created |
| POST: /api/v1/purposes | API page | 890123 | Created |Total: N domain groups, M API pages updated, K created, J skipped, F failed.
Error Reference
| Scenario | Action |
|---|---|
acli not found | Install via brew install atlassian/tap/acli |
acli auth status fails | Run acli auth login |
API doc directory not found or missing index.md | Re-ask for correct directory path |
| HTTP 401 on REST call | Check API token — re-check $CONFLUENCE_API_TOKEN or ask user |
| HTTP 404 on page | Verify page ID is correct; page may have been deleted |
| HTTP 409 version conflict | Re-fetch version with acli and retry |
| Section title has no match | Ask user to manually provide page ID |