Skip to main content
RapidDev - Software Development Agency
bolt-ai-integrationsBolt Chat + API Route

How to Integrate Bolt.new with Confluence

Connect Bolt.new to Confluence using the Confluence REST API v2 with an Atlassian API token and basic authentication (email + token, base64-encoded). Fetch spaces, pages, and blog posts to build custom documentation portals or knowledge base viewers. All outbound Confluence API calls work in Bolt's WebContainer preview. Page content is returned as Atlassian Document Format (ADF) JSON — convert it to HTML for rendering.

What you'll learn

  • How to create an Atlassian API token for Confluence authentication
  • How to fetch spaces and pages from Confluence's REST API v2
  • How to search Confluence content using the CQL query language
  • How to convert Confluence's ADF content format to displayable HTML
  • How to build a custom documentation portal backed by Confluence
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate19 min read25 minutesProductivityApril 2026RapidDev Engineering Team
TL;DR

Connect Bolt.new to Confluence using the Confluence REST API v2 with an Atlassian API token and basic authentication (email + token, base64-encoded). Fetch spaces, pages, and blog posts to build custom documentation portals or knowledge base viewers. All outbound Confluence API calls work in Bolt's WebContainer preview. Page content is returned as Atlassian Document Format (ADF) JSON — convert it to HTML for rendering.

Build Custom Documentation Portals and Knowledge Base Viewers from Confluence

Confluence is the documentation backbone for millions of engineering teams and enterprises worldwide, particularly those already using Jira for project tracking. While Confluence itself provides a powerful editing and organization interface, many teams want to surface specific documentation to external audiences — customers, partners, or different internal groups — with custom styling, navigation, or access controls that Confluence's native permissions system does not accommodate. Building a custom portal on top of Confluence's API lets you keep your team writing in a familiar tool while controlling exactly how content is presented.

Confluence's REST API v2 is a modern, well-documented API that returns JSON throughout. Authentication uses your Atlassian account email combined with an API token, encoded as Basic auth — the same pattern used by Jira's API, making it easy to integrate both in the same Bolt application. The API organizes content into spaces (equivalent to top-level sections or departments), pages (the main content unit), and blog posts (time-based content). Pages can be nested hierarchically, with parent-child relationships that map naturally to a documentation structure.

Page content in Confluence is stored and returned in Atlassian Document Format (ADF), a structured JSON representation of rich text. ADF is more verbose than Markdown but more semantic than HTML, with typed nodes for paragraphs, headings, lists, tables, code blocks, inline mentions, and other Confluence-specific elements. For display in your Bolt app, you will need to traverse the ADF node tree and convert it to HTML — the @atlaskit/adf-utils package from Atlassian handles this conversion, or you can write a simple node-type renderer for the elements you care about.

Integration method

Bolt Chat + API Route

Bolt generates Next.js API routes that call Confluence's REST API v2 using basic authentication — your Atlassian account email combined with an API token, base64-encoded as a Basic auth header. The Confluence API is JSON-based REST that works fully from Bolt's WebContainer for all outbound calls. Page content is delivered as Atlassian Document Format (ADF) which requires conversion to HTML for rendering in React components.

Prerequisites

  • A Bolt.new account with a Next.js project
  • An Atlassian account with access to a Confluence Cloud instance
  • An Atlassian API token from id.atlassian.com/manage-profile/security/api-tokens
  • Your Confluence Cloud domain (e.g., yourcompany.atlassian.net)
  • At least one Confluence space with pages to query

Step-by-step guide

1

Create an Atlassian API Token and Configure Authentication

Confluence Cloud uses Atlassian's unified authentication system. Unlike services with dedicated API key UIs, Atlassian API tokens are created from your Atlassian account profile and work across all Atlassian products (Confluence, Jira, Bitbucket) associated with your account. To create an API token, go to id.atlassian.com/manage-profile/security/api-tokens. You may need to log in with your Atlassian account. On the API Tokens page, click Create API token. Give it a descriptive label like 'Bolt App' with a note about the date. Click Create. The token is shown once — copy it immediately. Atlassian API tokens are long random strings with no specific prefix pattern. Confluence Cloud authentication uses HTTP Basic Auth, but instead of username and password, you use your Atlassian account email address and the API token. Combine them as email:token, then base64-encode the combined string. In Node.js: Buffer.from(`${email}:${token}`).toString('base64'). Pass this in the Authorization header as Basic {base64string}. Add three environment variables to your .env file: CONFLUENCE_DOMAIN (your Confluence Cloud hostname, like yourcompany.atlassian.net — no https prefix, no /wiki path), CONFLUENCE_EMAIL (your Atlassian account email), and CONFLUENCE_API_TOKEN (the token you just created). These must not have the NEXT_PUBLIC_ prefix. The Confluence REST API v2 base URL is https://{CONFLUENCE_DOMAIN}/wiki/api/v2. The older v1 REST API is at https://{CONFLUENCE_DOMAIN}/wiki/rest/api. Some operations (especially content search) still use v1 endpoints. The helper library should handle both URL bases. Test the connection by calling GET /wiki/api/v2/spaces — this should return a list of spaces your account has access to. If you see a 401, verify the Authorization header format. If you see a 403, the token is valid but the account lacks permission to read that content.

Bolt.new Prompt

Add CONFLUENCE_DOMAIN, CONFLUENCE_EMAIL, and CONFLUENCE_API_TOKEN to the .env file with placeholder values. Create a lib/confluence.ts utility that exports a confluenceFetch helper. Build the base64 Basic auth header from email + token using Buffer.from(). Accept a path and optional query params. Use https://{CONFLUENCE_DOMAIN}/wiki{path} as the full URL. Handle JSON error responses. Export separate confluenceV1 and confluenceV2 helpers since some endpoints use /rest/api (v1) and others use /api/v2.

Paste this in Bolt.new chat

lib/confluence.ts
1// lib/confluence.ts
2function getAuthHeader(): string {
3 const email = process.env.CONFLUENCE_EMAIL;
4 const token = process.env.CONFLUENCE_API_TOKEN;
5 if (!email || !token) {
6 throw new Error('CONFLUENCE_EMAIL and CONFLUENCE_API_TOKEN must be set in .env');
7 }
8 return `Basic ${Buffer.from(`${email}:${token}`).toString('base64')}`;
9}
10
11function getBaseUrl(): string {
12 const domain = process.env.CONFLUENCE_DOMAIN;
13 if (!domain) throw new Error('CONFLUENCE_DOMAIN must be set in .env');
14 return `https://${domain}/wiki`;
15}
16
17type FetchOptions = {
18 method?: string;
19 body?: unknown;
20};
21
22async function confluenceFetchInternal<T = unknown>(
23 path: string,
24 params?: Record<string, string | number>,
25 options: FetchOptions = {}
26): Promise<T> {
27 const url = new URL(`${getBaseUrl()}${path}`);
28 if (params) {
29 Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, String(v)));
30 }
31
32 const response = await fetch(url.toString(), {
33 method: options.method || 'GET',
34 headers: {
35 Authorization: getAuthHeader(),
36 'Content-Type': 'application/json',
37 Accept: 'application/json',
38 },
39 body: options.body ? JSON.stringify(options.body) : undefined,
40 });
41
42 if (!response.ok) {
43 const err = await response.json().catch(() => ({ message: response.statusText })) as { message?: string };
44 throw new Error(err.message || `Confluence API error: ${response.status} ${path}`);
45 }
46
47 return response.json() as Promise<T>;
48}
49
50// REST API v2 (modern)
51export async function confluenceV2<T = unknown>(
52 path: string,
53 params?: Record<string, string | number>
54): Promise<T> {
55 return confluenceFetchInternal<T>(`/api/v2${path}`, params);
56}
57
58// REST API v1 (legacy, needed for search and some content endpoints)
59export async function confluenceV1<T = unknown>(
60 path: string,
61 params?: Record<string, string | number>
62): Promise<T> {
63 return confluenceFetchInternal<T>(`/rest/api${path}`, params);
64}

Pro tip: If your organization uses Confluence Data Center (self-hosted) instead of Confluence Cloud, the authentication method is different — Data Center uses Personal Access Tokens in the Authorization header as 'Bearer {token}' rather than Basic auth. The API endpoints are also slightly different. This guide covers Confluence Cloud (*.atlassian.net). Check your Confluence URL to determine which version you are using.

Expected result: The lib/confluence.ts helper is configured. Test by calling confluenceV2('/spaces') from an API route — you should see a list of Confluence spaces with names and keys, confirming authentication is working correctly.

2

Fetch Spaces and Pages from Confluence

Confluence organizes content into spaces (like departments or projects) and pages (the individual documents). The v2 API uses cursor-based pagination with a cursor parameter for subsequent pages, and returns a limit of 25 results by default (maximum 250). The spaces endpoint (GET /api/v2/spaces) returns all spaces accessible to your account. Each space has a key (short identifier like 'DEV', 'HR', 'DOCS'), a name, type ('global' for public spaces, 'personal' for individual spaces), and status. For documentation portals, filter to global status:'current' spaces and exclude personal spaces. The pages endpoint (GET /api/v2/pages) returns pages with optional filters. Use the spaceId query parameter to restrict to a specific space. The response includes page ID, title, status, version number, and parentId for building navigation trees. For the page body content, you must either request it via body-format='storage' query parameter on the page list endpoint, or fetch it separately from the individual page endpoint — listing with body content is much slower for large space queries, so fetch bodies on-demand for individual pages only. For building hierarchical navigation, fetch pages with parentId filtering. GET /api/v2/pages?parentId={pageId} returns the direct children of a specific page. The root-level pages in a space have parentId equal to the space's homepage ID. Build a tree by starting from the homepage and recursively fetching children as users expand navigation nodes. Page status in Confluence can be 'current' (published), 'draft', or 'archived'. For public documentation portals, always filter to status='current' to avoid exposing draft content. The API returns all statuses by default without filtering.

Bolt.new Prompt

Create two Next.js API routes. First, /api/confluence/spaces/route.ts that calls confluenceV2('/spaces') with status='current' filter. Map results to { id, key, name, type }. Cache 5 minutes. Second, /api/confluence/pages/route.ts that accepts spaceId (required), parentId (optional), limit (default 25) query params. Call confluenceV2('/pages') with those params and status='current'. Return { pages: [{id, title, status, version, parentId}], cursor: nextCursor }. Cache 60 seconds per spaceId+parentId combination.

Paste this in Bolt.new chat

app/api/confluence/pages/route.ts
1// app/api/confluence/pages/route.ts
2import { NextResponse } from 'next/server';
3import { confluenceV2 } from '@/lib/confluence';
4
5interface ConfluencePage {
6 id: string;
7 title: string;
8 status: string;
9 version: { number: number; createdAt: string };
10 parentId: string | null;
11 spaceId: string;
12 _links: { webui: string };
13}
14
15interface ConfluencePagesResponse {
16 results: ConfluencePage[];
17 _links: { next?: string };
18}
19
20const cache = new Map<string, { data: unknown; expiresAt: number }>();
21
22export async function GET(request: Request) {
23 const { searchParams } = new URL(request.url);
24 const spaceId = searchParams.get('spaceId');
25 const parentId = searchParams.get('parentId');
26 const limit = parseInt(searchParams.get('limit') || '25', 10);
27 const cursor = searchParams.get('cursor');
28
29 if (!spaceId) {
30 return NextResponse.json({ error: 'spaceId is required' }, { status: 400 });
31 }
32
33 const cacheKey = `${spaceId}-${parentId}-${limit}-${cursor}`;
34 const cached = cache.get(cacheKey);
35 if (cached && Date.now() < cached.expiresAt) {
36 return NextResponse.json(cached.data);
37 }
38
39 try {
40 const params: Record<string, string | number> = {
41 'space-id': spaceId,
42 status: 'current',
43 limit,
44 };
45 if (parentId) params['parent-id'] = parentId;
46 if (cursor) params.cursor = cursor;
47
48 const data = await confluenceV2<ConfluencePagesResponse>('/pages', params);
49
50 const domain = process.env.CONFLUENCE_DOMAIN;
51 const result = {
52 pages: data.results.map((p) => ({
53 id: p.id,
54 title: p.title,
55 status: p.status,
56 version: p.version.number,
57 updatedAt: p.version.createdAt,
58 parentId: p.parentId,
59 url: `https://${domain}/wiki${p._links.webui}`,
60 })),
61 nextCursor: data._links.next
62 ? new URL(data._links.next, 'https://x').searchParams.get('cursor')
63 : null,
64 };
65
66 cache.set(cacheKey, { data: result, expiresAt: Date.now() + 60_000 });
67 return NextResponse.json(result);
68 } catch (err) {
69 const message = err instanceof Error ? err.message : 'Failed to fetch pages';
70 return NextResponse.json({ error: message }, { status: 500 });
71 }
72}

Pro tip: Confluence page IDs are numeric strings (like '12345678'). Always treat them as strings in TypeScript even though they look like numbers — some Confluence APIs return them as numbers and some as strings depending on version, so normalizing to string prevents type comparison bugs when matching parent/child relationships.

Expected result: The pages API route returns a list of current pages for the specified space. The response includes direct Confluence URLs for each page so you can link users back to Confluence for editing while displaying content in your Bolt portal.

3

Fetch and Render Confluence Page Content

Fetching a single page with its content requires either requesting the body in the list endpoint (slow for lists) or using the individual page endpoint with explicit body expand. Confluence v2 API returns page bodies in ADF (Atlassian Document Format), a structured JSON tree. The storage format (XHTML-like) is also available and is often simpler to convert to HTML for display. For the storage format, request it by adding body-format=storage to the individual page request: GET /api/v2/pages/{pageId}?body-format=storage. The storage format is an XHTML-like XML string wrapped in a value field. It uses standard HTML tags for most content plus Confluence-specific macros in structured-macro tags. For a simpler conversion, request the body in 'view' format (body-format=view), which returns already-processed HTML that Confluence generates for browser display. This HTML includes Confluence's own CSS class names and structure, so you will need to strip or override styles for embedding in your custom UI. The view format is the easiest approach for rendering page content with correct formatting. Alternatively, the export/html endpoint (GET /wiki/spaces/{spaceKey}/pages/{pageId}?output=html) returns clean HTML for some Confluence versions, though this v1 endpoint is not officially supported in the v2 API. For the ADF JSON format, parse the document tree recursively. Top-level doc node contains an array of content nodes. Each node has a type (paragraph, heading, bulletList, codeBlock, table, etc.) and attrs for type-specific attributes (heading level, code language). Text content is in text nodes with optional marks for bold, italic, link, and code inline formatting. The atlassian/adf-react package renders ADF natively in React without manual conversion.

Bolt.new Prompt

Create a Next.js API route at app/api/confluence/pages/[id]/route.ts that fetches a single Confluence page with body content. Use confluenceV2(`/pages/${id}`) with body-format=view query param to get the view HTML. Clean the HTML by removing Confluence-specific style tags, class attributes, and metadata divs. Return { id, title, html, lastUpdated, version, spaceId, url }. Build a React page detail component that renders the HTML inside a max-w-prose container with tailwind prose classes.

Paste this in Bolt.new chat

app/api/confluence/pages/[id]/route.ts
1// app/api/confluence/pages/[id]/route.ts
2import { NextResponse } from 'next/server';
3import { confluenceV2 } from '@/lib/confluence';
4
5interface ConfluencePageDetail {
6 id: string;
7 title: string;
8 status: string;
9 version: { number: number; createdAt: string; authorId: string };
10 spaceId: string;
11 body: { view: { value: string; representation: string } };
12 _links: { webui: string };
13}
14
15function cleanConfluenceHtml(html: string): string {
16 if (!html) return '';
17 return html
18 // Remove inline style attributes
19 .replace(/\sstyle="[^"]*"/gi, '')
20 // Remove Confluence-specific data attributes
21 .replace(/\sdata-[a-z-]+="[^"]*"/gi, '')
22 // Remove ac: namespace elements (Confluence macros rendered as HTML)
23 .replace(/<ac:[^>]*>.*?<\/ac:[^>]*>/gs, '')
24 .replace(/<ac:[^/]*/gi, '')
25 // Normalize spaces
26 .replace(/\s+/g, ' ')
27 .trim();
28}
29
30export async function GET(
31 _request: Request,
32 { params }: { params: { id: string } }
33) {
34 try {
35 const page = await confluenceV2<ConfluencePageDetail>(`/pages/${params.id}`, {
36 'body-format': 'view',
37 });
38
39 const domain = process.env.CONFLUENCE_DOMAIN;
40 const cleanHtml = cleanConfluenceHtml(page.body?.view?.value || '');
41
42 return NextResponse.json({
43 id: page.id,
44 title: page.title,
45 html: cleanHtml,
46 lastUpdated: page.version.createdAt,
47 version: page.version.number,
48 spaceId: page.spaceId,
49 url: `https://${domain}/wiki${page._links.webui}`,
50 });
51 } catch (err) {
52 const message = err instanceof Error ? err.message : 'Failed to fetch page content';
53 return NextResponse.json({ error: message }, { status: 500 });
54 }
55}

Pro tip: The 'view' body format HTML from Confluence references images using relative URLs internal to Confluence. To display images correctly, either replace relative image src values with absolute URLs using your CONFLUENCE_DOMAIN, or proxy image requests through your own API route. Images in Confluence pages are served from your Confluence instance and require authenticated requests to access.

Expected result: The page detail route returns Confluence page content as cleaned HTML. Rendering this in a React component with dangerouslySetInnerHTML displays the page text with basic formatting. Images may not display yet — that requires the image URL fixing step or a proxy approach.

4

Implement Confluence Content Search with CQL

Confluence Query Language (CQL) is a powerful search syntax that allows filtering content by space, type, title, text, labels, creator, modifier, and modification date. For documentation portals and knowledge base tools, CQL search is often more valuable than navigating the page hierarchy. The search endpoint is GET /wiki/rest/api/content/search (v1) with a cql query parameter. Basic CQL for full-text search: type = 'page' AND text ~ 'search term'. To restrict to a space: type = 'page' AND space.key = 'DEV' AND text ~ 'deploy'. For recently modified pages: type = 'page' AND lastModified >= '2025-04-01'. Multiple conditions use AND and OR operators. The search response includes matching pages with a title, excerpt (shows matching text with highlighted query terms), type (page or blogpost), space name, URL, and metadata. The excerpt field contains the surrounding text around search hits — display this as a snippet in search results to help users determine relevance before clicking. For a good search UX, debounce the search input so you are not firing API requests on every keystroke. A 300ms debounce is standard. Show a loading indicator while searching and a 'No results found' state for empty queries. Highlight the search term in the result titles and excerpts using a simple string replace that wraps matches in a mark element. CQL search respects Confluence permissions — the API only returns pages that the authenticated user has permission to view. If your Bolt app is used by different team members, the search results are automatically filtered to each user's accessible content when using OAuth. With a single API token (admin or service account), all content in accessible spaces is returned.

Bolt.new Prompt

Create a search API route at app/api/confluence/search/route.ts. Accept query (required), spaceKey (optional), type (optional, default 'page'), limit (default 10). Build CQL: start with "type = '{type}' AND (title ~ '{query}' OR text ~ '{query}')". If spaceKey provided, add AND space.key = '{spaceKey}'. Call confluenceV1('/content/search') with cql param. Return results with title, excerpt, space.name, type, lastModified, and webui URL. Also build a SearchResults React component that shows a list of results with title, space badge, excerpt, and a link to the Confluence page.

Paste this in Bolt.new chat

app/api/confluence/search/route.ts
1// app/api/confluence/search/route.ts
2import { NextResponse } from 'next/server';
3import { confluenceV1 } from '@/lib/confluence';
4
5interface SearchResult {
6 id: string;
7 title: string;
8 type: string;
9 excerpt: string;
10 space: { key: string; name: string };
11 history: { lastUpdated: { when: string; by: { displayName: string } } };
12 _links: { webui: string };
13}
14
15interface SearchResponse {
16 results: SearchResult[];
17 totalSize: number;
18 start: number;
19 limit: number;
20}
21
22export async function GET(request: Request) {
23 const { searchParams } = new URL(request.url);
24 const query = searchParams.get('query')?.trim();
25 const spaceKey = searchParams.get('spaceKey');
26 const type = searchParams.get('type') || 'page';
27 const limit = parseInt(searchParams.get('limit') || '10', 10);
28
29 if (!query) {
30 return NextResponse.json({ error: 'query parameter is required' }, { status: 400 });
31 }
32
33 // Escape CQL special characters in query
34 const escapedQuery = query.replace(/"/g, '\\"');
35
36 let cql = `type = "${type}" AND (title ~ "${escapedQuery}" OR text ~ "${escapedQuery}")`;
37 if (spaceKey) cql += ` AND space.key = "${spaceKey}"`;
38 cql += ' ORDER BY score DESC';
39
40 try {
41 const domain = process.env.CONFLUENCE_DOMAIN;
42 const data = await confluenceV1<SearchResponse>('/content/search', {
43 cql,
44 limit,
45 expand: 'space,history,history.lastUpdated',
46 });
47
48 const results = data.results.map((r) => ({
49 id: r.id,
50 title: r.title,
51 type: r.type,
52 excerpt: r.excerpt || '',
53 space: { key: r.space?.key, name: r.space?.name },
54 lastUpdated: r.history?.lastUpdated?.when,
55 updatedBy: r.history?.lastUpdated?.by?.displayName,
56 url: `https://${domain}/wiki${r._links.webui}`,
57 }));
58
59 return NextResponse.json({ results, total: data.totalSize });
60 } catch (err) {
61 const message = err instanceof Error ? err.message : 'Confluence search failed';
62 return NextResponse.json({ error: message }, { status: 500 });
63 }
64}

Pro tip: CQL queries are case-insensitive for the ~ (contains) operator but case-sensitive for = (equals). For user-facing search, always use ~ for text matching. Escape double quotes in user search input by replacing " with \" in the CQL string to prevent CQL injection and query parsing errors.

Expected result: The search route returns Confluence pages and blog posts matching the query term, with excerpts showing the context around each match. Results include direct Confluence URLs for linking back to the source document.

Common use cases

Customer-Facing Documentation Portal

Build a public-facing documentation site that surfaces content from a specific Confluence space. Customers browse documentation by category without needing a Confluence account. Your Bolt app fetches only published pages from designated spaces, renders them with consistent branding and navigation, and provides a search interface using Confluence's CQL search syntax.

Bolt.new Prompt

Build a documentation portal powered by Confluence. Create /api/confluence/pages that fetches pages from a specific space using GET https://{CONFLUENCE_DOMAIN}/wiki/api/v2/pages with spaceKey query param. Return title, version, lastUpdated, author, and excerpt. Create /api/confluence/pages/[id] that fetches a single page's body (body.storage format) and converts XHTML storage format to clean HTML by removing Confluence-specific tags. Build a React docs portal with a sidebar showing page titles organized by parent, a main content area rendering the page HTML, and a search input that calls /api/confluence/search. Store CONFLUENCE_DOMAIN, CONFLUENCE_EMAIL, CONFLUENCE_API_TOKEN in process.env.

Copy this prompt to try it in Bolt.new

Team Knowledge Base Dashboard

Create an internal dashboard that aggregates recent Confluence activity — newly created pages, recently updated documentation, and most viewed content — giving team members a quick overview of what documentation is current and what has changed recently. This is especially useful for onboarding new team members who need to discover the most relevant documentation quickly.

Bolt.new Prompt

Build a Confluence knowledge base dashboard. Create /api/confluence/recent that fetches recently modified pages using the Confluence REST API with sort=-modified and a limit of 10. Create /api/confluence/spaces that fetches all spaces the token has access to. Build a React dashboard with a space selector dropdown, a 'Recently Updated' feed showing page title, last modified date, and author avatar initials, and a quick search. Each page item links to the actual Confluence page via the page's _links.webui URL.

Copy this prompt to try it in Bolt.new

Documentation Search Interface

Build a powerful search interface for Confluence content using CQL (Confluence Query Language). Let users search across all spaces or restrict to specific spaces, filter by content type (page vs blog post), and sort by relevance or recency. Display results with title, space name, excerpt, and last modified date — giving teams a faster and more focused search experience than Confluence's built-in interface.

Bolt.new Prompt

Create a Confluence search interface. Build /api/confluence/search that accepts query (required), spaceKey (optional), type (page|blogpost|all), and limit (default 10). Call GET /wiki/rest/api/content/search with cql param. Build CQL: title ~ "{query}" OR text ~ "{query}", optionally AND space.key = "{spaceKey}", optionally AND type = "{type}". Return results with title, excerpt, space name, type, lastModified, and direct URL. Build a React search page with a search input, space dropdown, type toggle, and results list. Show a snippet of matching text for each result.

Copy this prompt to try it in Bolt.new

Troubleshooting

401 Unauthorized even with correct email and API token

Cause: Basic auth for Atlassian requires base64 encoding of 'email:token' — not just the token alone. A common error is passing only the token in the Authorization header without the email prefix, or using the token directly as a bearer token (which Confluence does not support for API token auth).

Solution: Verify the Authorization header is constructed correctly: Authorization: Basic {base64(email:token)}. In Node.js: Buffer.from(`${email}:${token}`).toString('base64'). The email must be your exact Atlassian account email (lowercase, no extra spaces). Regenerate the API token at id.atlassian.com if unsure.

typescript
1// Correct Basic auth construction:
2const credentials = Buffer.from(`${process.env.CONFLUENCE_EMAIL}:${process.env.CONFLUENCE_API_TOKEN}`).toString('base64');
3const headers = { Authorization: `Basic ${credentials}` };

403 Forbidden when accessing a specific space or page

Cause: The Atlassian account associated with the API token does not have permission to view the space or page. Confluence has granular space-level and page-level permissions separate from account access.

Solution: Ask a Confluence administrator to grant view access to the space for your account. Alternatively, use a service account or admin account token that has access to all spaces needed by the integration. Test access by trying to view the space in a browser while logged in as the account whose token you are using.

Page body returns as null or empty even though the page has content

Cause: The page list endpoint does not include body content by default — you must explicitly request it with the body-format query parameter on the individual page endpoint, not the list endpoint.

Solution: Fetch page content separately using the individual page endpoint: GET /api/v2/pages/{id}?body-format=view. Do not try to include body content in list responses for large space queries — it is very slow. Fetch bodies on-demand when a user clicks to view a specific page.

typescript
1// Correct endpoint for fetching page with body:
2const page = await confluenceV2(`/pages/${pageId}`, { 'body-format': 'view' });
3// page.body.view.value contains the HTML

Images in rendered page content appear broken with 404 errors

Cause: Confluence page HTML from the 'view' format uses relative URLs for images (/wiki/download/attachments/...) that reference files stored in your Confluence instance. These URLs require authentication to access and are relative, not absolute.

Solution: Replace relative image URLs with absolute ones by prepending your Confluence domain: replace '/wiki/' with 'https://yourcompany.atlassian.net/wiki/' in the HTML string before returning it from your API route. Images will still require authentication — proxy them through your API route or make the Confluence instance publicly accessible for the specific file download path.

typescript
1// Fix relative image URLs in page HTML:
2const domain = process.env.CONFLUENCE_DOMAIN;
3const fixedHtml = cleanHtml.replace(
4 /src="\/wiki\//g,
5 `src="https://${domain}/wiki/`
6);

Best practices

  • Store CONFLUENCE_EMAIL, CONFLUENCE_API_TOKEN, and CONFLUENCE_DOMAIN without the NEXT_PUBLIC_ prefix — these credentials grant access to potentially sensitive company documentation.
  • Cache Confluence API responses aggressively — spaces (5 minutes), page lists (60 seconds), page content (5 minutes). Confluence's API can be slow for large instances and documentation rarely changes in real time.
  • For public-facing documentation portals, consider a scheduled sync that copies Confluence content to your own database — this eliminates dependency on Confluence uptime and allows faster queries without per-request API calls.
  • Always filter page requests to status='current' to avoid exposing draft content in your documentation portal — Confluence's default API behavior returns all statuses.
  • Use the 'view' body format for rendering page content rather than 'storage' or 'atlas_doc_format' — the view format requires the least parsing work for browser display.
  • Handle CQL query injection by escaping double quotes in user search input before including it in CQL strings — malformed CQL returns 400 errors that degrade the search experience.
  • Respect Confluence's rate limits — heavy documentation portals with many concurrent users should cache responses or sync to a local database rather than making per-request API calls.
  • Use the _links.webui URL returned by the API to provide 'Edit in Confluence' links — this lets users move from your custom portal to Confluence's editor when they need to update content.

Alternatives

Frequently asked questions

Can I use the Confluence API in Bolt's WebContainer preview without deploying?

Yes — all outbound Confluence API calls work in Bolt's WebContainer preview. You can fetch spaces, pages, and page content, and perform CQL searches during development without deploying. Confluence does not send webhook callbacks in a way that requires an incoming connection, so there is no deployment requirement for standard read operations.

Does Bolt.new have a native Confluence integration?

No — Bolt.new does not include a built-in Confluence connector. The integration uses Confluence's REST API directly with Atlassian API token authentication. Bolt's AI can generate the integration code from a description of your portal or dashboard requirements.

Can I use the same Atlassian API token for both Jira and Confluence?

Yes — Atlassian API tokens are tied to your Atlassian account, not to specific products. The same token and email combination works for both the Confluence API and the Jira API in your Atlassian organization. You can store one CONFLUENCE_API_TOKEN (or ATLASSIAN_API_TOKEN) and use it for both integrations in your Bolt app.

What is CQL and how is it different from SQL?

CQL (Confluence Query Language) is Confluence's built-in search syntax for querying pages, spaces, and other content objects. It is conceptually similar to SQL WHERE clauses but operates on Confluence's content model rather than database tables. CQL supports field comparisons (title = 'Page Name'), text search (text ~ 'keyword'), date comparisons (lastModified >= '2025-01-01'), and boolean operators (AND, OR, NOT). It cannot be used to modify data — only for search and filtering.

How do I handle Confluence pages with complex formatting like tables and macros?

Complex Confluence content (tables, code macros, info panels, status badges) is included in the 'view' format HTML but uses Confluence's internal CSS classes for styling. When embedded in your Bolt app, these elements may appear unstyled. The simplest approach is to add CSS rules targeting the specific class names used by Confluence macros, or to strip macro content and replace it with simplified HTML. The Confluence 'storage' format exposes macros as XML elements you can handle individually in your conversion logic.

Does this work with Confluence Server (on-premise) or only Confluence Cloud?

This guide covers Confluence Cloud (*.atlassian.net). Confluence Server and Data Center use different authentication (personal access tokens with Bearer header instead of Basic auth with API token) and slightly different API paths. The core content model and most endpoints are similar, but Server instances may be on older API versions. Check your Confluence URL — if it ends in .atlassian.net, you are on Cloud; any other domain is Server or Data Center.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.