🌐 English · Русский
Jira.js is a TypeScript client for the Atlassian Jira Cloud REST APIs, for Node.js and browsers. It covers three surfaces:
- Jira Cloud platform API - issues, projects, fields, workflows
- Jira Agile API - sprints, boards, backlog
- Jira Service Management API - requests, queues, organizations
6.0 is a rewrite, not a refresh.
npm install jira.jsnow installs 6.x. Read MIGRATION.md before upgrading — it says plainly who should stay onjira.js@5, which is supported until the end of 2026.
- ✅ Type-Safe: every endpoint, parameter and model is typed, and
src/ships with the package so "go to definition" lands on the real source - ✅ Validated at runtime: responses are checked against a schema, and drift is reported by field instead of surfacing as
undefinedthree frames later - ✅ Promise-based: clean, async/await-friendly methods throughout
- ✅ Tree-Shakable: import a single endpoint function instead of a whole client
- ✅ Universal: one ESM build for Node.js 22+ and modern browsers
- ✅ One dependency:
zod, and nothing else - ✅ Typed errors: a hierarchy with predicates that survive bundling, minification and duplicate installs
- ✅ OAuth 2.0 (3LO): automatic refresh, single-flight,
401retry and cloud id resolution
Built for Jira integrations, automation, webhook handlers, CI/CD pipelines and browser-based tools.
Requires Node.js 22 or newer. The package is ESM-only — there is no CommonJS build.
# Using npm
npm install jira.js
# Using yarn
yarn add jira.js
# Using pnpm
pnpm add jira.jsTypeScript users: type definitions are included - no additional @types package needed.
import { createCloudClient } from 'jira.js';
const jira = createCloudClient({
host: 'https://your-domain.atlassian.net',
auth: {
type: 'basic',
email: 'your@email.com',
apiToken: 'YOUR_API_TOKEN', // Create one: https://id.atlassian.com/manage-profile/security/api-tokens
},
});
const project = await jira.projects.getProject({ projectIdOrKey: 'YOUR_PROJECT_KEY' });
const issue = await jira.issues.createIssue({
fields: {
summary: 'Hello Jira.js!',
issuetype: { name: 'Task' },
project: { key: project.key },
},
});
console.log(`Issue created: ${issue.key}`);host is the bare site URL — the API path belongs to the request, not here.
Need more than one surface? Build the client once and hand it to each factory. Under OAuth 2.0 this matters: two clients mean two token states, and since Atlassian rotates the refresh token on every refresh, whichever refreshes first invalidates the other's copy.
import { createClient } from 'jira.js/core';
import { createAgileClient, createCloudClient } from 'jira.js';
const client = createClient({ host, auth });
const jira = createCloudClient(client);
const agile = createAgileClient(client);📚 Full API reference, guides, and examples available at: https://mrrefactoring.github.io/jira.js/
The documentation includes:
- Complete API reference for all endpoints
- TypeScript examples and code samples
- Authentication guides
- Error handling patterns
- Best practices and tips
- Jira Cloud platform API: issues, projects, users, fields, workflows, schemes
- Jira Software (Agile) API: sprint management, boards, backlogs, agile workflows
- Jira Service Management API: request handling, queues, customers, organizations
There is one platform surface, generated from Jira's v3 specification. Version2Client and Version3Client are gone — the difference between them was never the endpoints, it was rich text. Rich-text fields still accept a wiki-markup string: that write is routed through Jira's v2 endpoint, which parses the markup server-side, and the result is read back so what you get is a real Atlassian Document Format document.
// Wiki markup — still works, still formats
await jira.issueComments.addComment({
issueIdOrKey: 'PROJ-1',
body: 'h2. Heading\n\n*bold* and {code}inline{code}',
});Reads always come back as a document, never as a string.
Authentication is the auth field — a discriminated union on type.
- Create an API token: https://id.atlassian.com/manage-profile/security/api-tokens
- Configure the client:
const jira = createCloudClient({
host: 'https://your-domain.atlassian.net',
auth: { type: 'basic', email: 'YOUR@EMAIL.ORG', apiToken: 'YOUR_API_TOKEN' },
});When something else already obtained an access token and you manage its lifetime yourself:
const jira = createCloudClient({
host: 'https://your-domain.atlassian.net',
auth: { type: 'bearer', token: 'YOUR_ACCESS_TOKEN' },
});Nothing is refreshed for you here — when the token expires, requests fail with AuthError.
jira.js supports the full Atlassian OAuth 2.0 (3LO) flow. Provide refresh credentials and the client refreshes the access token before expiry (and on 401), collapses concurrent refreshes into one call, persists the rotated refresh token via onTokenRefresh, and routes requests through the API gateway (https://api.atlassian.com/ex/jira/{cloudId}) — so no host is needed. clientSecret and refresh are server-side only.
const jira = createCloudClient({
// no `host` — the cloudId is resolved automatically (pass `siteUrl` or `cloudId` to pin it)
auth: {
type: 'oauth2',
accessToken: 'CURRENT_ACCESS_TOKEN',
refreshToken: 'CURRENT_REFRESH_TOKEN',
clientId: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_CLIENT_SECRET',
expiresAt: Date.now() + 3600 * 1000, // optional; epoch milliseconds
onTokenRefresh: async ({ accessToken, refreshToken, expiresAt }) => {
await saveTokens({ accessToken, refreshToken, expiresAt }); // persist the rotated tokens
},
},
});Persisting the rotated refresh token is not optional — Atlassian invalidates the previous one on every refresh.
jira.js also exports stateless helpers for the authorization-code flow — generateAuthorizationUrl, exchangeAuthorizationCode, refreshOAuth2Token, getAccessibleResources, parseCallbackUrl. See the step-by-step OAuth 2.0 guide.
JWT (Atlassian Connect) is not supported in 6.0 and has no replacement. If you authenticate Connect installations with a shared secret, stay on
jira.js@5— see MIGRATION.md. Atlassian Connect itself is reaching end of support in Q4 2026.
Every failure arrives as one of the library's own error types, each with a predicate:
import { isNotFoundError, isRateLimitError } from 'jira.js';
try {
await jira.issues.getIssue({ issueIdOrKey: 'INVALID-123' });
} catch (error) {
if (isNotFoundError(error)) return null;
if (isRateLimitError(error) && error.retryAfterMs) {
await new Promise(resolve => setTimeout(resolve, error.retryAfterMs));
}
throw error;
}| Error | When | Extra |
|---|---|---|
ApiError |
Any non-2xx; base of the ones below | status, statusText, body |
AuthError |
401 |
|
ScopeError |
401, token lacks the scope |
|
ForbiddenError |
403 |
|
NotFoundError |
404 |
|
RateLimitError |
429 |
retryAfterMs |
ServerError |
5xx |
|
NetworkError |
Request never completed | code |
OAuthError |
The token flow failed | |
ConfigError |
Impossible client configuration | |
SchemaMismatchError |
2xx of the wrong shape | report |
Use the predicates rather than instanceof: they read a branded symbol instead of walking the prototype chain, so they keep working when a bundler splits chunks, when minification renames classes, and when two copies of the package end up in one node_modules.
Retries are off by default. retry: { maxAttempts, initialDelayMs, backoffFactor } opts in for network errors and 502/503/504 only — never 4xx, never other 5xx.
Every response is checked against a schema. When one does not match, the library does not throw: the body comes back unvalidated and the problem is reported once per distinct field, on stderr.
[jira.js] GET /rest/api/3/project/{projectIdOrKey}/role answered with something the schema
does not describe: at `10002`, expected string, got number. The response is returned
unvalidated.
The shapes Jira sends depend on things a library cannot see — your site's locale, which features are on, team-managed versus company-managed projects, an enum Atlassian grew this week. A schema here being wrong about one of those is not your bug and should not stop your program.
const jira = createCloudClient({
host,
auth,
onSchemaMismatch: 'warn', // 'silent' | 'throw' | (report) => void
});Use 'throw' in a test suite, where a mismatch is the thing under test. The report names field paths and types and never the values at them — it is meant to be pasted into an issue. See the Response Validation guide.
Access endpoints using the client.<group>.<method> pattern:
// Get all projects
const projects = await jira.projects.searchProjects();
// Create a sprint (Agile surface)
const sprint = await agile.sprint.createSprint({ name: 'Q4 Sprint' });Available API groups:
🔽 Agile Cloud API
🔽 Jira Cloud platform API
- api
- announcementBanner
- appDataPolicy
- applicationRoles
- appMigration
- auditRecords
- avatars
- classificationLevels
- dashboards
- filters
- fieldSchemes
- filterSharing
- groupAndUserPicker
- groups
- instanceInformation
- issues
- issueAttachments
- issueBulkOperations
- issueComments
- issueCustomFieldAssociations
- issueCustomFieldConfigurationApps
- issueCommentProperties
- issueFields
- issueFieldConfigurations
- issueCustomFieldContexts
- issueCustomFieldOptions
- issueCustomFieldOptionsApps
- issueCustomFieldValuesApps
- issueLinks
- issueLinkTypes
- issueNavigatorSettings
- issueNotificationSchemes
- issuePriorities
- issueProperties
- issueRedaction
- issueRemoteLinks
- issueResolutions
- issueSearch
- issueSecurityLevel
- issueSecuritySchemes
- issueTypes
- issueTypeSchemes
- issueTypeScreenSchemes
- issueTypeProperties
- issueVotes
- issueWatchers
- issueWorklogs
- issueWorklogProperties
- jiraExpressions
- jiraSettings
- jql
- jqlFunctionsApps
- labels
- licenseMetrics
- migrationOfConnectModulesToForge
- myself
- permissions
- permissionSchemes
- plans
- prioritySchemes
- projects
- projectTemplates
- projectAvatars
- projectCategories
- projectClassificationLevels
- projectComponents
- projectEmail
- projectFeatures
- projectKeyAndNameValidation
- projectPermissionSchemes
- projectProperties
- projectRoles
- projectRoleActors
- projectTypes
- projectVersions
- screens
- screenTabs
- screenTabFields
- screenSchemes
- serverInfo
- serviceRegistry
- status
- tasks
- teamsInPlan
- timeTracking
- uiModificationsApps
- users
- userNavProperties
- userProperties
- userSearch
- webhooks
- workflows
- workflowTransitionRules
- workflowSchemes
- workflowSchemeProjectAssociations
- workflowSchemeDrafts
- workflowStatuses
- workflowStatusCategories
- workflowTransitionProperties
- appProperties
- dynamicModules
🔽 Service Desk API
See the full endpoint reference in the API documentation.
The package declares "sideEffects": false and ships one module per source file, so a bundler can drop everything you do not import.
createCloudClient is convenient and expensive: it wires up every endpoint on the platform surface. For a bundle that calls a handful of endpoints, compose the client yourself from the flat functions instead:
import { createClient } from 'jira.js/core';
import { getIssue, createIssue } from 'jira.js/cloud';
import { createSprint } from 'jira.js/agile';
const client = createClient({
host: 'https://your-domain.atlassian.net',
auth: { type: 'basic', email, apiToken },
});
const issue = await getIssue(client, { issueIdOrKey: 'KEY-1' });Every function takes the client as its first argument — the same client the factories build, so the two styles mix freely.
| Import | Contents |
|---|---|
jira.js |
The three factories, error types and predicates, OAuth helpers |
jira.js/core |
createClient, transport, errors, OAuth, multipart helpers |
jira.js/cloud |
Platform API functions, parameters and response types |
jira.js/agile |
Agile API functions, parameters and response types |
jira.js/serviceDesk |
Service Management functions, parameters and response types |
jira.js/browser |
Prebuilt browser bundle |
The surface subpaths carry the types alongside the functions, so a type-only import costs nothing at runtime:
import type { Issue, GetIssue } from 'jira.js/cloud';The three surfaces are not re-exported from the root — they collide on a handful of names, so import from the one you mean.
Deep imports need an
exports-aware resolver:moduleResolution: "bundler","node16"or"nodenext". The legacy"node"resolution cannot see them, and cannot load an ESM-only package either.
Schemas are the bulk of the package — each response type carries the schema it is validated against — so the saving is roughly proportional to how much of the API you leave out.
Jira.js is perfect for:
- 🔄 CI/CD Integration: Automate issue creation and updates in your deployment pipelines
- 🤖 Automation Scripts: Build custom automation for Jira workflows and processes
- 📊 Reporting & Analytics: Extract and analyze Jira data for custom dashboards
- 🔗 Webhook Handlers: Process Jira webhooks and integrate with external systems
- 🛠️ Custom Tools: Build admin tools, migration scripts, and custom Jira applications
- 📱 Browser Apps: Create browser-based Jira management interfaces
- 🔌 Third-Party Integrations: Connect Jira with other services and platforms
Q: Does this work with Jira Server/Data Center?
A: No, Jira.js is designed specifically for Jira Cloud. For on-premise Jira, consider using the REST API directly.
Q: Is TypeScript required?
A: No, but TypeScript is fully supported with comprehensive type definitions. You can use Jira.js with plain JavaScript too.
Q: Can I use this in the browser?
A: Yes. The package is browser-safe throughout and ships a prebuilt bundle at jira.js/browser. Calling Jira directly from a page is usually blocked by CORS and exposes credentials to anyone with devtools, so this suits extensions, Forge apps and proxied setups rather than putting an API token in a web app.
Q: How do I handle authentication?
A: Email + API token, a bearer token, or OAuth 2.0 (3LO) with automatic refresh. See the Authentication section above.
Q: Can I still use CommonJS?
A: No. 6.0 is ESM-only — require('jira.js') does not work. From a CommonJS module, use a dynamic await import('jira.js'), or stay on jira.js@5.
Q: What happened to JWT / Atlassian Connect?
A: It was removed in 6.0 and has no replacement. Stay on jira.js@5, which receives security and critical fixes until the end of 2026 — when Atlassian Connect itself reaches end of support.
Q: A response failed validation. Is that a bug in my code?
A: Usually not. It means the schema shipped here is behind what your Jira actually sends. By default the body is returned anyway and the problem is reported once — please open an issue with the report, which contains field paths and types and no values from your data.
Explore our other Atlassian integration libraries:
- Confluence.js - Interact with Confluence API
- Trello.js - Trello API integration
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
MIT License © MrRefactoring
See LICENSE for details.