# Confluence API Doc

> Sync API documentation from a docs/api/ directory (multi-file structure generated by api-doc-gen) to Confluence pages using acli + REST API. Each endpoint file becomes one Confluence page, grouped under domain parent pages. Prompts for directory path, parent page URL, and credentials at runtime — no pre-configured environment variables required. Use this skill whenever: uploading or syncing API docs to Confluence, updating Confluence pages from docs/api/, publishing documentation, "sync api doc", "push doc to confluence", "sync docs/api/ to confluence", "อัปเดต api doc ไป confluence", "sync confluence pages", "confluence-api-doc", or when the user wants to publish or update any API documentation to Confluence.

- Skill: `jojious/confluence-api-doc` (Agent Skill)
- Install (CLI): `npx skillmds@latest add jojious/confluence-api-doc`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jojious/confluence-api-doc/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: jojious (https://skillmd.com/u/jojious)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/jojious/confluence-api-doc

---


# 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 Purposes
```

Each `.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):

1. **API doc directory path** — e.g., `docs/api/` (relative to project root, or absolute). Default: `docs/api/`
2. **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+Reference` or a folder URL like `https://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.md` must exist in the provided path
- At least one subdirectory with `.md` files 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

```bash
acli auth status
```

If not authenticated or acli not found:
- Not installed → tell user: `brew install atlassian/tap/acli` or 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 with `https://`, e.g., `company.atlassian.net` → `https://company.atlassian.net`
- **EMAIL** — `Email:` field, e.g., `user@company.com`

> **Why REST API for writes?** `acli confluence page` currently only supports `view`. For page create/update we use Confluence REST API via `curl`. URL and email come from `acli 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

1. **Read `index.md`** — extract service name (from H1), overview paragraph, and Common Error Responses section
2. **List subdirectories** — each subdirectory = one domain group (e.g., `consent/` → "Consent"). Skip `health/` — health check endpoints are infrastructure-only and not synced to Confluence.
3. **List `.md` files per subdirectory** — each file = one API endpoint page
4. **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.
5. **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 pages
```

Ask 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 children
```

### Discover Existing Pages

First, fetch all children (and grandchildren) under the parent page to find existing pages:

```bash
# 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 as `SPACE_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:
```bash
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/consents` matches 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):

```bash
acli confluence page view --id <PAGE_ID> --include-version --json
```

Extract `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:
1. **Breadcrumb line** — first line starting with `>` (e.g., `> [API Documentation](../index.md) > ...`)
2. **H1 heading** — first `# ` line (used as page title, not body content)

### Conversion Rules

**CRITICAL — Processing order matters. Follow these phases strictly:**

#### Phase 1: Extract code blocks FIRST (before any other conversion)

Scan the entire Markdown content for fenced code blocks (` ``` ` pairs). For each block:

1. Capture the language identifier (e.g., `json`, `bash`, `yaml`)
2. Capture ALL lines between the opening and closing ` ``` ` **verbatim** — do NOT apply any HTML conversion to these lines
3. Replace the entire block (including fences) with the Confluence code macro:

```xml
<ac:structured-macro ac:name="code">
  <ac:parameter ac:name="language">LANG</ac:parameter>
  <ac:plain-text-body><![CDATA[...entire code content verbatim...]]></ac:plain-text-body>
</ac:structured-macro>
```

**Rules for code blocks:**
- Content inside `<![CDATA[...]]>` must be the raw text — **never** wrap lines in `<p>`, `<br/>`, or any HTML tags
- Preserve original indentation and newlines exactly as-is
- Language mapping: `sh` → `bash`, `js` → `javascript`; all others use their name directly
- Code blocks without a language identifier: omit the `<ac:parameter>` line

#### Phase 2: Convert remaining Markdown (non-code-block content only)

| Markdown | Confluence Storage |
|---|---|
| `**bold**` | `<strong>bold</strong>` |
| `*italic*` | `<em>italic</em>` |
| `[[text](url)]` | `<a href="url">text</a>` — **process before normal links** (used in type columns like `array[[Type](#anchor)]`) |
| `[text](url)` | `<a href="url">text</a>` |
| `## Heading` | `<h2>Heading</h2>` |
| `\| col \| col \|` table | `<table><tbody><tr><td>...</td></tr></tbody></table>` |
| Plain text paragraphs (separated by blank lines) | `<p>text</p>` |
| `- item` / `* item` unordered list | `<ul><li>item</li></ul>` |
| `1. item` ordered list | `<ol><li>item</li></ol>` |
| `` `inline code` `` | `<code>inline code</code>` |

**Nested list handling (critical):**

When an ordered list item contains indented sub-items (e.g., `- ` bullets indented under a `1. ` item), the sub-items become a `<ul>` **inside** that `<li>`, and the `<li>` is then closed. Subsequent numbered items (`5. `, `6. `, etc.) must continue as siblings in the **same parent `<ol>`** — do NOT nest them inside the previous item's sub-list.

Example input:
```
4. Validate collection point
   - Check active status
   - Check purpose mapping
5. Create consent record
6. Return response
```

Correct output:
```html
<ol>
  <li>Validate collection point
    <ul><li>Check active status</li><li>Check purpose mapping</li></ul>
  </li>
  <li>Create consent record</li>
  <li>Return response</li>
</ol>
```

Wrong output (items 5-6 nested inside item 4's sub-list):
```html
<ol>
  <li>Validate collection point
    <ul><li>Check active status</li><li>Check purpose mapping</li>
      <li>Create consent record</li><li>Return response</li>
    </ul>
  </li>
</ol>
```

## Step 7: Sync Pages (Create + Update) via REST API

Before writing, resolve the API token:

```bash
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)

1. **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.
2. **Second pass — Individual API pages**: Create or update all endpoint pages as children of their respective domain group pages.

### Creating a New Page

```bash
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

```bash
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 |

