---
title: "Confluence Permissions Migration: API Examples & Edge Cases"
slug: confluence-permissions-migration-api-examples-edge-cases
date: 2026-10-20
author: Rishabh Makhar
categories: [Confluence, Atlassian, Migration Guide]
excerpt: "How to migrate Confluence space permissions, page restrictions, and RBAC roles via the REST API — with code examples and the edge cases that break migrations."
tldr: "Confluence permissions span three layers with asymmetric inheritance — migrate via V1/V2 REST APIs, but always compute effective access including ancestor restrictions before cutover."
canonical: https://clonepartner.com/blog/confluence-permissions-migration-api-examples-edge-cases
---

# Confluence Permissions Migration: API Examples & Edge Cases


**Confluence permissions are the hardest part of any Confluence migration to get right — and the easiest to silently get wrong.** Content migrates as structured data. Permissions migrate as a tangled graph of space-level grants, page-level restrictions, group memberships, and inheritance rules that interact in non-obvious ways. Drop a single page restriction during migration and you've exposed confidential content. Over-restrict and your users can't find their own documents.

This guide covers the real architecture of Confluence's permission model, the exact API endpoints for reading and writing permissions at every level, the OAuth scopes required for each operation, identity mapping patterns including edge cases, rate limit handling, an API error taxonomy, and the edge cases that break even experienced engineers' migration scripts — including RBAC roles and the V2 Content Permissions API.

> [!NOTE]
> **Scope:** This guide focuses on **Confluence Cloud** as the primary reference. Confluence Data Center uses different API base paths (`/rest/api/` vs `/wiki/api/v2/`), different authentication (Personal Access Tokens instead of OAuth 2.0 / API tokens), and different rate-limit behavior. Where Data Center behavior diverges materially, it's called out inline.

If you're migrating out of the Atlassian ecosystem entirely, see [Confluence to SharePoint Migration: Methods, Limits & Macro Mapping](https://clonepartner.com/blog/blog/confluence-to-sharepoint-migration-methods-limits-macro-mapping) or [Confluence to Slack Canvas Migration: The Technical Guide](https://clonepartner.com/blog/blog/confluence-to-slack-canvas-migration-the-technical-guide) for platform-specific permission translation strategies.

## How Confluence Permissions Actually Work: The Three-Layer Model

**Confluence permissions operate on three independent layers: global permissions, space permissions, and page restrictions.** These layers interact with additive-then-subtractive logic that confuses even seasoned admins. ([support.atlassian.com](https://support.atlassian.com/confluence-cloud/docs/what-are-confluence-cloud-permissions-and-restrictions))

| Layer | Controls | Can Widen Access? | Can Narrow Access? |
|-------|----------|-------------------|--------------------|
| **Global** | Site-wide access ("Can use" Confluence) | Yes — grants site entry | No — only grants or withholds entry |
| **Space** | Per-space operations: view, edit, create, delete, export, administer | Yes — grants capabilities within a space | No — only additive within its layer |
| **Page restrictions** | Per-page view and edit access | No — cannot grant more than the space allows | Yes — narrows access below space-level grants |

**Global permissions** control whether someone can use Confluence at all. They don't behave like space permissions or page restrictions.

**Space permissions** set the floor — what a group or user can do within a space. If a user lacks the **View Pages** space permission, they cannot see any page in that space regardless of page-level settings.

**Page restrictions** are exceptions that can only reduce access. Confluence supports two types: **View** (restricts who can see the page) and **Edit** (restricts who can modify it). A page with a view restriction set to a single named user will be invisible to everyone else in the space — including users with full space-level edit permissions.

Access is also **additive across sources**. A person can receive access directly and through groups, teams, or user classes, and their effective access combines those sources. Deleting one direct grant doesn't necessarily remove access if the same person still inherits the right through a group. ([support.atlassian.com](https://support.atlassian.com/confluence-cloud/docs/faq-role-based-access-in-confluence/))

**The `restrict` space permission is distinct from page-level restrictions** and is frequently overlooked in migrations. It controls which users are allowed to *apply* page restrictions — i.e., who can lock down pages. If you replicate space permissions but drop the `restrict` permission, users who previously could apply page restrictions lose that capability silently.

This asymmetry is the first thing to internalize before writing migration code. You must migrate all three layers, in order: global access, then space permissions (including `restrict`), then page restrictions.

### What inherits and what doesn't

The edit page restriction is not inherited by child pages — only view restrictions propagate down the page tree. This is the single most common source of confusion in permission migrations.

If you restrict viewing on a parent page to User A and User B, all child pages inherit that view restriction. But if you restrict *editing* on that parent, child pages remain editable by anyone with space-level edit permissions.

Attachments follow page visibility: if a user cannot view the page an attachment lives on, the attachment link is not rendered. ([support.atlassian.com](https://support.atlassian.com/confluence-cloud/docs/add-or-remove-page-restrictions))

**Pages created from Blueprints or templates can have restrictions baked in at creation time.** These restrictions don't appear in the page restriction API response in the same way as manually applied restrictions — they're applied at page creation and look like direct restrictions. If your source space uses templates that embed restriction logic, verify that the restriction entries exist on individual pages (not just on the template) before assuming they'll migrate correctly.

For migration, this means you cannot reconstruct effective permissions from page-level data alone. You must walk the ancestor chain for every page to compute inherited view restrictions.

## The Identity Mapping Problem

You cannot copy a permission payload from a source Confluence instance and POST it to a target. The identities will not match.

Confluence Cloud uses **Atlassian Account IDs (AAIDs)** for users and unique UUIDs for groups. An AAID in Tenant A is entirely different from the AAID for the same person in Tenant B, unless both tenants share an Atlassian Organization and Atlassian Access configuration.

To migrate permissions, you must build a translation table: export the user list from the source (mapping AAID to email), export the user list from the target (mapping email to new AAID), and rewrite every permission payload through that table before pushing it to the destination.

```python
def build_identity_map(source_users: list, target_users: list) -> tuple:
    """Map source accountIds to target accountIds by email (primary) and
    display name (fallback). Returns (identity_map, unmapped_users)."""
    target_by_email = {u["email"]: u["accountId"] for u in target_users}
    target_by_display = {u.get("displayName", "").lower(): u["accountId"]
                         for u in target_users}
    identity_map = {}
    unmapped = []

    for user in source_users:
        email = user.get("email", "")
        display = user.get("displayName", "").lower()

        if email in target_by_email:
            identity_map[user["accountId"]] = target_by_email[email]
        elif display and display in target_by_display:
            # Fallback: display name match — flag for manual review
            identity_map[user["accountId"]] = target_by_display[display]
            user["match_method"] = "display_name_fallback"
            unmapped.append(user)  # Still log for review
        else:
            unmapped.append(user)

    return identity_map, unmapped
```

**When email matching fails**, escalate in this order:
1. **Display name match** — use as a fallback, but flag every match for human review before applying permissions
2. **SSO attribute comparison** — if both tenants use the same IdP (e.g., Okta, Entra ID), query the IdP for a canonical identifier (employee ID, UPN) and match on that
3. **Service accounts and app users** — filter these out of the identity map entirely; they require explicit remapping decisions, not automated matching
4. **No match found** — do not silently drop the restriction entry; log it to an unmapped-principals report and apply the fallback policy defined before migration starts

> [!WARNING]
> **Deactivated users are invisible in the target.** If a source instance has page restrictions referencing deactivated users, those restrictions still apply in the source but the users won't exist in the target. You must decide: drop the restriction entry (which widens access) or substitute a service account (which preserves the lock). Neither option is neutral — define a fallback policy before you start.

> [!CAUTION]
> **Most failed permission migrations are identity failures, not API failures.** If source users, groups, guests, or external collaborators don't line up with real destination principals, your script can return HTTP 200 and still create the wrong security model.

**Guest and external collaborator permissions** require separate handling. In Confluence Cloud, guests have site-level guest access and may have individual page shares rather than space-level permissions. The guest share relationship is not exposed through the same space permission endpoints — it surfaces through the content sharing APIs. If your source space has guest-shared pages, enumerate them separately and decide whether the target instance supports external sharing at all before attempting to replicate.

**Group name collisions** are a related failure mode: two Confluence instances can have groups with identical names but different membership rosters. Never migrate group-based permissions by name without first comparing group membership between source and target. Export group members from both sides via `GET /wiki/rest/api/group/{groupId}/membersByGroupId` and diff the lists before treating a name match as a safe mapping.

## Which API Should You Use?

This depends on what your target instance looks like. Confluence Cloud now has three access modes: **pre-roles** (legacy permissions only), **roles transition** (legacy + roles side by side), and **roles only** (roles fully replace the legacy permission matrix). ([support.atlassian.com](https://support.atlassian.com/confluence-cloud/docs/faq-role-based-access-in-confluence/))

**Detect the target mode first.** Call the Space Role Mode endpoint to determine which mode the destination uses:

```bash
curl -sS -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
  -H "Accept: application/json" \
  "https://$SITE.atlassian.net/wiki/api/v2/spaces/$SPACE_ID/role-assignments"
```

If the endpoint returns role assignments, the space is operating in RBAC mode. If the call returns a 404 or empty set while the space clearly has permissions, the space is in legacy (pre-roles) mode. Check the Atlassian changelog for the current rollout status of roles-only mode for your tenant tier. ([developer.atlassian.com](https://developer.atlassian.com/cloud/confluence/changelog/))

If the source and destination sites are in different modes, do not assume the migration preserves the same access semantics. Atlassian's migration guidance says the destination mode after migration can vary based on the source mode, destination mode, and migration type. ([support.atlassian.com](https://support.atlassian.com/confluence-cloud/docs/what-to-expect-from-roles-when-migrating-confluence-data/))

**General guidance:**

- Use the **V2 role APIs** for space access on RBAC tenants
- Use the **V2 content permission APIs** for new restriction automation
- Use the **V1 restriction APIs** only when maintaining older Cloud scripts or when the V2 rollout hasn't reached your tenant

Atlassian explicitly recommends the V2 content permission endpoints for new integrations and recommends updating scripts that assign individual space permissions to use the role APIs before a site moves to roles-only. ([developer.atlassian.com](https://developer.atlassian.com/cloud/confluence/changelog/))

## OAuth 2.0 Scopes Required for Permission Migration

A complete permission migration script requires the following OAuth 2.0 scopes on Confluence Cloud. Missing any of these causes silent failures or 403 errors on specific operations:

| Operation | Endpoint | Required Scope |
|-----------|----------|---------------|
| Read space permissions | `GET /wiki/api/v2/spaces/{id}/permissions` | `read:space:confluence` |
| Write space permissions (V1) | `POST /wiki/rest/api/space/{key}/permission` | `write:space:confluence` |
| Read space role assignments | `GET /wiki/api/v2/spaces/{id}/role-assignments` | `read:space:confluence` |
| Write space role assignments | `POST /wiki/api/v2/spaces/{id}/role-assignments` | `write:space:confluence` |
| Read page restrictions | `GET /wiki/rest/api/content/{id}/restriction` | `read:content:confluence` |
| Write page restrictions | `POST /wiki/rest/api/content/{id}/restriction` | `write:content:confluence` |
| Check effective permissions | `POST /wiki/rest/api/content/{id}/permission/check` | `read:content:confluence` |
| Read group members | `GET /wiki/rest/api/group/{groupId}/membersByGroupId` | `read:group:confluence` |
| Read user details | `GET /wiki/rest/api/user` | `read:user:confluence` |

For admin-level operations — including reading restrictions on pages your app user isn't explicitly granted access to — you additionally need `read:confluence-content.all` (Classic scope) or the equivalent granular admin scope. Without this, restricted pages are invisible in API responses with no error signal.

When configuring an OAuth 2.0 (3LO) app for migration, request all scopes at authorization time. Scope additions after initial authorization require re-authorization.

## How to Read Space Permissions via API

**Space permissions define what each user, group, or user class can do within a Confluence space.** The V2 REST API exposes these through a dedicated endpoint.

### V2 API (recommended)

The `GET /spaces/{id}/permissions` endpoint returns space permission assignments for a specific space, requires permission to view the space, and uses the `read:space:confluence` OAuth scope.

```python
import requests

BASE_URL = "https://your-instance.atlassian.net/wiki/api/v2"
AUTH = ("your-email@example.com", "your-api-token")

def get_space_permissions(space_id: int) -> list:
    """Fetch all permission assignments for a space."""
    permissions = []
    url = f"{BASE_URL}/spaces/{space_id}/permissions"
    while url:
        resp = requests.get(url, auth=AUTH, headers={"Accept": "application/json"})
        resp.raise_for_status()
        data = resp.json()
        permissions.extend(data.get("results", []))
        url = data.get("_links", {}).get("next")
    return permissions
```

> [!WARNING]
> **Cloud V2 uses integer space IDs, not space keys.** If you only have the space key (e.g., `ENG`), resolve it first via `GET /wiki/api/v2/spaces?keys=ENG` to get the numeric ID.

For role-based spaces, use `GET /wiki/api/v2/spaces/{id}/role-assignments` to inventory role assignments. To resolve group membership, use `GET /wiki/rest/api/group/{groupId}/membersByGroupId`. ([developer.atlassian.com](https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-space-roles/))

### V1 API (legacy)

`GET /wiki/rest/api/space/{spaceKey}?expand=permissions` returns space details with a `permissions` array. This endpoint is still heavily used but doesn't support the RBAC model.

## How to Write Space Permissions

### Granular permissions (V1 API)

The V1 `POST /wiki/rest/api/space/{spaceKey}/permission` endpoint adds new permissions to a space. If the permission is a group permission, the group can be identified by its group name or group ID. Apps cannot access this REST resource — including when utilizing user impersonation. It requires 'Admin' permission for the space.

```python
def add_space_permission(space_key: str, account_id: str, operation: str, target: str):
    """Add a single space permission via V1 API.

    operation: 'read', 'create', 'delete', 'administer', etc.
    target: 'space', 'page', 'blogpost', 'comment', 'attachment'
    """
    url = f"https://your-instance.atlassian.net/wiki/rest/api/space/{space_key}/permission"
    payload = {
        "subject": {
            "type": "user",
            "identifier": account_id
        },
        "operation": {
            "key": operation,
            "target": target
        }
    }
    resp = requests.post(url, json=payload, auth=AUTH,
                         headers={"Content-Type": "application/json"})
    resp.raise_for_status()
    return resp.json()
```

**Supported operation/target pairs for V1 space permissions:**

| Operation key | Target | What it grants |
|--------------|--------|----------------|
| `read` | `space` | View pages, blog posts, and space content |
| `administer` | `space` | Administer the space |
| `export` | `space` | Export space content |
| `restrict` | `space` | Apply page restrictions |
| `delete_own` | `space` | Delete own content |
| `delete_mail` | `space` | Delete mail |
| `create` | `page` | Create pages |
| `delete` | `page` | Delete pages |
| `create` | `blogpost` | Create blog posts |
| `delete` | `blogpost` | Delete blog posts |
| `create` | `comment` | Create comments |
| `delete` | `comment` | Delete comments |
| `create` | `attachment` | Create attachments |
| `delete` | `attachment` | Delete attachments |

Do not omit the `restrict` / `space` pair. It controls who can lock down pages and is frequently dropped in migrations that treat "view" and "edit" as the only meaningful permission categories.

### Role assignments (V2 API for RBAC sites)

If the target instance is in roles-only mode, you cannot assign granular permissions — you must map source permission sets to roles.

**Role-Based Access Control (RBAC)** replaces Confluence's legacy 14-checkbox permission matrix with four default roles — **Admin**, **Manager**, **Collaborator**, and **Viewer** — plus up to 10 custom roles per site. ([support.atlassian.com](https://support.atlassian.com/confluence-cloud/docs/faq-role-based-access-in-confluence/))

The default roles map to legacy permissions approximately as follows:

| Role | Equivalent legacy permissions |
|------|------------------------------|
| **Admin** | All permissions including space administration |
| **Manager** | All non-admin permissions including delete |
| **Collaborator** | Create and edit pages, comments, attachments; cannot delete |
| **Viewer** | Read-only: view pages and blog posts |

Legacy permission combinations that don't cleanly map to one of these four roles — for example, a user with "create pages" but not "edit" or "comment" — result in **Custom access** rather than a clean role assignment. Custom access states require manual resolution after migration and cannot be automated at scale.

Assign roles to **groups** rather than individual users wherever possible. This keeps identity management in the directory and permission management in Confluence — easier to audit and maintain after cutover.

```bash
curl -sS -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -X POST "https://$SITE.atlassian.net/wiki/api/v2/spaces/$SPACE_ID/role-assignments" \
  -d '[{"principal":{"principalType":"GROUP","principalId":"GROUP-UUID"},"roleId":"viewer-role-id"}]'
```

If you omit `roleId` for a principal, the V2 API removes that principal's assignment. ([developer.atlassian.com](https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-space-roles/))

> [!WARNING]
> **Custom access trap:** Legacy scripts that keep writing granular permissions on a role-based site push users into **Custom access** instead of clean role assignments. If you're modernizing a pipeline, cut over the old permission-writing jobs too — not just the migration script. ([support.atlassian.com](https://support.atlassian.com/confluence-cloud/docs/faq-role-based-access-in-confluence/))

Translate recurring access patterns into the smallest role set you can defend. A source user with "view + create pages" but not "edit" doesn't map cleanly to any default role. Anything that doesn't fit should become a small set of custom roles (max 10 per site) — not a long tail of one-off exceptions.

## How to Read Page Restrictions

**Page restrictions (called "content permissions" in the V2 API) control who can view or edit a specific page, independent of space permissions.** This is where most migration bugs hide.

### V1 API

The `GET /wiki/rest/api/content/{id}/restriction` endpoint returns the restrictions on a piece of content and requires permission to view the content. Required OAuth scope: `read:content:confluence`.

```python
def get_page_restrictions(page_id: str) -> dict:
    """Get direct restrictions on a specific page."""
    url = f"https://your-instance.atlassian.net/wiki/rest/api/content/{page_id}/restriction"
    resp = requests.get(url, auth=AUTH, headers={"Accept": "application/json"})
    resp.raise_for_status()
    return resp.json()
```

> [!CAUTION]
> **The restrictions API does not return inherited restrictions.** To determine whether a page has inherited restrictions, you must walk the ancestor chain and check restrictions on each ancestor individually. A page that appears unrestricted in the API response may still be invisible to most users because a parent three levels up has a view restriction.

Here's the pattern for computing effective view restrictions, including inheritance:

```python
def get_effective_view_restrictions(page_id: str) -> dict:
    """Compute effective view restrictions including inherited ones."""
    direct = get_page_restrictions(page_id)

    ancestors_url = f"https://your-instance.atlassian.net/wiki/rest/api/content/{page_id}?expand=ancestors"
    resp = requests.get(ancestors_url, auth=AUTH, headers={"Accept": "application/json"})
    resp.raise_for_status()
    ancestors = resp.json().get("ancestors", [])

    inherited_restrictions = []
    for ancestor in ancestors:
        ancestor_restrictions = get_page_restrictions(ancestor["id"])
        read_ops = ancestor_restrictions.get("read", {}).get("restrictions", {})
        if read_ops.get("user", {}).get("results") or read_ops.get("group", {}).get("results"):
            inherited_restrictions.append({
                "ancestor_id": ancestor["id"],
                "ancestor_title": ancestor.get("title", ""),
                "restrictions": read_ops
            })

    return {
        "page_id": page_id,
        "direct_restrictions": direct,
        "inherited_view_restrictions": inherited_restrictions
    }
```

Because spaces can contain tens of thousands of pages, you must iterate through every page and query its restrictions. Always paginate using `start` and `limit` parameters (V1) or cursor-based pagination (V2), and follow `_links.next` until the list is exhausted.

> [!CAUTION]
> **Restrictions on content you can't see.** Pages that have restrictions don't show up when an app hits the content endpoint without admin-level access. If your migration service account doesn't have admin-level access, restricted pages are invisible in API responses — no error, no indication they exist. Run the export as a site admin using the `read:confluence-content.all` scope.

**Performance optimization — batch ancestor lookups:** Most pages in a space share a common ancestor tree. Rather than making per-page ancestor API calls, fetch the full page tree for a space once via `GET /wiki/api/v2/pages?space-id={id}&limit=250` with the `ancestor-id` field, reconstruct the tree in memory, then compute inherited restrictions top-down. This reduces ancestor-resolution API calls from O(pages × depth) to O(pages) for the initial fetch, plus O(unique ancestor nodes) for restriction checks.

## How to Write Page Restrictions

The `POST /wiki/rest/api/content/{id}/restriction` endpoint adds restrictions to a piece of content. It does not change any existing restrictions on the content and requires permission to edit the content. Required OAuth scope: `write:content:confluence`.

```python
def add_page_view_restriction(page_id: str, account_ids: list, group_names: list = None):
    """Restrict view access on a page to specific users and groups."""
    payload = [{
        "operation": "read",
        "restrictions": {
            "user": [{"type": "known", "accountId": aid} for aid in account_ids],
            "group": [{"type": "group", "name": name} for name in (group_names or [])]
        }
    }]
    url = f"https://your-instance.atlassian.net/wiki/rest/api/content/{page_id}/restriction"
    resp = requests.post(url, json=payload, auth=AUTH,
                         headers={"Content-Type": "application/json"})
    resp.raise_for_status()
    return resp.json()
```

> [!WARNING]
> **The POST method is additive.** If the target page already has restrictions, POSTing new ones adds to the list. To replace all restrictions atomically, use `PUT /wiki/rest/api/content/{id}/restriction` which overwrites the full restriction set. To remove a specific entry, use `DELETE /wiki/rest/api/content/{id}/restriction/byOperation/{operationKey}/user?accountId={accountId}`.

Apply restrictions in tree order — parent pages first — to preserve view restriction inheritance. Apply space permissions before page restrictions. Order matters: you cannot set meaningful page restrictions for a user unless they already have space-level access.

## The V2 Content Permissions API

Atlassian reworked content-level access control in Confluence Cloud, introducing separate APIs for interacting with **permissions** (what widens access) and the **restriction state** (what tightens access). This model is available for pages, blog posts, databases, whiteboards, and folders. ([developer.atlassian.com](https://developer.atlassian.com/cloud/confluence/changelog/))

### What's different

- **Two separate concepts:** "Restriction state" controls the general access mode (Open, Edit-restricted, View-restricted). "Permissions" control specific user/group entries within that state.
- Developers can now set the full array of content-level permission configurations. With the V1 APIs, developers could not model read or update permissions in an OPEN state, or read permissions in an EDIT_RESTRICTED state.
- All principals hold either only the read restriction (Can view) or both the read and update restrictions (Can edit). You cannot set an update restriction without the matching read restriction on view-restricted content.
- Attempts to remove only the `read` restriction from a principal that has both `read` and `update` will be rejected. Scripts written against older V1 behavior can produce surprising failures when the target page is in V2 mode.

> [!WARNING]
> **Breaking change in progress:** The V1 behavior change is rolling out tenant-by-tenant. The same API call can succeed on one Confluence site and fail on another during the rollout window. If your migration pipeline targets multiple tenants, you need error handling that accounts for both old and new behavior — specifically, catch the cases where removing a `read` restriction on a principal that holds both `read` and `update` returns a 400.

### Server/Data Center: No write API for restrictions before 8.8

Confluence Server doesn't have REST API endpoints to add or change content restrictions — only to read them. Only Confluence Cloud has those types of REST API endpoints.

The ability to update (PUT) content restrictions via the REST API was introduced in Confluence Data Center v8.8.0. For older versions (Confluence 6+), use the experimental endpoint at `/rest/experimental/content/{contentId}/restriction`. That endpoint uses the same body format, has been available since Confluence 6, and is still available in Confluence 9.

## API Error Taxonomy for Permission Operations

Permission migration scripts encounter a predictable set of errors. Here's what each means and how to handle it:

| HTTP Status | Scenario | Meaning | Correct Response |
|-------------|----------|---------|-----------------|
| `400 Bad Request` | POST restriction with unknown accountId | Principal doesn't exist in the target instance | Check identity map; this is an unmapped user — apply fallback policy |
| `400 Bad Request` | Remove `read` from principal with both `read` + `update` | V2 content permission constraint violation | Remove both `read` and `update` together, or use PUT to replace full set |
| `400 Bad Request` | Invalid operation/target pair | Unsupported permission combination | Check operation/target table; fix the payload |
| `403 Forbidden` | GET restriction on page you lack access to | Service account lacks admin access or `read:confluence-content.all` scope | Re-authenticate with admin account or add scope |
| `403 Forbidden` | POST space permission on non-admin account | Caller lacks space admin rights | Elevate service account to space admin before migration |
| `403 Forbidden` | App user hitting V1 space permission endpoint | Apps cannot use `/space/{key}/permission` with impersonation | Use OAuth user token, not app-only token |
| `404 Not Found` | GET restriction on page ID that doesn't exist | Page was deleted or ID is wrong | Log and skip; do not abort the full migration |
| `404 Not Found` | Space key not found | Space key mismatch between source and target | Verify space key mapping table |
| `409 Conflict` | PUT page update in approval-required space | Space requires publishing approval workflow | Save as draft, complete approval, then publish |
| `429 Too Many Requests` | Rate limit exceeded | Point quota exhausted for current hour | Read `Retry-After` header, sleep exact duration, retry same request |

**Never treat a 400 or 404 as a reason to abort the full migration.** Log it, apply the defined fallback, and continue. A single unmapped principal should not stop 10,000 correct permission writes.

## Rate Limits and Operational Constraints

Confluence Cloud uses a **points-based model** to measure API usage. Permission operations are particularly vulnerable because resolving a single page's effective permissions can require 3–5 API calls.

All quotas are measured in points per hour and reset at the top of each UTC hour. The default tier (Tier 1 — Global Pool) gives your app a single 65,000-point hourly quota shared across all tenants.

For app migration contexts specifically, Confluence enforces 6,000 requests per app per Confluence Cloud instance per minute, and 12,000 requests per Confluence Cloud instance per minute total.

**What that means in practice:** For a space with 5,000 pages, computing effective permissions requires roughly 5,000 calls for page restrictions, ~15,000 calls for ancestor chains (average depth of 3), and 5,000 calls for page metadata — approximately 25,000 API calls for the read phase alone. At 6,000 requests/minute, the permission audit takes roughly 4–5 minutes of API time. The write phase is comparable. A full migration of a 5,000-page space with read + write phases runs 8–10 minutes of API time under ideal conditions; real migrations are slower due to retries, error handling, and batching overhead.

When you hit the rate limit, Confluence returns a `429` status code and a `Retry-After` header. Your migration script must intercept every `429`, read that header, sleep the thread for the specified duration, and retry the exact request. Ignoring `Retry-After` and continuing to send requests can result in temporary IP blocks or token revocation.

Beyond rate limits, two other operational constraints break otherwise correct scripts:

- **Publishing approvals:** In approval-required spaces, direct REST API page updates return HTTP `409 Conflict`. Affected integrations must save drafts, complete approval, then publish. If your migration pipeline republishes pages while preserving restrictions, this matters. ([developer.atlassian.com](https://developer.atlassian.com/cloud/confluence/changelog/))
- **Group drift in phased migrations:** Membership changes to groups already migrated to Cloud are not reflected later. What was already migrated stays as-is, and admins must make equivalent group changes manually. ([support.atlassian.com](https://support.atlassian.com/migration/docs/how-confluence-users-and-groups-are-migrated/))

## Edge Cases That Break Permission Migrations

These are the failure modes that recur across real-world migrations:

**Anonymous access.** Confluence supports granting space permissions to anonymous users. Most target platforms don't. If your source space allows anonymous viewing, you must decide explicitly how to handle it — converting to "all logged-in users" is usually the closest equivalent.

**App user permissions.** Confluence Cloud spaces often have permissions granted to Connect or Forge app users. These app identities don't exist on the target instance. They fail silently during migration unless you filter them out or remap them.

**Group name collisions.** Two Confluence instances can have groups with the same name but different membership. Migrating group-based permissions by name without verifying membership leads to access grants to the wrong people. Always diff group membership via `GET /wiki/rest/api/group/{groupId}/membersByGroupId` before treating a name match as a safe mapping.

**The 14-checkbox to 4-role collapse.** If migrating into an RBAC roles-only instance, the four default roles (Admin, Manager, Collaborator, Viewer) each bundle a specific set of individual permissions. Permission combinations that don't match any default role result in "Custom access" states that need manual resolution.

**The `restrict` permission drop.** Migration scripts that enumerate only "read" and "create/edit/delete" style permissions miss the `restrict` / `space` pair, silently removing users' ability to apply page restrictions in the destination space.

**Space export doesn't migrate permissions.** Atlassian's Import a Confluence Cloud space flow does not import user accounts or groups. Group-based space or page permissions are lost unless the destination already has a group with the same name. A space import is not a permission migration strategy by itself. ([support.atlassian.com](https://support.atlassian.com/confluence-cloud/docs/import-a-confluence-cloud-space))

**Restrictions on content you can't see.** Pages that have restrictions don't appear when an API call hits the content endpoint without admin-level access. If your migration service account doesn't have admin-level access, restricted pages are invisible in API responses — no error, no indication they exist. Always run the export as a site admin.

**Blueprint-embedded restrictions.** Pages created from Blueprints or templates may have restrictions applied at creation by the template definition, not by subsequent admin actions. These look like direct restrictions in the API but won't be re-created if you migrate the page content without separately migrating the restriction entries. Verify restriction entries exist on individual pages — don't assume template metadata covers them.

**Guest and external collaborator shares.** Guest-shared pages don't surface through space permission endpoints. If your source has guest-shared content, enumerate it separately through the content sharing APIs and confirm the target instance supports external sharing before attempting replication.

## What Drops When Migrating Between Platforms

When migrating from Confluence to a different platform, permission translation is always lossy. The question is which losses you can accept.

| Target Platform | Space Permissions | Page View Restrictions | Page Edit Restrictions | Group-Based Access | Inheritance |
|----------------|-------------------|----------------------|----------------------|-------------------|-------------|
| **Confluence** (Cloud ↔ Cloud) | ✅ Full fidelity | ✅ Full fidelity | ✅ Full fidelity | ✅ If groups exist on target | ✅ Preserved |
| **Confluence** (Server → Cloud) | ⚠️ Requires RBAC mapping | ✅ Via API | ✅ Via API | ⚠️ Group name/ID mismatch | ✅ Preserved |
| **SharePoint** | ⚠️ Different model entirely | ⚠️ Requires item-level permissions | ⚠️ Separate mechanism | ⚠️ Entra ID groups | ⚠️ Inheritance-based |
| **Notion** | ⚠️ Teamspace-level only | ⚠️ Page-level sharing (Enterprise) | ⚠️ No edit-only restriction | ⚠️ Group concept differs | ❌ No inheritance |

For deeper coverage of cross-platform permission mapping, see [Migrating Permissions from SharePoint to Notion: A Step-by-Step Guide](https://clonepartner.com/blog/blog/migrating-permissions-from-sharepoint-to-notion-a-step-by-step-guide).

## What Changes on Confluence Data Center

Confluence Data Center uses **different base paths, different auth patterns, and different permission endpoints**. Do not copy Cloud examples into Data Center unchanged.

Data Center REST lives under `/rest/api/`, and Data Center supports **personal access tokens** from version 7.9+, passed as Bearer tokens. Cloud scripts typically authenticate with Atlassian email + API token using basic auth; Data Center scripts use PAT bearer auth or older username/password setups. ([developer.atlassian.com](https://developer.atlassian.com/server/confluence/rest/latest/))

```bash
# Space permissions (Data Center)
curl -sS -H "Authorization: Bearer $CONFLUENCE_PAT" \
  -H "Accept: application/json" \
  "https://confluence.example.com/rest/api/space/ENG/permissions"

# Page restrictions (Data Center)
curl -sS -H "Authorization: Bearer $CONFLUENCE_PAT" \
  -H "Accept: application/json" \
  "https://confluence.example.com/rest/api/content/123456/restriction/byOperation"
```

Data Center does not have an equivalent to the Cloud V2 roles API. Space permissions on Data Center remain the 14-checkbox model. There is no RBAC role mapping step when migrating Data Center to Data Center — but if [migrating Data Center to Cloud](https://clonepartner.com/blog/blog/confluence-server-to-cloud-migration-the-technical-guide) into an RBAC-enabled tenant, the full role mapping exercise applies.

A Cloud-to-Cloud permission migrator and a Data Center-to-Cloud permission migrator are related but not the same tool. The endpoint shapes, authentication, identity resolution, and target access model are different enough to warrant designing them separately.

## Validating Permission Accuracy After Migration

Permission migration validation requires comparing **effective access** — not just the permission entries themselves. Two different configurations can produce identical effective access, and two identical-looking configurations can produce different effective access due to group membership differences.

### Use the permission-check API

`POST /wiki/rest/api/content/{id}/permission/check` evaluates site permissions, space permissions, and content restrictions together and returns a single true/false verdict for a given subject and operation. This is the correct post-migration verification endpoint — it accounts for inheritance and multi-source additive access in one call. Required OAuth scope: `read:content:confluence`. ([developer.atlassian.com](https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-content-permissions/))

```bash
curl -sS -u "$ATLASSIAN_EMAIL:$ATLASSIAN_API_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -X POST "https://$SITE.atlassian.net/wiki/rest/api/content/$PAGE_ID/permission/check" \
  -d '{"subject":{"type":"user","identifier":"TARGET_AAID"},"operation":"read"}'
```

### Validation checklist

1. **Record-count parity:** Count permission entries per space on source and target. Mismatches indicate dropped or duplicated entries.
2. **Spot-check restricted pages:** For every page that had view restrictions on the source, verify the restriction exists on the target and references the correct mapped principals.
3. **Negative testing:** Confirm that a user who *shouldn't* have access to a restricted page actually receives a 403 or empty result.
4. **Group membership diffing:** For every group-based permission, verify the group exists on the target and has the expected members.
5. **Referential integrity:** Ensure no restriction references an AAID that doesn't exist in the target directory.
6. **Verify the `restrict` space permission:** Confirm that users who previously could apply page restrictions in the source space retain that capability in the destination.
7. **Check audit log alignment:** Atlassian's Admin audit log API records permission changes. Export the audit log from both instances post-migration and compare change events to identify gaps.

A good validation set is small and intentional: private HR pages, executive spaces, partner-facing spaces, public engineering docs, and one or two pages with nested restrictions and attachments. Test both `read` and `update` operations where edit rights matter. Run a random sample across at least five different user identities: a space admin, a regular editor, a view-only user, a user who appears in a restriction directly, and a user who only has access through a group.

For broader validation patterns, see [How to Preserve Version History in Knowledge Base Migrations](https://clonepartner.com/blog/blog/how-to-preserve-version-history-in-knowledge-base-migrations).

## When to Flatten Permissions During Migration

A migration is the best time to clean house. Replicating years of ad-hoc, user-level page restrictions degrades the search experience and creates administrative overhead in the new instance.

**Flatten permissions during migration** where possible. Instead of mapping individual user restrictions, map those users to functional groups (e.g., `finance-team`, `eng-leadership`) and apply restrictions to the group. If a page has restrictions tied exclusively to deactivated users, strip the restrictions entirely rather than migrating them to a dead account.

Groups are still the right unit for identity, even on role-based sites. Atlassian is explicit that roles don't replace groups: groups collect people together, while roles define what those people can do in a space. ([support.atlassian.com](https://support.atlassian.com/confluence-cloud/docs/faq-role-based-access-in-confluence/))

**When deciding whether to flatten vs preserve page-level restrictions:**

- If fewer than 20% of pages in a space have restrictions, the restriction graph is tractable — migrate it as-is and flatten opportunistically during the next admin cycle.
- If more than 50% of pages have restrictions, the access model has likely grown organically without design. Migrating it faithfully perpetuates the problem. Use the migration to rationalize access into 3–5 group-based permission tiers.
- Any page with restrictions referencing exclusively deactivated users should be treated as effectively unrestricted — strip the restrictions or move the page to an archive space.

> [!TIP]
> **A safe cutover pattern:** Freeze identity changes → run a sample migration on a test space → validate effective access with `permission/check` for at least 10 representative users → run the full migration → do one short post-cutover reconciliation pass for late content and late permission changes.

## When to Bring in Help

Permission migrations are tractable for small instances — a handful of spaces with simple group-based access. They get non-linear fast. An instance with 200 spaces, nested page restrictions, deactivated users, app-granted permissions, and an RBAC transition in progress is a different problem entirely.

If your migration involves more than 50 spaces with unique permission configurations, heavy use of page-level restrictions, identity mapping across identity providers (e.g., on-prem AD to Entra ID), an RBAC transition on the target, or compliance requirements mandating permission audit trails — you're looking at a custom engagement, not a weekend script.

ClonePartner handles Confluence permission migrations with multi-stage validation: field-level checks, record-count parity, referential integrity verification, and negative-access testing. Every project gets a dedicated migration engineer who validates the permission mapping before and after the move. Most migrations complete within 1–5 business days depending on data volume, complexity, and API rate limits. Fixed price per project — no hourly billing, no hidden fees, no per-record metering.

> Need to migrate Confluence permissions without exposing restricted content or locking out users? Book a free 30-minute scoping call and get a fixed-price proposal.
>
> [Talk to us](https://clonepartner.com/talk-to-us?duration=30&utm_source=blog&utm_medium=button&utm_campaign=demo_bookings&utm_content=cta_click&utm_term=demo_button_click)

## Frequently asked questions

### Does the Confluence API return inherited page restrictions?

No. The GET /wiki/rest/api/content/{id}/restriction endpoint only returns restrictions set directly on that page. To find inherited view restrictions, you must walk the ancestor chain by fetching each parent page's restrictions individually. Edit restrictions do not inherit to child pages at all — only view restrictions do.

### Can I migrate Confluence permissions with a space export?

Not fully. Atlassian's Cloud space import does not import user accounts or groups, and group-based space or page permissions are lost unless same-name groups already exist in the destination. A space import is not a permission migration strategy by itself.

### How does Confluence RBAC affect permission migration?

Confluence Cloud is transitioning from a 14-checkbox permission matrix to four default roles (Admin, Manager, Collaborator, Viewer) plus up to 10 custom roles. If the target instance is in Roles-only mode, you cannot assign granular permissions — you must map source permission sets to the closest matching role. Permission combinations that don't match any default role result in 'custom access' states.

### How do I validate effective access after a Confluence permission migration?

Use POST /wiki/rest/api/content/{id}/permission/check. This endpoint evaluates site permissions, space permissions, and content restrictions together and returns whether a given subject has the requested operation. Test against representative users, restricted pages, and both read and update operations.

### What are the Confluence Cloud API rate limits for permission migrations?

Confluence Cloud uses a points-based model. The default tier provides a shared 65,000-point hourly quota. For app migrations specifically, Atlassian enforces 6,000 requests per app per instance per minute and 12,000 requests per instance per minute total. Computing effective permissions for a 5,000-page space can require 25,000+ API calls for the read phase alone.
