Authentication

The REST API uses OAuth 2.0 (3LO) — every request must include a valid Bearer token.

Authorization: Bearer YOUR_ACCESS_TOKEN

Access tokens expire after 1 hour. For automated pipelines, use refresh token rotation to obtain new tokens without user interaction.


Required Scopes

Both scopes must be present in your OAuth integration before authorizing:

Scope Purpose
export:markdown:custom Grants access to the export endpoints
read:forge-app:confluence Required for Forge API route access

Include offline_access in the authorization URL to receive a refresh token.


Token Rotation

How to Refresh

curl --request POST \
  --url 'https://auth.atlassian.com/oauth/token' \
  --header 'Content-Type: application/json' \
  --data '{
    "grant_type": "refresh_token",
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",
    "refresh_token": "YOUR_STORED_REFRESH_TOKEN"
  }'

The response includes a new access_token and a new refresh_token.

Important — rotating refresh tokens: Atlassian issues a new refresh token on every refresh call. Always save the latest refresh token returned. Reusing an old refresh token invalidates the entire chain and requires re-authorization from scratch.

Refresh Token Expiry

Refresh tokens expire after 90 days of inactivity. Any active pipeline that refreshes at least once every 90 days keeps the token alive indefinitely.


Automation Pattern (Node.js)

import fs from 'fs';

const TOKEN_FILE    = './tokens.json';
const CLIENT_ID     = process.env.ATLASSIAN_CLIENT_ID;
const CLIENT_SECRET = process.env.ATLASSIAN_CLIENT_SECRET;

function loadTokens() {
  return JSON.parse(fs.readFileSync(TOKEN_FILE, 'utf8'));
}

function saveTokens(tokens) {
  fs.writeFileSync(TOKEN_FILE, JSON.stringify(tokens, null, 2));
}

async function getValidToken() {
  const tokens = loadTokens();

  const payload = JSON.parse(Buffer.from(tokens.access_token.split('.')[1], 'base64').toString());
  const expiresAt = payload.exp * 1000;

  if (Date.now() < expiresAt - 5 * 60 * 1000) {
    return tokens.access_token;
  }

  const res = await fetch('https://auth.atlassian.com/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      grant_type:    'refresh_token',
      client_id:     CLIENT_ID,
      client_secret: CLIENT_SECRET,
      refresh_token: tokens.refresh_token,
    }),
  });

  const newTokens = await res.json();
  if (!newTokens.access_token) throw new Error(`Token refresh failed: ${JSON.stringify(newTokens)}`);

  saveTokens({ access_token: newTokens.access_token, refresh_token: newTokens.refresh_token });
  return newTokens.access_token;
}

Automation Pattern (Python)

import json, time, base64, os, requests
from pathlib import Path

TOKEN_FILE    = Path('tokens.json')
CLIENT_ID     = os.environ['ATLASSIAN_CLIENT_ID']
CLIENT_SECRET = os.environ['ATLASSIAN_CLIENT_SECRET']

def load_tokens():
    return json.loads(TOKEN_FILE.read_text())

def save_tokens(tokens):
    TOKEN_FILE.write_text(json.dumps(tokens, indent=2))

def get_valid_token():
    tokens = load_tokens()

    payload_b64 = tokens['access_token'].split('.')[1]
    payload = json.loads(base64.b64decode(payload_b64 + '=='))

    if time.time() < payload['exp'] - 300:
        return tokens['access_token']

    r = requests.post('https://auth.atlassian.com/oauth/token', json={
        'grant_type':    'refresh_token',
        'client_id':     CLIENT_ID,
        'client_secret': CLIENT_SECRET,
        'refresh_token': tokens['refresh_token'],
    })
    r.raise_for_status()
    new_tokens = r.json()

    save_tokens({'access_token': new_tokens['access_token'], 'refresh_token': new_tokens['refresh_token']})
    return new_tokens['access_token']

Security Best Practices

  • Store tokens in environment variables or a secrets manager — never in source code
  • Never commit tokens.json or any file containing tokens to version control
  • Use separate OAuth integrations per environment (dev, staging, production)
  • Re-authorize immediately if a refresh token is invalidated