atlassian-api-client
v4.0.0
Published
Typed Node.js/TypeScript clients and CLI for Confluence Cloud v2, Jira Cloud Platform v3, Jira Software/Agile, and DevOps APIs
Downloads
135
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, workflows, schemes, fields, and administration
- Jira Software/Agile and DevOps APIs — Boards, sprints, epics, enhanced issue pagination and counts, builds, deployments, development information, feature flags, operations, remote links, and security information
Zero runtime dependencies. Uses native fetch (Node.js 24+).
Contents
- Install
- Quick Start
- Authentication
- Pagination
- Error Handling
- Middleware
- OpenAPI Type Generation
- CLI
- Selected Resource Map
- Recipes
- Project Links
Install
npm install atlassian-api-clientSupported Runtimes
- Node.js >= 24.0.0
Use with coding agents
A Claude Code-compatible 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',
},
});Jira Software on-premises OAuth integrations
Atlassian's system-to-system OAuth proxy is opt-in. Set
softwareIntegrationProxy.cloudId to route Jira Software Development
Information, Builds, Deployments, and Feature Flag ingestion through
api.atlassian.com:
const client = new JiraClient({
baseUrl: 'https://yourcompany.atlassian.net',
auth: {
type: 'bearer',
token: process.env.ATLASSIAN_OAUTH_TOKEN!,
},
softwareIntegrationProxy: {
cloudId: process.env.ATLASSIAN_SOFTWARE_CLOUD_ID!,
},
});
await client.bulk.submitBuilds({ builds: [] });Builds, Deployments, and Feature Flag ingestion use proxy version 0.1.
Development Information changes from the site route /rest/devinfo/0.10 to the
proxy route /jira/devinfo/0.1/cloud/{cloudId}. In particular,
bulk.submitFeatureFlags uses
/jira/featureflags/0.1/cloud/{cloudId}/bulk. Deployment gating-status is not
available through the integration proxy, so that request remains on the tenant
/rest/deployments/0.1 route. Feature Flag lookup/deletion and every other Jira
resource continue to use baseUrl. The option requires bearer auth and does not
activate merely because bearer auth is configured. The built-in transport
authorizes exactly api.atlassian.com in addition to the existing
allowedHosts; custom injected transports receive the fully-qualified proxy
URLs and remain responsible for their own credential-boundary enforcement.
See Atlassian's guide to integrating Jira Software Cloud with on-premises tools.
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.get('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).
Verifying inbound asymmetric (RS256) JWTs
Atlassian signs lifecycle callbacks (installed/uninstalled) and context/iframe
tokens with RS256 using a per-install key pair. Apps verify those tokens
against the matching public key — verifyConnectAsymmetricJwt does exactly
that, with strict algorithm pinning (only RS256 is accepted; none and
HS256 are rejected to defeat algorithm-confusion attacks).
import { verifyConnectAsymmetricJwt } from 'atlassian-api-client';
const claims = await verifyConnectAsymmetricJwt(token, {
// The library core never makes network calls. Supply a resolver that fetches
// the PEM public key by `kid`, or pass a `publicKey` you already hold.
publicKeyResolver: async (kid) =>
fetch(`https://connect-install-keys.atlassian.com/${kid}`).then((r) => r.text()),
issuer: clientKey, // require iss === clientKey
audience: 'https://my-app.example.com', // require this value in aud
});The verifier checks the signature before trusting any claim, then validates
exp/iat/nbf (with a 30s default clock skew, configurable via
maxClockSkewSeconds), and optionally iss, aud, and qsh. All failures
throw ValidationError with a distinct, non-leaking message (the token,
signature, and key material are never echoed).
Response Caching
import { createCacheMiddleware } from 'atlassian-api-client';
const cacheMiddleware = createCacheMiddleware({
ttl: 30_000, // 30s TTL (default: 60s)
maxSize: 200, // max entries (default: 100, LRU eviction)
});
const client = new ConfluenceClient({
baseUrl: 'https://yourcompany.atlassian.net',
auth: { type: 'basic', email: '...', apiToken: '...' },
middleware: [cacheMiddleware],
});Caches GET responses in memory. Keys include auth identity, method, path, and query string so responses remain partitioned by caller. Expired entries are lazily evicted; capacity pressure evicts the least recently used entry.
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.
Circuit Breaker
import { createCircuitBreakerMiddleware, CircuitBreakerOpenError } from 'atlassian-api-client';
const client = new JiraClient({
baseUrl: 'https://yourcompany.atlassian.net',
auth: { type: 'basic', email: '...', apiToken: '...' },
middleware: [createCircuitBreakerMiddleware({ failureThreshold: 5, resetTimeoutMs: 30_000 })],
});
try {
await client.issues.get('PROJ-1');
} catch (error) {
if (error instanceof CircuitBreakerOpenError) {
console.warn(`Circuit open; retry after ~${error.msUntilHalfOpen}ms`);
}
}Protects against cascading failures from an unhealthy downstream. After failureThreshold consecutive qualifying failures (5xx, network errors, timeouts) the breaker opens and subsequent requests are rejected immediately with CircuitBreakerOpenError — no HTTP calls are made. After resetTimeoutMs elapses, the transition to half-open is lazy: it occurs on the next incoming request after the timeout expires, not via a background timer. That request becomes the single trial; a successful trial resets the breaker to closed.
Failure classification: only NetworkError, TimeoutError, and HttpError with a 5xx status count as failures. 4xx responses (including 429), ValidationError, abort errors, and other non-transport errors pass through without affecting the counter — the circuit only opens on infrastructure-level failures.
Per-baseUrl semantics: each createCircuitBreakerMiddleware() call creates an isolated state machine. Install one instance per client to get per-baseUrl isolation:
// Isolated breakers: a Confluence outage does not block Jira calls.
const confluenceBreaker = createCircuitBreakerMiddleware();
const jiraBreaker = createCircuitBreakerMiddleware();
const confluenceClient = new ConfluenceClient({ ..., middleware: [confluenceBreaker] });
const jiraClient = new JiraClient({ ..., middleware: [jiraBreaker] });Recommended compose order: place the circuit breaker outermost (first in the array) so an open breaker short-circuits before spending a rate-limit token or running OAuth refresh logic:
middleware: [
createCircuitBreakerMiddleware({ failureThreshold: 5, resetTimeoutMs: 30_000 }),
createOAuthRefreshMiddleware(...),
createCacheMiddleware(),
createBatchMiddleware(),
]Retry interaction: executeWithRetry never retries a CircuitBreakerOpenError. There are two reasons: (a) burning through retry attempts wastes quota before surfacing the open state to the caller, and (b) if the reset timer elapses mid-retry-loop, the first retry after timeout would consume the single HALF_OPEN trial attempt.
Interaction with retries option: the circuit breaker middleware runs inside executeWithRetry, so each retry attempt counts as an independent qualifying failure. With the default retries: 3, a single logical user request can contribute up to 4 qualifying failures (initial attempt + 3 retries). Choose failureThreshold relative to your retries setting — for example, failureThreshold: 5 with retries: 3 means the breaker can open after as few as 2 logical requests that each exhaust all retries.
Proactive Rate Limiting (Token Bucket)
Smooth outbound traffic before it reaches the Atlassian API with a client-side token-bucket limiter. Unlike the reactive RateLimitError path (which fires after the server returns HTTP 429), this middleware enforces a local quota and waits when the bucket is empty instead of dispatching the request and letting the server reject it.
import { JiraClient, createRateLimiterMiddleware } from 'atlassian-api-client';
const client = new JiraClient({
baseUrl: 'https://yourcompany.atlassian.net',
auth: { type: 'bearer', token: accessToken },
middleware: [
createRateLimiterMiddleware({
tokensPerInterval: 10, // allow up to 10 requests per second
intervalMs: 1000,
maxWaitMs: 5000, // throw RateLimiterExhaustedError after 5s wait
}),
],
});Key behaviours:
- Bucket starts full — an initial burst up to
tokensPerIntervalrequests is allowed without any delay. - Waits, never rejects by default — when the bucket is empty the middleware sleeps until the next token is available, smoothing traffic rather than failing fast. Set
maxWaitMsto cap the wait and throwRateLimiterExhaustedErrorinstead. - Abort-aware — the wait honours
RequestOptions.signal, so caller-initiated cancellations and transport timeouts still work correctly during a wait. - Retry interaction — each retry attempt re-enters the middleware and consumes a token. This is intentional: retried requests represent real outbound traffic and should be rate-limited just like first attempts.
- Recommended ordering — place this middleware after the circuit breaker (when present) so a tripped circuit short-circuits without burning tokens, and after (outside) cache/batch so cached/deduped hits don't burn a token.
RateLimiterExhaustedError (code 'RATE_LIMITER_EXHAUSTED') is a client-side error, distinct from the server-side RateLimitError (HTTP 429).
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']);
// → granular Jira + Confluence scopes, sorted and deduplicated
const softwareScopes = detectRequiredScopes([
'jira.boards.getBacklogApproximateCount',
'jira.linkedWorkspaces.listSecurity',
]);
// → ['read:board-scope:jira-software', 'read:issue-details:jira', 'read:security:jira']
const allOps = listKnownOperations();
// → ['confluence.pages.create', 'confluence.pages.delete', ...]The operation annotations contain 38 Confluence v2 scopes, 33 Jira Software scopes, and 180 Jira Platform Beta scopes (247 unique granular strings after overlap). Validation also recognizes 8 granular catalog entries not referenced by current operations and 16 classic or Jira Software compatibility scopes, for 271 accepted strings in total. detectRequiredScopes() continues to recommend granular scopes; the operation registry remains a selected convenience mapping rather than a claim that every SDK method has been mapped. atlas scopes validate <scope>... validates scope strings without making a network request.
OpenAPI Type Generation
generateTypes(spec) is a public utility that turns an OpenAPI 3.x spec object into TypeScript
interface and type declarations. Call it on your own specs — this library does not
vendor or commit any generated types.
import { generateTypes } from 'atlassian-api-client';
const spec = {
openapi: '3.0.1',
info: { title: 'My API', version: '1.0.0' },
components: {
schemas: {
Issue: {
type: 'object',
properties: {
id: { type: 'string' },
summary: { type: 'string', nullable: true },
},
},
},
},
};
const { source, typeNames } = generateTypes(spec);
// source → 'export interface Issue { id?: string; summary?: string | null; }'
// typeNames → ['Issue']Supports $ref, allOf, oneOf, anyOf, enum, nullable, and additionalProperties.
Spec Drift Guard
A CI script (scripts/regenerate-types.ts) monitors three upstream Atlassian OpenAPI specs. It
fetches each live spec, verifies type generation, and compares its canonical contract fingerprint
with the pinned snapshot in spec/. Routes, parameters, request/response schemas, and security
metadata are included; prose-only descriptions and examples are ignored.
The script commits nothing — it is a read-only smoke-test.
# Run locally
npm run spec-drift
npm run api-coverageExample output:
✓ jiraPlatform: 971 types; contract ecba06ea28d7 (https://developer.atlassian.com/cloud/.../swagger-v3.v3.json)
✓ jiraSoftware: 66 types; contract 1b3a0fd3f68e (https://developer.atlassian.com/cloud/.../swagger.v3.json)
✓ confluence: 142 types; contract dbd7f3110251 (https://developer.atlassian.com/cloud/.../openapi-v2.v3.json)api-coverage compares executable SDK routes with the pinned snapshots and fails on unresolved
route extraction, a missing non-deprecated operation, or an unexpected in-scope SDK route. Its lexical pass uses a code-token-only
view to locate client declarations, resource wiring, resource-local path assignments, helper
returns, and call sites, then an aligned literal view to resolve their values and paths. Comments,
quoted/template examples, and regex literals cannot satisfy coverage or shadow runtime wiring or
path discovery. Runtime path transformations, unsafe reassignments, computed request properties,
shadowed pagination/query imports, uncalled or non-public request callables, unwired or duplicate
resource classes, statically dead calls, and unresolved OpenAPI Path Item references fail closed;
nested resource modules are included and all eight OpenAPI HTTP operation keys are audited.
Deprecated omissions remain visible in the report without failing the check.
In CI, both guards run on a weekly schedule and on manual dispatch only (.github/workflows/spec-drift.yml).
It deliberately does not run on push or pull_request — a transient upstream outage must never
block contributor PRs. A scheduled-job failure is the intended signal that a spec has drifted.
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
Use environment variables so credentials do not leak through shell history or process listings:
export ATLASSIAN_BASE_URL=https://yourcompany.atlassian.net
export [email protected]
export ATLASSIAN_API_TOKEN=your-token
# Bearer auth: OAuth 2.0 access token or PAT
export ATLASSIAN_AUTH_TYPE=bearer
export ATLASSIAN_API_TOKEN=your-bearer-tokenATLASSIAN_AUTH_TYPE defaults to basic. Bearer mode does not require ATLASSIAN_EMAIL.
Credential flags remain available for backward compatibility, but do not use them in interactive shells, scripts, CI logs, or chat transcripts.
For Jira Software on-premises integrations, add the Jira-only cloud ID option. It routes Development Information, Builds, Deployments, and Feature Flag ingestion through Atlassian's OAuth proxy; bearer auth alone keeps the normal site routes:
export ATLASSIAN_AUTH_TYPE=bearer
export ATLASSIAN_API_TOKEN=your-system-to-system-oauth-token
export ATLASSIAN_SOFTWARE_CLOUD_ID=11111111-2222-3333-4444-555555555555
atlas jira bulk submit-builds --value '{"builds":[]}'
# Equivalent per-command opt-in
atlas jira bulk submit-deployments \
--software-cloud-id 11111111-2222-3333-4444-555555555555 \
--value '{"deployments":[]}'
atlas jira bulk submit-feature-flags \
--software-cloud-id 11111111-2222-3333-4444-555555555555 \
--value '{"flags":[]}'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 minimalScope validation
Check whether OAuth 2.0 scope strings are recognised Atlassian Cloud scopes — auth-free, no network calls. Prints a JSON { valid, unknown, allValid } report; exits 0 when every scope is valid and 1 when any are unknown (handy in CI before requesting consent):
atlas scopes validate read:issue:jira write:issue:jira
# → { "valid": ["read:issue:jira", "write:issue:jira"], "unknown": [], "allValid": true }Invoking atlas scopes validate with no scope arguments prints the full known-scope catalog (to stderr) as a usage hint.
Selected Resource Map
The clients expose a broad Atlassian API surface. The tables below highlight common entry points rather than every available resource and method. Use TypeScript autocomplete for the complete client API, or run atlas confluence --help and atlas jira --help for the complete CLI resource lists.
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 |
| 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,
});X-Request-Id propagation
The client always captures the server-assigned request id from response headers (X-AREQUESTID first, then X-Request-Id) and exposes it as response.requestId. On error responses the id is available as error.requestId (also serialised by error.toJSON() for structured logging).
Outbound id generation is opt-in. Set requestId: { generate: true } to attach a UUID to every request. The same id is reused across all retry attempts so the server can correlate a logical request regardless of how many tries it took.
import { ConfluenceClient, HttpError, type RequestIdOptions } from 'atlassian-api-client';
const client = new ConfluenceClient({
baseUrl: 'https://yourcompany.atlassian.net',
auth: { type: 'basic', email, apiToken },
requestId: {
generate: true, // attach X-Request-Id to every outbound request
header: 'X-Request-Id', // default; override to match your gateway's expected name
// generator: () => myIdGen(), // optional: replace crypto.randomUUID with your own factory
// readResponseHeaders: ['X-AREQUESTID', 'X-Request-Id'], // default inbound header list
},
});
// Success path — the server's response id is on the ApiResponse
const response = await client.pages.get('123456');
console.log('server request id:', response.requestId); // e.g. 'arq-a1b2c3...'
// Error path — same field on HttpError, included in toJSON() for structured loggers
try {
await client.pages.get('missing');
} catch (err) {
if (err instanceof HttpError) {
console.error('failed request id:', err.requestId);
}
}Proxy / custom fetch dispatcher
Install undici, then inject an undici-powered fetch to route transport requests through a proxy or tune keep-alive. OAuth token refresh has a separate fetch option; pass the same function to createOAuthRefreshMiddleware when refresh calls should use the proxy too.
import { ConfluenceClient } from 'atlassian-api-client';
import { fetch as undiciFetch, ProxyAgent } from 'undici';
const dispatcher = new ProxyAgent('http://proxy.internal:8080');
const proxyFetch = ((url, init) => undiciFetch(url, { ...init, dispatcher })) as typeof fetch;
const client = new ConfluenceClient({
baseUrl: 'https://yourcompany.atlassian.net',
auth: { type: 'basic', email, apiToken },
fetch: proxyFetch,
});For OAuth refresh, also pass fetch: proxyFetch inside createOAuthRefreshMiddleware({ ... }).
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.
Project Links
Development
# Install
npm install
# Build
npm run build
# Type check
npm run typecheck
# Lint
npm run lint
# Run every test suite
npm run test
# Run the fast TypeScript/V8 suite only
npm run test:unit
# Run all API gap analyzer scenarios (bounded to four processes)
npm run test:api-gap
# TypeScript tests with exact 100% V8 coverage
npm run test:coverage
# Full validation
npm run validateLicense
MIT
