atlassian-api-client
v1.0.1
Published
Typed Node.js/TypeScript clients and CLI for Atlassian Confluence Cloud REST API v2 and Jira Cloud Platform REST API v3
Maintainers
Readme
atlassian-api-client
Typed Node.js/TypeScript clients and CLI for Atlassian Cloud APIs.
- Confluence Cloud REST API v2 — Pages, Spaces, Blog Posts, Comments, Attachments, Labels, Content Properties, Custom Content, Whiteboards, Tasks, Versions
- Jira Cloud Platform REST API v3 — Issues, Projects, Search (JQL), Users, Issue Types, Priorities, Statuses, Issue Comments, Issue Attachments, Labels, Boards, Sprints, Workflows, Dashboards, Filters, Fields, Webhooks, JQL helpers, Bulk operations
Zero runtime dependencies. Uses native fetch (Node.js 24+).
Install
npm install atlassian-api-clientSupported Runtimes
- Node.js >= 24.0.0
Use with coding agents
A Claude Code skill named atlassian-api-client-cli ships inside this package and teaches coding agents how to drive the atlas CLI safely (env-only auth, first-try gotchas, JQL quoting, pagination, output formats).
# User-wide install, into ~/.claude/skills/atlassian-api-client-cli
npx --package atlassian-api-client -- atlas install-skill
# Project-local install, into <cwd>/.claude/skills/atlassian-api-client-cli
npx --package atlassian-api-client -- atlas install-skill --local
# Print the bundled source path without copying (for symlinks / custom tooling)
npx --package atlassian-api-client -- atlas install-skill --print
# Preview what would be copied
npx --package atlassian-api-client -- atlas install-skill --dry-runinstall-skill is a top-level utility command with an options-only shape: run it as atlas install-skill [options].
If atlassian-api-client is already a dependency in your project, the shorter npx atlas install-skill form resolves to node_modules/.bin/atlas and works the same way. The explicit --package form is safer when calling from a clean shell because it pins the source package and won't accidentally resolve an unrelated atlas package from the registry.
The skill source lives at skill/SKILL.md with deeper resource matrices in skill/reference/. It's versioned alongside the npm package: every install stamps the destination SKILL.md with the package version it was copied from.
Quick Start
Confluence
import { ConfluenceClient } from 'atlassian-api-client';
const confluence = new ConfluenceClient({
baseUrl: 'https://yourcompany.atlassian.net',
auth: {
type: 'basic',
email: '[email protected]',
apiToken: process.env.ATLASSIAN_API_TOKEN!,
},
});
// List pages in a space
const pages = await confluence.pages.list({ spaceId: '123456' });
console.log(pages.results);
// Get a specific page
const page = await confluence.pages.get('789');Jira
import { JiraClient } from 'atlassian-api-client';
const jira = new JiraClient({
baseUrl: 'https://yourcompany.atlassian.net',
auth: {
type: 'basic',
email: '[email protected]',
apiToken: process.env.ATLASSIAN_API_TOKEN!,
},
});
// Get an issue
const issue = await jira.issues.get('PROJ-123');
console.log(issue.fields);
// Search with JQL
const results = await jira.search.search({
jql: 'project = PROJ AND status = "In Progress"',
});
console.log(results.issues);Authentication
Basic Auth (Email + API Token)
const client = new ConfluenceClient({
baseUrl: 'https://yourcompany.atlassian.net',
auth: {
type: 'basic',
email: '[email protected]',
apiToken: 'your-api-token',
},
});Generate an API token at: https://id.atlassian.com/manage-profile/security/api-tokens
Bearer Auth (OAuth 2.0 / PAT)
const client = new JiraClient({
baseUrl: 'https://yourcompany.atlassian.net',
auth: {
type: 'bearer',
token: 'your-oauth-token',
},
});Self-hosted / non-Atlassian baseUrl
By default, only baseUrls whose host ends in .atlassian.{net,com}, .jira-dev.com, or .jira.com are accepted — the transport refuses to send the configured Authorization header to any other host. For self-hosted Jira / Confluence or a reverse proxy in front of Atlassian, pass allowedHosts (bare hostnames, no port) to opt in:
const client = new JiraClient({
baseUrl: 'https://jira.internal.example',
auth: { type: 'bearer', token: process.env.PAT! },
allowedHosts: ['jira.internal.example'],
});The list must include the baseUrl host itself; resource paths that resolve to a host outside the list throw ValidationError before any HTTP call is made.
Pagination
Async Iteration
// Confluence - cursor-based pagination
for await (const page of confluence.pages.listAll({ spaceId: '123' })) {
console.log(page.title);
}
// Jira - offset-based pagination
for await (const project of jira.projects.listAll()) {
console.log(project.name);
}
// Jira search
for await (const issue of jira.search.searchAll({ jql: 'project = PROJ' })) {
console.log(issue.key);
}Manual Pagination
// Confluence
const result = await confluence.pages.list({ spaceId: '123', limit: 25 });
console.log(result.results); // current page items
// result._links.next contains cursor for next page
// Jira
const projects = await jira.projects.list({ maxResults: 50 });
console.log(projects.values); // current page items
console.log(projects.total); // total availableError Handling
import {
AtlassianError,
AuthenticationError,
NotFoundError,
RateLimitError,
} from 'atlassian-api-client';
try {
await jira.issues.get('PROJ-999');
} catch (error) {
if (error instanceof RateLimitError) {
console.log(`Rate limited. Retry after ${error.retryAfter}s`);
} else if (error instanceof NotFoundError) {
console.log('Issue not found');
} else if (error instanceof AuthenticationError) {
console.log('Invalid credentials');
} else if (error instanceof AtlassianError) {
console.log(`API error: ${error.code} - ${error.message}`);
}
}Retry & Timeout
const client = new JiraClient({
baseUrl: 'https://yourcompany.atlassian.net',
auth: { type: 'basic', email: '...', apiToken: '...' },
timeout: 15000, // 15s timeout (default: 30s)
retries: 5, // max retry attempts (default: 3)
retryDelay: 2000, // base delay for backoff (default: 1000ms)
maxRetryDelay: 60000, // max delay cap (default: 30000ms)
});Retries use exponential backoff with jitter. Retryable: 429, 500, 502, 503, 504, and network errors.
Response Body Size Cap
ClientConfig.maxResponseBytes (default: unset, no cap) bounds the size of any single buffered response body the transport will materialise. When a body exceeds the cap, the request throws ResponseTooLargeError (code: 'RESPONSE_TOO_LARGE_ERROR') instead of loading it into memory.
import { ConfluenceClient, ResponseTooLargeError } from 'atlassian-api-client';
const client = new ConfluenceClient({
baseUrl: 'https://yourcompany.atlassian.net',
auth: { type: 'basic', email: '...', apiToken: '...' },
maxResponseBytes: 50 * 1024 * 1024, // 50 MB cap
});
try {
await client.pages.getPage('123');
} catch (error) {
if (error instanceof ResponseTooLargeError) {
console.error(`Response exceeded ${error.limitBytes} bytes (status: ${error.status ?? 'n/a'})`);
}
}Enforcement applies to responseType: 'json' and 'arrayBuffer' AND to the error-response body parsed for error-message extraction — so a misconfigured upstream returning a multi-gigabyte 5xx body cannot exhaust the Node heap on a single request. responseType: 'stream' is exempt by design: the caller owns drain/abort of the ReadableStream. Detection combines a Content-Length fast-fail with a running stream-read tally that cancels the body mid-read on overflow.
Middleware
HttpTransport accepts an optional middleware chain for cross-cutting concerns.
OAuth 2.0 Token Refresh
import { ConfluenceClient, createOAuthRefreshMiddleware } from 'atlassian-api-client';
const oauthMiddleware = createOAuthRefreshMiddleware({
accessToken: process.env.ACCESS_TOKEN!,
refreshToken: process.env.REFRESH_TOKEN!,
clientId: process.env.CLIENT_ID!,
clientSecret: process.env.CLIENT_SECRET!,
onTokenRefreshed: (tokens) => {
// Persist the new tokens
saveTokens(tokens.accessToken, tokens.refreshToken);
},
});
const client = new ConfluenceClient({
baseUrl: 'https://yourcompany.atlassian.net',
auth: { type: 'bearer', token: process.env.ACCESS_TOKEN! },
middleware: [oauthMiddleware],
});Automatically injects Authorization: Bearer and silently refreshes on 401 responses.
Token endpoint allowlist (security): tokenEndpoint defaults to https://auth.atlassian.com/oauth/token, and only that host is accepted by default. The validation happens at createOAuthRefreshMiddleware construction time — a misconfigured endpoint (typo, poisoned env var) throws ValidationError before any HTTP traffic, instead of POSTing client_id + client_secret + refresh_token to an attacker host on the first 401. For self-hosted IdPs, proxied auth, or staging endpoints, opt in explicitly:
createOAuthRefreshMiddleware({
accessToken: '...',
refreshToken: '...',
clientId: '...',
clientSecret: '...',
tokenEndpoint: 'https://idp.internal.example/oauth/token',
// REPLACES the default — mirrors ClientConfig.allowedHosts semantics.
allowedTokenEndpointHosts: ['idp.internal.example'],
});This is a separate allowlist from ClientConfig.allowedHosts because the OAuth refresh code path calls fetch directly and bypasses the transport-side check by design.
Herd protection (stability): when many concurrent requests hit a 401 at the same time, the middleware already deduplicates the token exchange to a single in-flight refresh. Two additional knobs flatten the surrounding failure modes:
createOAuthRefreshMiddleware({
accessToken: '...',
refreshToken: '...',
clientId: '...',
clientSecret: '...',
retryJitterMs: 100, // default — spread post-refresh retries over 0..100ms
failureCooldownMs: 1000, // default — replay a refresh failure for 1s instead of re-firing
});retryJitterMs(default100,0disables) staggers each waiter's retry after the shared refresh resolves, so N concurrent requests don't dispatch N simultaneous retried API calls and stampede a just-recovered backend or re-trigger upstream rate-limits.failureCooldownMs(default1000,0disables) caches the most recent refresh failure for the configured duration. Subsequent 401s during the window replay the cached error (preserving the originalOAuthErrorfor debugging) without firing a new token-endpoint call — so an auth-server outage no longer becomes an unbounded refresh loop.
Both are validated as non-negative finite numbers at construction; the jitter sleep honours RequestOptions.signal so an aborted caller doesn't pay the delay.
Atlassian Connect JWT
import { createConnectJwtMiddleware } from 'atlassian-api-client';
const connectMiddleware = createConnectJwtMiddleware({
issuer: 'com.example.my-app',
sharedSecret: process.env.CONNECT_SECRET!,
});
const client = new JiraClient({
baseUrl: 'https://yourcompany.atlassian.net',
auth: { type: 'bearer', token: '' },
middleware: [connectMiddleware],
});Signs every request with an HS256 JWT per the Atlassian Connect spec (QSH, iss, iat, exp claims).
Response Caching
import { createCacheMiddleware } from 'atlassian-api-client';
const cacheMiddleware = createCacheMiddleware({
ttl: 30_000, // 30s TTL (default: 60s)
maxSize: 200, // max entries (default: 100, FIFO eviction)
});
const client = new ConfluenceClient({
baseUrl: 'https://yourcompany.atlassian.net',
auth: { type: 'basic', email: '...', apiToken: '...' },
middleware: [cacheMiddleware],
});Caches GET responses in memory. Keyed by method + path + query string. Lazily evicts expired entries.
Request Batching / Deduplication
import { createBatchMiddleware } from 'atlassian-api-client';
const client = new JiraClient({
baseUrl: 'https://yourcompany.atlassian.net',
auth: { type: 'basic', email: '...', apiToken: '...' },
middleware: [createBatchMiddleware()],
});Coalesces concurrent identical in-flight requests so only one HTTP call is made.
OAuth Scope Detection
Map Atlassian operation names to the required Cloud OAuth 2.0 scopes:
import { detectRequiredScopes, listKnownOperations } from 'atlassian-api-client';
const scopes = detectRequiredScopes(['jira.issues.create', 'confluence.pages.get']);
// → ['write:jira-work', 'read:confluence-content.all']
const allOps = listKnownOperations();
// → ['confluence.pages.create', 'confluence.pages.delete', ...]OpenAPI Type Generation
Generate TypeScript interfaces from an OpenAPI 3.x schema:
import { generateTypes } from 'atlassian-api-client';
const spec = {
components: {
schemas: {
Issue: {
type: 'object',
properties: {
id: { type: 'string' },
summary: { type: 'string', nullable: true },
},
},
},
},
};
const { source } = generateTypes(spec);
// → 'export interface Issue { id?: string; summary?: string | null; }'Supports $ref, allOf, oneOf, anyOf, enum, nullable, and additionalProperties.
CLI
The atlas CLI provides command-line access to both APIs.
# Install globally
npm install -g atlassian-api-client
# Or use via npx
npx -p atlassian-api-client atlas --helpSyntax
atlas <api> <resource> <action> [args] [options]Auth
Via flags or environment variables:
export ATLASSIAN_BASE_URL=https://yourcompany.atlassian.net
export [email protected]
export ATLASSIAN_API_TOKEN=your-token
# Or pass inline
atlas --base-url https://... --email user@... --token ...Self-hosted / non-Atlassian baseUrl
For security, the CLI's default host allowlist only accepts *.atlassian.{net,com}, *.jira-dev.com, and *.jira.com — calls outside that suffix list fail with ValidationError. Self-hosted or proxied deployments must opt in with --allowed-hosts (or the ATLASSIAN_ALLOWED_HOSTS env var). Entries are bare hostnames (no scheme, no port) and must include the baseUrl host itself:
atlas confluence spaces list \
--base-url https://jira.internal.example \
--allowed-hosts jira.internal.exampleExamples
# Confluence
atlas confluence pages list --space-id 123
atlas confluence pages get 456
atlas confluence spaces list
# Jira
atlas jira issues get PROJ-123
atlas jira projects list
atlas jira search --jql "project = PROJ AND status = Open"
atlas jira users me
# Output formats
atlas jira issues get PROJ-123 --format table
atlas jira projects list --format minimalAPI Reference
ConfluenceClient
| Resource | Methods |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| pages | list, get, create, update, delete, listAll |
| spaces | list, get, listAll |
| blogPosts | list, get, create, update, delete, listAll |
| comments | listFooter, getFooter, createFooter, updateFooter, deleteFooter, listInline, getInline, createInline, updateInline, deleteInline |
| attachments | listForPage, get, upload, delete, listAllForPage |
| labels | listForPage, listForSpace, listForBlogPost, listAllForPage |
| contentProperties | list, get, create, update, delete |
| customContent | list, get, create, update, delete |
| whiteboards | get, create, delete |
| tasks | list, get, update |
| versions | listForPage, getForPage, listForBlogPost, getForBlogPost |
JiraClient
| Resource | Methods |
| ------------------ | -------------------------------------------------------------------------- |
| issues | get, create, update, delete, getTransitions, transition |
| projects | list, get, listAll |
| search | search, searchGet, searchAll |
| users | get, getCurrentUser, search |
| issueTypes | list, get |
| priorities | list, get |
| statuses | list |
| issueComments | list, get, create, update, delete |
| issueAttachments | list, get, upload |
| labels | list |
| boards | list, get, getIssues |
| sprints | get, create, update, delete, getIssues |
| workflows | list, get |
| dashboards | list, get, create, update, delete |
| filters | list, get, create, update, delete |
| fields | list, listAll, create, update, delete |
| webhooks | list, register, delete |
| jql | getAutocompleteData, parse, sanitize, getFieldReferenceSuggestions |
| bulk | createBulk, setPropertyBulk, deletePropertyBulk |
Recipes
Copy-paste snippets for common setups. Each recipe is self-contained.
Custom logger
Warnings the client emits through its configured logger, such as rate-limit proximity and deprecated constructor usage, are routed through the logger you provide.
import { ConfluenceClient, type Logger } from 'atlassian-api-client';
import pino from 'pino';
const pinoLogger = pino();
const logger: Logger = {
debug: (msg, ctx) => pinoLogger.debug(ctx, msg),
info: (msg, ctx) => pinoLogger.info(ctx, msg),
warn: (msg, ctx) => pinoLogger.warn(ctx, msg),
error: (msg, ctx) => pinoLogger.error(ctx, msg),
};
const client = new ConfluenceClient({
baseUrl: 'https://yourcompany.atlassian.net',
auth: { type: 'basic', email, apiToken },
logger,
});Proxy / custom fetch dispatcher
Inject an undici-powered fetch to route every request through a proxy or tune keep-alive. The custom fetch is used by the transport and by OAuth token-refresh calls.
import { ConfluenceClient } from 'atlassian-api-client';
import { fetch as undiciFetch, ProxyAgent } from 'undici';
const dispatcher = new ProxyAgent('http://proxy.internal:8080');
const client = new ConfluenceClient({
baseUrl: 'https://yourcompany.atlassian.net',
auth: { type: 'basic', email, apiToken },
fetch: ((url, init) => undiciFetch(url, { ...init, dispatcher })) as typeof fetch,
});OAuth 2.0 with token persistence
createOAuthRefreshMiddleware injects the access token on every request and refreshes automatically on a 401. A shared in-flight refresh promise prevents token-endpoint stampedes; the retryJitterMs and failureCooldownMs knobs (see the OAuth 2.0 Token Refresh section) extend that protection to the post-refresh retry burst and the auth-server-outage loop. Use onTokenRefreshed to persist new tokens so worker restarts don't lose them.
import { JiraClient, createOAuthRefreshMiddleware } from 'atlassian-api-client';
import { readFile, writeFile } from 'node:fs/promises';
const tokens = JSON.parse(await readFile('.atlassian-tokens.json', 'utf8'));
const client = new JiraClient({
baseUrl: 'https://yourcompany.atlassian.net',
auth: { type: 'bearer', token: tokens.accessToken }, // initial header; middleware keeps it fresh
middleware: [
createOAuthRefreshMiddleware({
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
clientId: process.env.ATLASSIAN_CLIENT_ID!,
clientSecret: process.env.ATLASSIAN_CLIENT_SECRET!,
// tokenEndpoint defaults to 'https://auth.atlassian.com/oauth/token'
onTokenRefreshed: async (next) => {
await writeFile(
'.atlassian-tokens.json',
JSON.stringify({ accessToken: next.accessToken, refreshToken: next.refreshToken }),
);
},
}),
],
});Retry tuning
Override the defaults per client. Non-retryable statuses (4xx except 429) are never retried regardless of retries.
const client = new JiraClient({
baseUrl: 'https://yourcompany.atlassian.net',
auth: { type: 'bearer', token },
retries: 5, // default 3
retryDelay: 500, // default 1000 ms (base for exponential backoff)
maxRetryDelay: 15_000, // default 30_000 ms (ceiling)
timeout: 20_000, // default 30_000 ms (per-request AbortController)
});Caching + batching
For a read-heavy dashboard, layer the cache outermost under auth so every request still carries a fresh token, and batch innermost so concurrent identical requests collapse into one fetch. See docs/ARCHITECTURE.md#middleware-ordering for the full ordering rationale.
import {
JiraClient,
createCacheMiddleware,
createBatchMiddleware,
createOAuthRefreshMiddleware,
} from 'atlassian-api-client';
const client = new JiraClient({
baseUrl: 'https://yourcompany.atlassian.net',
auth: { type: 'bearer', token: accessToken },
middleware: [
createOAuthRefreshMiddleware({
/* … */
}),
createCacheMiddleware({ ttl: 30_000, maxSize: 500 }),
createBatchMiddleware(),
],
});Architecture
See docs/ARCHITECTURE.md for a detailed description of the layered design, core infrastructure, and key design decisions.
Development
# Install
npm install
# Build
npm run build
# Type check
npm run typecheck
# Lint
npm run lint
# Test
npm run test
# Test with coverage
npm run test:coverage
# Full validation
npm run validateLicense
MIT
