At a glance
Start
--service. When it is the only enabled service, it listens on http://localhost:4000.
Without seed config, the site has one admin account, [email protected] (Site Admin), whose API token is jira_test_api_token.
Point jira.js at the emulator with host:
URL mapping
Auth
- Basic: an account’s email and API token, as Jira Cloud takes them. Seeded accounts use
jira_test_api_tokenunless the seed setsapi_token. Wrong credentials or a deactivated account get HTTP 401 withX-Seraph-LoginReason: AUTHENTICATED_FAILED, which jira.js reports as anAuthError. - OAuth 2.0 (3LO): an access token from the authorization code flow, accepted only through the gateway path and checked against the operation’s scopes.
- Bearer: a token the CLI issued. The default token (and any token the CLI does not know) acts as the first site admin; a token whose login is an account id or email acts as that account.
GET /rest/api/3/serverInfo needs none.
Site admins (admin: true) manage projects and admin webhooks; other accounts get HTTP 403 there. Every account can browse, create, edit, and transition every issue. Comments can be edited or deleted by their author or an admin.
OAuth 2.0 (3LO)
Apps that act for a user sign in the way Atlassian documents: send the user to/authorize, exchange the code at /oauth/token, find the site’s cloud id with /oauth/token/accessible-resources, and call /ex/jira/{cloudId}/... with the access token.
GET /authorizetakesaudience=api.atlassian.com,client_id,scope,redirect_uri,state, andresponse_type=code, and shows a consent screen listing the scopes and the site’s active accounts. Choosing an account redirects toredirect_uriwithcodeandstate; Cancel redirects witherror=access_denied. An unknown app or callback URL gets an error page. Other problems redirect witherroranderror_description:Service not found: <audience>for a wrong audience,unsupported_response_type, andinvalid_scopefor an unknown scope or one the app was not given.POST /oauth/tokentakes JSON (as Atlassian documents) or a form, with the client credentials in the body or as Basic auth.grant_type=authorization_codetakescodeand the sameredirect_uri; codes last ten minutes and work once.grant_type=refresh_tokentakesrefresh_token. Responses are{ access_token, expires_in: 3600, token_type: "Bearer", scope }, plusrefresh_tokenwhenoffline_accesswas granted. Wrong client credentials get HTTP 401{ "error": "access_denied", "error_description": "Unauthorized" }; a bad code gets HTTP 403invalid_grant.- Refresh tokens rotate: each refresh returns a new one. The one sent may be replayed for ten minutes; replaying it later gets HTTP 403
{ "error": "invalid_grant", "error_description": "Unknown or invalid refresh token." }and revokes every token from that sign-in. Refresh tokens expire after 90 days without use. GET /oauth/token/accessible-resourcesreturns the site,[{ id, name, url, scopes, avatarUrl }], whereidis the cloud id andurlis the emulator’s address.GET /meneedsread:meand returns the user’saccount_id,email,name,picture,account_status,nickname,zoneinfo,locale, andextended_profile.- Access tokens are JWTs that last an hour. They work only through
/ex/jira/{cloudId}; on the site’s own paths they get HTTP 401 withX-Seraph-LoginReason. An expired, revoked, or forged token gets HTTP 401{ "code": 401, "message": "Unauthorized" }, which makes jira.js refresh and retry. - Each operation takes its classic scope or its main granular scope, and a token without either gets HTTP 401
{ "code": 401, "message": "Unauthorized; scope does not match" }, which jira.js reports as aScopeError. Reads takeread:jira-work(read:issue:jira,read:project:jira,read:filter:jira, and so on), writes takewrite:jira-work, users andmyselftakeread:jira-user, project changes takemanage:jira-project, and app webhooks takemanage:jira-webhook. The Agile API takes only Jira Software scopes such asread:board-scope:jira-software,write:sprint:jira-software, andread:epic:jira-software. Admin webhooks take no scope, so apps cannot use them. - One grant exists per app per account; consenting again replaces its scopes.
oauth_apps to check client ids, callback URLs, and scopes the way the developer console does. With no apps seeded, any client id is accepted with any scope and callback URL, and the app is registered with the secret it sends on its first token exchange.
jira.js reaches auth.atlassian.com and api.atlassian.com by fixed URLs, so give it a fetch that sends them to the emulator:
REST API
Issues
POST /rest/api/3/issue,POST /rest/api/3/issue/bulk(up to 50; failures listed byfailedElementNumber)GET /rest/api/3/issue/{issueIdOrKey}withfields(*all,*navigable, names,-name) andexpand(names,renderedFields,transitions,changelog)PUT /rest/api/3/issue/{issueIdOrKey}withfieldsandupdateoperations (set;addandremovefor labels and multi-select fields;addfor comments) andreturnIssue=trueDELETE /rest/api/3/issue/{issueIdOrKey}(an issue with subtasks needsdeleteSubtasks=true)PUT /rest/api/3/issue/{issueIdOrKey}/assignee(accountId: nullunassigns,"-1"picks the project default)GET /rest/api/3/issue/createmeta/{projectIdOrKey}/issuetypesand.../issuetypes/{issueTypeId}GET /rest/api/3/issue/{issueIdOrKey}/editmetaGET/POST /rest/api/3/issue/{issueIdOrKey}/transitionsGET /rest/api/3/issue/{issueIdOrKey}/changelogGET/POST /rest/api/3/issue/{issueIdOrKey}/comment,GET/PUT/DELETE .../comment/{id}GET/POST/DELETE /rest/api/3/issue/{issueIdOrKey}/watchers
description, environment, comment bodies) must be an Atlassian document on v3; anything else fails with Operation value must be an Atlassian Document.
Settable fields are summary, description, environment, issuetype, priority, assignee, reporter, labels, duedate, parent, resolution, and custom fields. Subtasks need a parent in the same project; standard issues may have an epic as parent. Setting anything else fails with Field '<id>' cannot be set. It is not on the appropriate screen, or unknown.
Edits and transitions record changelog items (field, fieldId, from, fromString, to, toString) for every field they change.
Workflows and transitions
New projects use Jira’s simplified workflow: To Do, In Progress, and Done, each reachable from any status through a global transition (11, 21, 31). Seeded workflows can limit transitions to certain statuses. Moving into a status in the done category sets the resolution (Done unless the transition sets one) and resolutiondate; moving out clears both.
Projects
GET /rest/api/3/project,GET /rest/api/3/project/search(query,keys,id,typeKey,orderBy)POST /rest/api/3/project(admins; returns{ self, id, key }with a numericid)GET/PUT/DELETE /rest/api/3/project/{projectIdOrKey}(changing a project key is not supported)GET /rest/api/3/project/{projectIdOrKey}/statuses
Users and metadata
GET /rest/api/3/myself,GET /rest/api/3/user,GET /rest/api/3/user/search,GET /rest/api/3/users,GET /rest/api/3/users/search,GET /rest/api/3/user/assignable/searchGET /rest/api/3/serverInfoGET /rest/api/3/status,GET /rest/api/3/status/{idOrName},GET /rest/api/3/statuscategory,GET /rest/api/3/statuscategory/{idOrKey}GET /rest/api/3/issuetype,GET /rest/api/3/issuetype/{id},GET /rest/api/3/issuetype/projectGET /rest/api/3/priority,GET /rest/api/3/priority/search,GET /rest/api/3/priority/{id}GET /rest/api/3/resolution,GET /rest/api/3/resolution/search,GET /rest/api/3/resolution/{id}GET /rest/api/3/field
REST API v2
Every/rest/api/2/... path answers like its /rest/api/3/... twin, except that rich text is a string: strings sent there become one paragraph per line, and documents read back come out as their plain text. jira.js sends requests there when given wiki markup strings, so both styles work against the emulator.
Atlassian gateway
/ex/jira/{cloudId}/... serves the same API for the site’s cloud id, the way OAuth 2.0 apps call Jira through api.atlassian.com. It is the only place OAuth access tokens work. Any other cloud id gets HTTP 404. Set the cloud id with site.cloud_id; the default is 11111111-2222-3333-4444-555555555555.
Errors
Errors use Jira’s body,{ "errorMessages": [...], "errors": { "<field>": "..." } }, with Jira’s messages: for example Issue does not exist or you do not have permission to see it. with HTTP 404, and You must specify a summary of the issue. under errors.summary with HTTP 400.
JQL search
GET/POST /rest/api/3/search/jqlwithjql,maxResults,nextPageToken,fields, andexpand. Issues come back as{ id }unlessfieldsasks for more (*all,*navigable, names,-name). Pages carrynextPageTokenuntilisLastis true; a token only works with the query that made it.maxResultsdefaults to 50 and is capped at 5000 when onlyidorkeyis asked for, 100 otherwise.POST /rest/api/3/search/approximate-countreturns{ count }.POST /rest/api/3/jql/matchchecks up to 50 issue ids against up to 10 queries.POST /rest/api/3/jql/parse?validation=strict|warn|nonereturns each query’s structure, with errors (strict) or warnings (warn) for unknown fields, values, and functions.GET/POST /rest/api/3/jql/autocompletedataandGET /rest/api/3/jql/autocompletedata/suggestions(fieldName,fieldValue) list fields, operators, functions, and values.GET /rest/api/3/issue/picker(query,currentJQL,currentIssueKey,currentProjectId,showSubTasks,showSubTaskParent) suggests issues by key or summary words in oneCurrent Searchsection.
GET/POST /rest/api/3/search answers HTTP 410 with Jira’s message pointing at /search/jql, as Jira Cloud has since 2025. Like Jira, the search endpoints refuse a query with no restriction (Unbounded JQL queries are not allowed here. Please add a search restriction to your query.).
The parser follows Atlassian’s grammar: AND, OR, NOT (or !), parentheses, quoted strings with escapes, cf[10016] and custom field names, ORDER BY with ASC and DESC. Syntax errors use Jira’s wording and position, such as Error in the JQL Query: Expecting either a value, list or function but got 'EOF'. (line 1, character 11).
Values name things the way Jira does: projects by key, name, or id; users by account id, email, or display name; statuses, types, priorities, and resolutions by name or id. Dates take
yyyy-MM-dd, yyyy/MM/dd, either with HH:mm, or periods like -5d and 4w 2d, all in UTC. Functions: currentUser(), now(), startOfDay(), endOfDay(), startOfWeek(), endOfWeek(), startOfMonth(), endOfMonth(), startOfYear(), endOfYear() (each with an optional increment such as -1w or +2d), standardIssueTypes(), and subTaskIssueTypes().
As in Jira, != and NOT IN never match an empty field: assignee != currentUser() skips unassigned issues. ~ matches every word on a light English stem, "exact phrases", term* and te?t wildcards, and -excluded words. WAS and CHANGED read the changelog and take AFTER, BEFORE, ON, DURING, and BY (CHANGED also takes FROM and TO). Without ORDER BY, results are newest first; empty values sort last ascending and first descending. Unknown fields, values, and functions fail with Jira’s messages, for example Field 'flavor' does not exist or you do not have permission to view it. and The value 'Shipped' does not exist for the field 'status'.
Agile boards and sprints
The Jira Software API lives under/rest/agile/1.0. Issue lists answer there with startAt paging and at the same path under /rest/software/1.0 with nextPageToken paging, as jira.js 6 calls them. Lists take jql (its ORDER BY replaces the default rank order), validateQuery, fields, and expand, and return every navigable field plus the Software fields sprint, closedSprints, flagged, and epic unless fields narrows them.
- Boards:
GET/POST /rest/agile/1.0/board(name,type,projectKeyOrId,filterId),GET /board/filter/{filterId},GET/DELETE /board/{boardId},GET .../configuration(columns per status category, estimation field, rank field),GET .../project - Board issues:
GET .../issue,GET .../backlog,GET .../sprint/{sprintId}/issue,GET .../epic/{epicId}/issue,GET .../epic/none/issue, andGET /rest/software/1.0/board/{boardId}/issue/approximate-countand.../backlog/approximate-count - Sprints:
GET /board/{boardId}/sprint(state=future,active,closed),POST /rest/agile/1.0/sprint,GET/PUT/POST/DELETE /sprint/{sprintId},POST .../issue(move and rank),GET .../issue,POST .../swap - Backlog and rank:
POST /rest/agile/1.0/backlog/issue,POST /backlog/{boardId}/issue,PUT /rest/agile/1.0/issue/rank(rankBeforeIssueorrankAfterIssue; 207 with per-issueentrieswhen some fail) - Issues:
GET /rest/agile/1.0/issue/{issueIdOrKey},GET/PUT .../estimation?boardId= - Epics:
GET/POST /rest/agile/1.0/epic/{epicIdOrKey}(summary,color,done),GET/POST .../issue,GET/POST /rest/agile/1.0/epic/none/issue
The board does not support sprints.
Sprints follow Jira’s lifecycle. New sprints are future. Setting state: "active" needs a start and end date and no other active sprint on the board; setting state: "closed" sets completeDate, and closed sprints cannot change. PUT replaces the sprint, clearing what the body leaves out; POST changes only what it names. Sprint names are at most 30 characters.
Membership lives in each issue’s Sprint field (customfield_10020): closed sprints stay as history and an issue is in at most one active or future sprint, so completing a sprint drops its unfinished issues back to the backlog while sprint in closedSprints() still finds them. The field can also be set on issue create and edit with a sprint id (null removes it). Every move records a Sprint changelog item and sends jira:issue_updated.
Rank (customfield_10019) orders boards, backlogs, and sprints. New issues go to the bottom; PUT /issue/rank and the move endpoints place issues before or after another, changing only the moved issues’ ranks and recording Rank changelog items (Ranked higher or Ranked lower). Search with ORDER BY Rank. Rank cannot be set through issue fields.
Epics are issues of type Epic; an issue belongs to an epic through its parent. Estimation uses the board’s number field, Story Points by default when the site has a field named Story Points or Story point estimate.
JQL gains sprint (by id or name, IS EMPTY) with openSprints(), closedSprints(), and futureSprints(), and ORDER BY Rank.
Saved filters
POST /rest/api/3/filter,GET/PUT/DELETE /rest/api/3/filter/{id},GET /rest/api/3/filter/search(filterName,accountId,id,orderBy),GET /rest/api/3/filter/my(includeFavourites),GET /rest/api/3/filter/favourite,PUT/DELETE /rest/api/3/filter/{id}/favourite
Webhooks
Admin webhooks are registered with/rest/webhooks/1.0/webhook (GET, POST) and /rest/webhooks/1.0/webhook/{id} (GET, PUT, DELETE), or seeded. The body takes name, url, events, filters, excludeBody, enabled, and secret. A JQL query under filters["issue-related-events-section"] limits issue and comment events to the issues it matches, checked after the change; it is validated when the webhook is saved, and currentUser() matches nobody there.
Events sent:
jira:issue_created,jira:issue_updated,jira:issue_deleted, withissue_event_type_nameset toissue_created,issue_updated,issue_assigned,issue_generic,issue_resolved,issue_reopened,issue_commented,issue_comment_edited,issue_comment_deleted, orissue_deleted. Updates carry thechangelog; comment events carry thecomment.comment_created,comment_updated,comment_deletedproject_created,project_updated,project_deletedsprint_created,sprint_updated(with the previous sprint asoldValue),sprint_started,sprint_closed,sprint_deletedboard_created,board_deleted
X-Atlassian-Webhook-Identifier and X-Atlassian-Webhook-Flow: Primary. A webhook with a secret adds X-Hub-Signature: sha256=<hex HMAC-SHA256 of the body>, which jira.js checks with verifyWebhookSignature from jira.js/webhooks. URL variables such as {issue.key}, {issue.id}, {project.key}, {project.id}, {comment.id}, and {modifiedUser.accountId} are filled in. excludeBody: true sends an empty body.
App webhooks
OAuth 2.0 apps register their own webhooks through/rest/api/3/webhook, and only apps may: Basic auth and CLI tokens get HTTP 403 Only Connect and OAuth 2.0 apps can use this operation.
POST /rest/api/3/webhooktakesurlandwebhooks, a list of{ events, jqlFilter, fieldIdsFilter?, issuePropertyKeysFilter? }, and answerswebhookRegistrationResultwithcreatedWebhookIdorerrorsfor each entry.GET /rest/api/3/webhooklists the app’s webhooks withexpirationDate;DELETE /rest/api/3/webhooktakeswebhookIdsand answers HTTP 202;PUT /rest/api/3/webhook/refreshtakeswebhookIdsand returns the newexpirationDate. Ids of other apps’ webhooks are ignored.GET /rest/api/3/webhook/failedlists deliveries that got no 2xx, oldest first, withmaxResults(up to 100) andafter, and anextlink while more remain.
jqlFilter takes issueKey, project, issuetype, status, priority, assignee, and reporter with =, !=, IN, and NOT IN, joined by AND and OR; anything else, and values the site does not have, are refused with a message. fieldIdsFilter goes with jira:issue_updated and sends only updates that change one of those fields. Sprint and version events ignore the JQL. Webhooks expire 30 days after they are registered or refreshed, and expired ones are still listed so they can be refreshed. An app may register five webhooks per user.
Matching events go out once per app and URL, with every matching webhook in matchedWebhookIds and Authorization: Bearer <JWT>, signed HS256 with the app’s client secret. Atlassian does not document the token’s claims; Baseline’s are iss (the emulator’s address), aud (the client id), iat, exp (ten minutes on), and jti, so check the signature and expiry rather than particular claims. Webhooks stop when their user removes the app.
Seed config
account_id. [email protected] (Site Admin) always exists. A transition without from is global. Without transitions, a workflow gets a global transition to each of its statuses. Custom field types are text, number, date, datetime, select, multiselect, user, and labels. A seeded issue’s fields take values the way the REST API does, and its description and comment bodies take plain text or an Atlassian document. A webhook’s jql becomes its issue filter; invalid JQL stops the seed. Custom fields cannot reuse the Sprint (customfield_10020) or Rank (customfield_10019) ids, and seeded fields without an id count up from customfield_10030.
An OAuth app’s scopes are the ones given in the developer console; offline_access needs no entry. Seeding any app turns on client id, callback URL, and scope checks.
Each board gets a filter named Filter for <board> with jql (default project = KEY ORDER BY Rank ASC), or uses a seeded filter named by filter. estimation_field names a number field (or null for none). Sprints list their issues by key; an active or closed sprint needs start_date and end_date, a board has at most one active sprint, and an issue is in at most one open sprint. Seeded issues are ranked in the order they are listed.
Inspector
Open the emulator’s root URL for tabs listing issues (filter by project with?project=ENG), projects and the site’s cloud id, boards with their sprints and saved filters, users, admin webhooks with their recent deliveries, and OAuth apps with their grants, app webhooks, and recent app deliveries.
Limits
- Connect and Forge apps are not modeled. OAuth covers one site, has no PKCE or
prompthandling, and checks one granular scope per operation where Atlassian lists a set. Users cannot revoke an app except by reset or by editing state. - App webhooks are not retried: a delivery without a 2xx goes straight to the failed list. Issue property and version events can be registered but are never sent, because neither feature is modeled.
- Boards have no quick filters, swimlanes, board properties, features, reports, versions, or column limits, and boards only live in projects. Filters have no share or edit permissions or subscriptions. Parallel sprints, the
Flaggedfield, and epic ranking are not modeled. - JQL has no
issueHistory(),membersOf(),linkedIssues(), or other functions beyond those listed above, no entity property clauses, and dates are in UTC rather than the searcher’s time zone. - Attachments, issue links, components, versions, worklogs, and issue properties are not modeled.
- One permission model applies everywhere: every account browses and edits every issue, and admins administer.
- Admin webhooks are not retried.
Conformance
See CONFORMANCE.md for every provider.