# Jira simulator

A local Jira Cloud for integration tests: issues, comments, worklogs, attachments, projects, versions, boards and sprints over REST v2, v3 and the Agile API.

`jira-twin` is a local, stateful simulator of the Jira Cloud REST API for testing code that files, works and searches issues: issues with their comments, worklogs, attachments, links, watchers and change history; projects with their versions and components; and Jira Software's boards, sprints and epics all keep their records between calls. It never calls Jira or Atlassian, sends no email or notification and accepts any email address and API token.

## Quickstart

<!-- BEGIN generated: quickstart (go run ./ingest/cmd/twindocs jira; do not edit) -->

Install the CLI, sign in and boot the simulator with `RYSTIC_URL` pointing at it.

On Pro you sign in with an API key from [your account](https://www.rystic.ai/account/api-keys).

```bash
curl -fsSL https://www.rystic.ai/install.sh | sh
rystic login rys_…   # your API key
export RYSTIC_URL=$(rystic run jira-twin -d --port 0 --scenario jira_sprint_board --json | jq -r .data.url)
curl -s "$RYSTIC_URL/_rystic/version"
# -> {..."product":"jira-twin"...}
```

On Enterprise, the licensed edition signs in with a license file in place of the key: `rystic login ./rystic-license.json` ([licensing](https://www.rystic.ai/docs/licensing)).

<!-- END generated: quickstart -->

### First calls

The default scenario is a software project, `PLAT`, in the middle of a sprint. Read its newest issue, file a new one and read it back:

```bash
curl -s -G "$RYSTIC_URL/rest/api/3/search/jql" -u me@example.com:token --data-urlencode 'jql=project = PLAT ORDER BY created DESC' -d fields=summary,status -d maxResults=1 | jq -c '.issues[0] | {key, summary: .fields.summary, status: .fields.status.name}'
# -> {"key":"PLAT-6","summary":"Checkout returns 500 on expired payment token","status":"Blocked"}
curl -s "$RYSTIC_URL/rest/api/3/issue" -u me@example.com:token -H 'Content-Type: application/json' -d '{"fields":{"project":{"key":"PLAT"},"issuetype":{"name":"Task"},"summary":"Retry failed webhooks"}}' | jq -c '{id, key}'
# -> {"id":"10107","key":"PLAT-7"}
curl -s "$RYSTIC_URL/rest/api/3/issue/PLAT-7?fields=summary,status" -u me@example.com:token | jq -c '{key, summary: .fields.summary, status: .fields.status.name}'
# -> {"key":"PLAT-7","summary":"Retry failed webhooks","status":"To Do"}
```

A search returns only issue ids unless it names `fields`, as Jira's does.

## Connect your client

Point a Jira library at `$RYSTIC_URL` as its site URL with any email address and API token, and the rest of your code stays as it is. Atlassian publishes no official Jira SDK, so these are the most used libraries. Python (`jira`):

```python
import os
from jira import JIRA

jira = JIRA(server=os.environ["RYSTIC_URL"], basic_auth=("me@example.com", "token"))
print(jira.myself()["accountId"])
```

Node (`jira.js`):

```js
import { createCloudClient } from "jira.js";

const jira = createCloudClient({
  host: process.env.RYSTIC_URL,
  auth: { type: "basic", email: "me@example.com", apiToken: "token" },
});
const me = await jira.myself.getCurrentUser();
```

Go (`go-jira`):

```go
import (
	"os"

	jira "github.com/andygrunwald/go-jira"
)

func main() {
	tp := jira.BasicAuthTransport{Username: "me@example.com", Password: "token"}
	client, _ := jira.NewClient(tp.Client(), os.Getenv("RYSTIC_URL"))
	client.User.GetSelf()
}
```

A client that reads its site from a config file, such as `jira-cli`, takes the simulator's URL as the server in that file.

### Retries

The simulator does not rate-limit, so no request is answered 429 and a client's backoff stays idle. Jira has no idempotency keys and the simulator reads none: a POST sent twice makes two records.

<!-- BEGIN generated: known-clients (go run ./ingest/cmd/twindocs jira; do not edit) -->

Each client's quickstart was run against this simulator in each way it connects, with the vendor's own hosts blocked.

| Client | Version | Kind | Works with | Fails with | Checked | Known gap |
|---|---|---|---|---|---|---|
| `atlassian-python-api` (PyPI) | v5.0.5 | sdk | base URL | — | 2026-11-06 | — |
| `github.com/andygrunwald/go-jira` (Go) | v1.17.0 | sdk | base URL | — | 2026-11-06 | — |
| `ankitpokhrel/jira-cli` (GitHub Releases) | v1.7.0 | cli | base URL | — | 2026-11-06 | — |
| `jira.js` (npm) | v6.4.0 | sdk | base URL | — | 2026-11-06 | — |
| `jira` (PyPI) | v3.10.5 | sdk | base URL | — | 2026-11-06 | — |
| `mcp-atlassian` (PyPI) | v0.23.1 | mcp-server | proxy | — | 2026-11-06 | — |

<!-- END generated: known-clients -->

## Auth

The simulator takes your credential where Jira Cloud does, as HTTP Basic with an email address and an API token (`curl -u me@example.com:token`), and checks no value: any non-empty user name works, with any token or none.

A request acts as one default user whatever token it carries, unless it names another user as below. Each scenario names one of its own users as that default, so `GET /rest/api/3/myself` answers that user and the comments, worklogs and attachments you create are authored by it. To act as another seeded user, send `X-Rystic-Actor: <accountId>`, or bind the email address you send to its account id with `POST /_rystic/credentials` and `{"bindings":{"<email>":"<accountId>"}}` ([the control plane](https://www.rystic.ai/docs/control-plane.md#routes-by-plan)); loading a scenario or a reset clears those bindings. With no scenario, the default is the user you seed under the key `member`, and `myself` answers account id `tb-member-account-id` until you seed one. A request whose Basic header has an empty user name, whose header is malformed or whose scheme is Bearer is refused with 401 and `Client must be authenticated to access this resource.` on a route that checks credentials. Not every route checks one: `serverInfo`, issue and project reads and the field and issue type lists answer a request with no credential, where Jira refuses some of them. Send a credential on every request so the code you test is the code that reaches Jira.

## API version

The simulator serves Jira Cloud's REST API versions 2 and 3 under `/rest/api/2` and `/rest/api/3`, and the Jira Software API under `/rest/agile/1.0`; the version is part of the path, so you pin it by the URL you call.

Both REST versions read and write the same records: an issue created through `/rest/api/2` is read back through `/rest/api/3`, and a description is stored as sent and returned in the form it arrived in, plain text or Atlassian Document Format, on either version. A comment follows Jira's own split: version 2 takes a plain-text body and version 3 takes Atlassian Document Format, and a create with the other form answers 400. A path under any other version, such as `/rest/api/latest` or `/rest/api/4`, answers 404 `No route in twin for GET /rest/api/4/issue/PLAT-1`. `GET /rest/api/3/serverInfo` reports a Cloud deployment.

## Coverage

Project, user and workflow administration, custom field setup, dashboards, groups, permission schemes, webhooks and Jira Service Management are out of scope; Limitations lists, by family, the published operations it does not serve, and `GET /_rystic/routes` lists the routes a running simulator serves. On 2026-11-06, the simulator matched Jira Cloud on 835 of the 1035 tests that compare the two (80.7%), and 17 of them gave no verdict.

<!-- BEGIN generated: served-operations (go run ./ingest/cmd/twindocs jira; do not edit) -->

<!-- coverage-claim -->
170 routes, counted from the simulator's route table.

| Resource | Operations |
|---|---|
| Attachment settings (1) | `GET /rest/api/3/attachment/meta` |
| Board (7) | `GET, POST /rest/agile/1.0/board`; `GET, DELETE /rest/agile/1.0/board/{boardId}`; `GET /rest/agile/1.0/board/{boardId}/backlog`; `GET /rest/agile/1.0/board/{boardId}/configuration`; `GET /rest/agile/1.0/board/{boardId}/epic` |
| Bulk operation task (1) | `GET /rest/api/3/bulk/queue/{taskId}` |
| Dynamic webhook (5) | `GET, POST, DELETE /rest/api/3/webhook`; `GET /rest/api/3/webhook/failed`; `PUT /rest/api/3/webhook/refresh` |
| Filter (9) | `POST /rest/api/3/filter`; `GET /rest/api/3/filter/favourite`; `GET /rest/api/3/filter/my`; `GET /rest/api/3/filter/search`; `GET, PUT, DELETE /rest/api/3/filter/{id}`; `PUT, DELETE /rest/api/3/filter/{id}/favourite` |
| Issue (45) | `POST /rest/agile/1.0/backlog/issue`; `GET /rest/agile/1.0/board/{boardId}/issue`; `POST /rest/agile/1.0/epic/none/issue`; `GET, POST /rest/agile/1.0/epic/{epicIdOrKey}`; `GET, POST /rest/agile/1.0/epic/{epicIdOrKey}/issue`; `PUT /rest/agile/1.0/epic/{epicIdOrKey}/rank`; `PUT /rest/agile/1.0/issue/rank`; `GET /rest/agile/1.0/issue/{issueIdOrKey}`; `GET, PUT /rest/agile/1.0/issue/{issueIdOrKey}/estimation`; `GET, POST /rest/agile/1.0/sprint/{sprintId}/issue`; `POST /rest/api/2/issue`; `POST /rest/api/2/issue/bulk`; `GET, PUT, DELETE /rest/api/2/issue/{issueIdOrKey}`; `PUT /rest/api/2/issue/{issueIdOrKey}/assignee`; `GET, POST /rest/api/2/issue/{issueIdOrKey}/transitions`; `GET, POST /rest/api/2/search/jql`; `POST /rest/api/3/bulk/issues/move`; `POST /rest/api/3/issue`; `POST /rest/api/3/issue/bulk`; `GET /rest/api/3/issue/picker`; `GET, PUT, DELETE /rest/api/3/issue/{issueIdOrKey}`; `PUT /rest/api/3/issue/{issueIdOrKey}/assignee`; `GET /rest/api/3/issue/{issueIdOrKey}/editmeta`; `GET, POST, DELETE /rest/api/3/issue/{issueIdOrKey}/remotelink`; `GET, PUT, DELETE /rest/api/3/issue/{issueIdOrKey}/remotelink/{linkId}`; `GET, POST /rest/api/3/issue/{issueIdOrKey}/transitions`; `GET /rest/api/3/label`; `POST /rest/api/3/search/approximate-count`; `GET, POST /rest/api/3/search/jql` |
| Issue attachment (5) | `GET /rest/api/3/attachment/content/{id}`; `GET /rest/api/3/attachment/thumbnail/{id}`; `GET, DELETE /rest/api/3/attachment/{id}`; `POST /rest/api/3/issue/{issueIdOrKey}/attachments` |
| Issue changelog (4) | `POST /rest/api/2/changelog/bulkfetch`; `POST /rest/api/3/changelog/bulkfetch`; `GET /rest/api/3/issue/{issueIdOrKey}/changelog`; `POST /rest/api/3/issue/{issueIdOrKey}/changelog/list` |
| Issue comment (10) | `GET, POST /rest/api/2/issue/{issueIdOrKey}/comment`; `GET, PUT, DELETE /rest/api/2/issue/{issueIdOrKey}/comment/{id}`; `GET, POST /rest/api/3/issue/{issueIdOrKey}/comment`; `GET, PUT, DELETE /rest/api/3/issue/{issueIdOrKey}/comment/{id}` |
| Issue field (4) | `GET /rest/api/2/field`; `GET /rest/api/3/field`; `GET /rest/api/3/field/search`; `POST /rest/api/3/jql/parse` |
| Issue link (5) | `POST /rest/api/2/issueLink`; `DELETE /rest/api/2/issueLink/{linkId}`; `POST /rest/api/3/issueLink`; `GET, DELETE /rest/api/3/issueLink/{linkId}` |
| Issue link type (3) | `GET /rest/api/2/issueLinkType`; `GET /rest/api/3/issueLinkType`; `GET /rest/api/3/issueLinkType/{issueLinkTypeId}` |
| Issue priority (1) | `GET /rest/api/3/priority/search` |
| Issue type (3) | `GET /rest/api/2/issue/createmeta/{projectIdOrKey}/issuetypes/{issueTypeId}`; `GET /rest/api/3/issue/createmeta/{projectIdOrKey}/issuetypes/{issueTypeId}`; `GET /rest/api/3/issuetype` |
| Issue vote (3) | `GET, POST, DELETE /rest/api/3/issue/{issueIdOrKey}/votes` |
| Issue watcher (6) | `GET, POST, DELETE /rest/api/2/issue/{issueIdOrKey}/watchers`; `GET, POST, DELETE /rest/api/3/issue/{issueIdOrKey}/watchers` |
| Issue worklog (10) | `GET, POST /rest/api/2/issue/{issueIdOrKey}/worklog`; `GET, PUT, DELETE /rest/api/2/issue/{issueIdOrKey}/worklog/{id}`; `GET, POST /rest/api/3/issue/{issueIdOrKey}/worklog`; `GET, PUT, DELETE /rest/api/3/issue/{issueIdOrKey}/worklog/{id}` |
| Project (10) | `GET /rest/agile/1.0/board/{boardId}/project`; `GET /rest/api/2/issue/createmeta`; `GET /rest/api/2/issue/createmeta/{projectIdOrKey}/issuetypes`; `GET /rest/api/2/project`; `GET /rest/api/2/project/search`; `GET /rest/api/2/project/{projectIdOrKey}`; `GET /rest/api/3/issue/createmeta/{projectIdOrKey}/issuetypes`; `GET /rest/api/3/issuetype/project`; `GET /rest/api/3/project/search`; `GET /rest/api/3/project/{projectIdOrKey}` |
| Project component (7) | `GET /rest/api/2/project/{projectIdOrKey}/components`; `POST /rest/api/3/component`; `GET, PUT, DELETE /rest/api/3/component/{id}`; `GET /rest/api/3/project/{projectIdOrKey}/component`; `GET /rest/api/3/project/{projectIdOrKey}/components` |
| Project version (10) | `GET /rest/agile/1.0/board/{boardId}/version`; `GET /rest/api/2/project/{projectIdOrKey}/versions`; `POST /rest/api/2/version`; `PUT /rest/api/2/version/{id}`; `GET /rest/api/3/project/{projectIdOrKey}/version`; `GET /rest/api/3/project/{projectIdOrKey}/versions`; `POST /rest/api/3/version`; `GET, PUT /rest/api/3/version/{id}`; `POST /rest/api/3/version/{id}/removeAndSwap` |
| Server information (2) | `GET /rest/api/2/serverInfo`; `GET /rest/api/3/serverInfo` |
| Sprint (7) | `GET /rest/agile/1.0/board/{boardId}/sprint`; `POST /rest/agile/1.0/sprint`; `GET, POST, PUT, DELETE /rest/agile/1.0/sprint/{sprintId}`; `POST /rest/agile/1.0/sprint/{sprintId}/swap` |
| User (8) | `GET /rest/api/2/myself`; `GET /rest/api/2/user`; `GET /rest/api/2/user/assignable/search`; `GET /rest/api/2/user/search`; `GET /rest/api/3/myself`; `GET /rest/api/3/user`; `GET /rest/api/3/user/assignable/search`; `GET /rest/api/3/user/search` |
| Workflow status (4) | `GET /rest/api/2/status`; `GET /rest/api/3/project/{projectIdOrKey}/statuses`; `GET /rest/api/3/status`; `GET /rest/api/3/statuses/search` |

<!-- END generated: served-operations -->

## Scenarios

<!-- BEGIN generated: scenarios (go run ./ingest/cmd/twindocs jira; do not edit) -->

The simulator ships 4 scenarios; the quickstart boots `jira_sprint_board`.

| Scenario | What it is for | What it seeds | Clock |
|---|---|---|---|
| `jira_fresh_site` | A site at its floor | attachment_settings 1, field 9, issue_link_type 4, issue_type 5, priority 5, project 1, server_info 1, status 4, user 1 | 2026-02-02T00:00:00Z |
| `jira_release_train` | A release project whose four versions stand in every lifecycle state from boot — archived, released, overdue-unreleased and planned — with issues pinned to each fix version, a saved filter over the patch scope, a webhook watching that scope and two bulk edit tasks, one finished and one still running. | attachment 1, attachment_settings 1, bulk_task 2, changelog 3, comment 2, component 2, field 9, filter 1, issue 4, issue_link 1, issue_link_type 4, issue_type 5, priority 5, project 1, remote_issue_link 1, server_info 1, status 4, user 2, version 4, vote 2, watcher 5, webhook 1, worklog 1 | 2026-02-02T00:00:00Z |
| `jira_sprint_board` (default) | A lived-in software project mid-sprint | attachment 2, attachment_settings 1, board 1, changelog 4, comment 4, component 2, field 9, filter 2, issue 6, issue_link 2, issue_link_type 4, issue_type 5, priority 5, project 1, remote_issue_link 1, server_info 1, sprint 3, status 4, user 5, version 2, vote 3, watcher 10, webhook 2, worklog 2 | 2026-02-02T00:00:00Z |
| `jira_triage_inbox` | Five unassigned customer reports sitting in To Do on a kanban board backed by one saved filter and one inbound webhook | attachment 3, attachment_settings 1, board 1, changelog 2, comment 3, field 9, filter 1, issue 5, issue_link 1, issue_link_type 4, issue_type 5, priority 5, project 1, remote_issue_link 2, server_info 1, status 4, user 4, vote 3, watcher 6, webhook 1 | 2026-02-02T00:00:00Z |

What each scenario holds:

- **`jira_fresh_site`**: One admin, one empty project and the full catalog of issue types, statuses, priorities, fields and link types — every issue listing returns nothing, so a first write's effect is unmistakable.
- **`jira_sprint_board`**: Six issues across To Do, In Progress, Blocked and Done — epic, story, subtask, bug and task — on a scrum board whose three sprints run from closed through active to future, with two saved filters, comments, worklogs, watchers, votes, attachments, links, change history, both a released and an open version and two registered webhooks, one already lapsed.
- **`jira_triage_inbox`**: Attachments, remote links back to the originating tickets, watchers, three votes on the loudest report, reporter comments and a duplicate link, including one report from a deactivated reporter.

Boot one by name, or load one into a running simulator:

```bash
rystic run jira-twin -d --port 0 --scenario jira_sprint_board
curl -s -X POST "$RYSTIC_URL/_rystic/scenario" -d '{"name":"jira_sprint_board"}'
```

<!-- END generated: scenarios -->

### What the scenarios do not hold

- Each scenario holds one project, with Jira's default issue types, statuses and priorities. Create more projects by seeding them ([Seeding state](#seeding-state)): the simulator serves no project create.
- Without `--scenario`, the simulator boots with no project, issue type, status or priority, so an issue create has nothing to name. Seed those first or boot a scenario.

## Seeding state

Each resource is one register, named `state_<resource>` (`state_issue`, `state_project`, `state_sprint` and so on), holding records keyed by their id; `GET /_rystic/registers` lists every register and its fields. Seed a project the scenario lacks, read it back and file an issue in it:

```bash
curl -s -X POST "$RYSTIC_URL/_rystic/seed" -d '{"state_project": {"10500": {"id": "10500", "key": "OPS", "name": "Operations", "projectTypeKey": "software"}}}'
curl -s "$RYSTIC_URL/rest/api/3/project/OPS" -u me@example.com:token | jq -c '{key, name}'
# -> {"key":"OPS","name":"Operations"}
curl -s "$RYSTIC_URL/rest/api/3/issue" -u me@example.com:token -H 'Content-Type: application/json' -d '{"fields":{"project":{"key":"OPS"},"issuetype":{"name":"Task"},"summary":"Rotate keys"}}' | jq -c '{id, key}'
# -> {"id":"10108","key":"OPS-1"}
```

A record is served as stored, with its `self` link filled in when you leave it out. An issue number counts per project from the seeded issues and an issue id counts across the site.

[Scenarios and seeding](https://www.rystic.ai/docs/concepts/worlds.md) covers seed files, refusals and the seed gate.

## Features

<!-- BEGIN generated: features (go run ./ingest/cmd/twindocs jira; do not edit) -->

Each feature has its own page:

- [Boards, sprints and epics](https://www.rystic.ai/docs/simulators/jira/features/agile.md): Jira Software's Agile API: boards with a backlog, sprints you run from create to complete and epics that group issues.
- [Issue attachments](https://www.rystic.ai/docs/simulators/jira/features/attachments.md): Upload files to an issue as Jira's multipart form does, download them through a signed redirect and delete them.
- [Versions and components](https://www.rystic.ai/docs/simulators/jira/features/releases.md): A project's release versions and components: create, release, archive and swap versions; add, rename and remove components.

<!-- END generated: features -->

## Determinism and reset

On `jira_sprint_board` the next issue is `PLAT-7` with id `10107`, and a comment or an attachment continues its own seeded numbering.

The clock moves one millisecond each time a request stamps or reports a time: creating an issue, comment, worklog, attachment or sprint, editing an issue, comment or worklog, or a `serverInfo` read. `GET /rest/api/3/serverInfo` reports it as `serverTime`.

Nothing moves on its own. Move the clock with `POST /_rystic/clock` and every later `created`, `updated` and sprint `completeDate` reads the new time. [Reset and determinism](https://www.rystic.ai/docs/concepts/reset-and-determinism.md) covers reset, the clock and the RNG.

## Limitations

<!-- BEGIN generated: known-gaps (go run ./ingest/cmd/twindocs jira; do not edit) -->

**Known issues.**

- A version, component or attachment seeded by a scenario can render differently from one created through the API: a seeded version carries release and start date fields a created one lacks, no seeded component uses the project-default assignee, and a seeded attachment's author shows an email address and avatars a created one lacks.
- A JSON write sent with the wrong Content-Type (form-urlencoded) gets a 400 error collection where Jira returns 415 with a problem document. This holds for moving issues to the backlog or into or out of an epic, swapping sprints, setting an estimate and a v2 bulk create or transition. It holds too for editing an issue, creating a component, creating or editing a version, editing a v3 comment and creating or editing a remote link. Removing a version with a swap and editing a v3 component accept the write where Jira returns 415, and registering a webhook returns 403 where Jira returns 415.
- A request with no credential can get a success where Jira refuses it. Listing labels and the bulk changelog fetch return 200 where Jira returns 401. A v3 issue create returns 201 where Jira returns 400, and the create-metadata issue types read returns 200 where Jira returns 404. Deleting a board deletes it where Jira returns 401. Other requests with no credential get a different refusal. A v2 issue create or bulk create and adding a vote return 401 where Jira returns 400, and the webhook routes return 403 where Jira returns 401.
- User search with an invalid parameter (v2 and v3) and the v3 user lookup without an account id return 400 where Jira returns 403. The v3 user lookup with an unknown parameter returns 403 where Jira returns 400, and the v2 user lookup returns 404 where Jira returns 400.
- GET /rest/api/2/field, GET /rest/api/2/project and a v2 issue read's 404 return Content-Type application/json, where Jira returns application/json;charset=UTF-8.
- An issue's watchers list can leave out a watcher's emailAddress, where Jira returns an address.
- Issue edit metadata leaves out the site's custom fields, and an invalid remote-link read answers 200 where Jira answers 403.
- Assigning an unknown assignee answers 400 where Jira answers 404, and deleting a comment without permission could not be reproduced against Jira, so the simulator's answer there is unchecked.
- The simulator does not model Jira's linking-disabled state, where reads of issue link types answer 404.
- GET /rest/api/2/myself omits avatarUrls for an account stored without them, where Jira returns them for every account.
- Listing a project's components through API version 2 returns each component's leadAccountId, a write-only field that Jira leaves out of the response; version 3 returns the component the way Jira does.
- Reading an issue with expand=changelog returns no changelog, and the expanded field names and schema leave out the site's custom fields. Workaround: Read the history from GET /rest/api/3/issue/{key}/changelog.
- A project read doesn't list versions or components created after the simulator started. Workaround: List them through the project's versions and components routes.
- A board created through the API has no columns or estimation, and its issue list ignores the board's filter.
- An issue's sprint fields stay out of date after its sprint starts, closes or is deleted.
- An issue's votes field doesn't follow the votes route: a vote added through the route isn't counted in the issue.
- Creating a filter with invalid JQL succeeds where Jira answers 400, and the saved filter's search URL names a route Jira removed.
- Following a self link from a v2 answer for an attachment, its content, an issue link, an issue type, a status or a priority answers 404. Workaround: Read these records through /rest/api/3.
- The v3 project search includes nextPage on its last page. Workaround: Stop paging when isLast is true.
- Creating, editing or deleting a worklog adds no item to the issue's changelog, where Jira records one.
- After a v2 worklog is logged on an issue with no estimate, timetracking omits the remaining estimate, which Jira reports as 0m. Workaround: Log worklogs through /rest/api/3, or treat an absent remaining estimate as 0.
- A version create that names its project by key, without projectId, is refused with "You must specify a valid project id.", where Jira creates the version. Workaround: Send the project's numeric projectId.
- A component create that carries the read-only isAssigneeTypeValid member is refused, where Jira ignores it and creates the component. Workaround: Leave isAssigneeTypeValid out of the create body.
- A newly created issue has no watcher entry for its reporter: a v3 read shows watchCount 0 and isWatching false, where Jira shows the reporter watching.
- A v2 bulk create accepts an issue with no summary or an unknown parent, which Jira refuses, and can give a new issue the key of a deleted one. Workaround: Create issues one at a time, or through the v3 bulk create route.
- A transition or edit that sets a resolution by a name the site does not have stores it: a v2 transition under Done's id, and a v3 transition or any edit as the bare name. What Jira returns is unconfirmed. Workaround: Set only the resolutions the site lists.

<!-- gaps-claim -->
Beyond the known issues, 1112 of the 1282 operations in the published spec are not served, 209 documented responses have no test yet and 221 behaviors are not modeled. The details follow.

**Not served.** 1112 of the 1282 operations in the published spec are not served.

- Backlog, 1 of 2 served: `POST /rest/agile/1.0/backlog/{boardId}/issue`
- Board, 11 of 26 served: `GET /rest/agile/1.0/board/filter/{filterId}`, `GET /rest/agile/1.0/board/{boardId}/epic/none/issue`, `GET /rest/agile/1.0/board/{boardId}/epic/{epicId}/issue`, `GET /rest/agile/1.0/board/{boardId}/features`, `PUT /rest/agile/1.0/board/{boardId}/features`, `POST /rest/agile/1.0/board/{boardId}/issue`, `GET /rest/agile/1.0/board/{boardId}/project/full`, `GET /rest/agile/1.0/board/{boardId}/properties`, … and 7 more
- Epic, 6 of 7 served: `GET /rest/agile/1.0/epic/none/issue`
- Filters, 9 of 26 served: `POST /rest/api/2/filter`, `GET /rest/api/2/filter/favourite`, `GET /rest/api/2/filter/my`, `GET /rest/api/2/filter/search`, `GET /rest/api/2/filter/{id}`, `PUT /rest/api/2/filter/{id}`, `DELETE /rest/api/2/filter/{id}`, `GET /rest/api/2/filter/{id}/columns`, … and 9 more
- Issue attachments, 6 of 16 served: `GET /rest/api/2/attachment/content/{id}`, `GET /rest/api/2/attachment/meta`, `GET /rest/api/2/attachment/thumbnail/{id}`, `GET /rest/api/2/attachment/{id}`, `DELETE /rest/api/2/attachment/{id}`, `GET /rest/api/2/attachment/{id}/expand/human`, `GET /rest/api/2/attachment/{id}/expand/raw`, `POST /rest/api/2/issue/{issueIdOrKey}/attachments`, … and 2 more
- Issue bulk operations, 2 of 9 served: `POST /rest/api/3/bulk/issues/delete`, `GET /rest/api/3/bulk/issues/fields`, `POST /rest/api/3/bulk/issues/fields`, `GET /rest/api/3/bulk/issues/transition`, `POST /rest/api/3/bulk/issues/transition`, `POST /rest/api/3/bulk/issues/unwatch`, `POST /rest/api/3/bulk/issues/watch`
- Issue comments, 10 of 12 served: `POST /rest/api/2/comment/list`, `POST /rest/api/3/comment/list`
- Issue fields, 3 of 22 served: `POST /rest/api/2/field`, `GET /rest/api/2/field/search`, `GET /rest/api/2/field/search/trashed`, `PUT /rest/api/2/field/{fieldId}`, `GET /rest/api/2/field/{fieldId}/association/project`, `GET /rest/api/2/field/{fieldId}/contexts`, `DELETE /rest/api/2/field/{id}`, `POST /rest/api/2/field/{id}/restore`, … and 11 more
- Issue link types, 3 of 10 served: `POST /rest/api/2/issueLinkType`, `GET /rest/api/2/issueLinkType/{issueLinkTypeId}`, `PUT /rest/api/2/issueLinkType/{issueLinkTypeId}`, `DELETE /rest/api/2/issueLinkType/{issueLinkTypeId}`, `POST /rest/api/3/issueLinkType`, `PUT /rest/api/3/issueLinkType/{issueLinkTypeId}`, `DELETE /rest/api/3/issueLinkType/{issueLinkTypeId}`
- Issue links, 5 of 6 served: `GET /rest/api/2/issueLink/{linkId}`
- Issue priorities, 1 of 16 served: `GET /rest/api/2/priority`, `POST /rest/api/2/priority`, `PUT /rest/api/2/priority/default`, `PUT /rest/api/2/priority/move`, `GET /rest/api/2/priority/search`, `GET /rest/api/2/priority/{id}`, `PUT /rest/api/2/priority/{id}`, `DELETE /rest/api/2/priority/{id}`, … and 7 more
- Issue remote links, 6 of 12 served: `GET /rest/api/2/issue/{issueIdOrKey}/remotelink`, `POST /rest/api/2/issue/{issueIdOrKey}/remotelink`, `DELETE /rest/api/2/issue/{issueIdOrKey}/remotelink`, `GET /rest/api/2/issue/{issueIdOrKey}/remotelink/{linkId}`, `PUT /rest/api/2/issue/{issueIdOrKey}/remotelink/{linkId}`, `DELETE /rest/api/2/issue/{issueIdOrKey}/remotelink/{linkId}`
- Issue search, 6 of 14 served: `GET /rest/api/2/issue/picker`, `POST /rest/api/2/jql/match`, `GET /rest/api/2/search`, `POST /rest/api/2/search`, `POST /rest/api/2/search/approximate-count`, `POST /rest/api/3/jql/match`, `GET /rest/api/3/search`, `POST /rest/api/3/search`
- Issue types, 2 of 16 served: `GET /rest/api/2/issuetype`, `POST /rest/api/2/issuetype`, `GET /rest/api/2/issuetype/project`, `GET /rest/api/2/issuetype/{id}`, `PUT /rest/api/2/issuetype/{id}`, `DELETE /rest/api/2/issuetype/{id}`, `GET /rest/api/2/issuetype/{id}/alternatives`, `POST /rest/api/2/issuetype/{id}/avatar2`, … and 6 more
- Issue votes, 3 of 6 served: `GET /rest/api/2/issue/{issueIdOrKey}/votes`, `POST /rest/api/2/issue/{issueIdOrKey}/votes`, `DELETE /rest/api/2/issue/{issueIdOrKey}/votes`
- Issue watchers, 6 of 8 served: `POST /rest/api/2/issue/watching`, `POST /rest/api/3/issue/watching`
- Issue worklogs, 10 of 20 served: `DELETE /rest/api/2/issue/{issueIdOrKey}/worklog`, `POST /rest/api/2/issue/{issueIdOrKey}/worklog/move`, `GET /rest/api/2/worklog/deleted`, `POST /rest/api/2/worklog/list`, `GET /rest/api/2/worklog/updated`, `DELETE /rest/api/3/issue/{issueIdOrKey}/worklog`, `POST /rest/api/3/issue/{issueIdOrKey}/worklog/move`, `GET /rest/api/3/worklog/deleted`, … and 2 more
- Issues, 26 of 48 served: `GET /rest/api/2/events`, `POST /rest/api/2/issue/archive`, `PUT /rest/api/2/issue/archive`, `POST /rest/api/2/issue/bulkfetch`, `GET /rest/api/2/issue/limit/adf/report`, `GET /rest/api/2/issue/limit/report`, `PUT /rest/api/2/issue/unarchive`, `GET /rest/api/2/issue/{issueIdOrKey}/changelog`, … and 14 more
- JQL, 1 of 14 served: `GET /rest/api/2/invalid-jqls-access-log`, `GET /rest/api/2/jql/autocompletedata`, `POST /rest/api/2/jql/autocompletedata`, `GET /rest/api/2/jql/autocompletedata/suggestions`, `POST /rest/api/2/jql/parse`, `POST /rest/api/2/jql/pdcleaner`, `POST /rest/api/2/jql/sanitize`, `GET /rest/api/3/invalid-jqls-access-log`, … and 5 more
- Labels, 1 of 2 served: `GET /rest/api/2/label`
- Myself, 2 of 12 served: `GET /rest/api/2/mypreferences`, `PUT /rest/api/2/mypreferences`, `DELETE /rest/api/2/mypreferences`, `GET /rest/api/2/mypreferences/locale`, `PUT /rest/api/2/mypreferences/locale`, `GET /rest/api/3/mypreferences`, `PUT /rest/api/3/mypreferences`, `DELETE /rest/api/3/mypreferences`, … and 2 more
- Project components, 7 of 16 served: `GET /rest/api/2/component`, `POST /rest/api/2/component`, `GET /rest/api/2/component/{id}`, `PUT /rest/api/2/component/{id}`, `DELETE /rest/api/2/component/{id}`, `GET /rest/api/2/component/{id}/relatedIssueCounts`, `GET /rest/api/2/project/{projectIdOrKey}/component`, `GET /rest/api/3/component`, … and 1 more
- Project versions, 9 of 30 served: `GET /rest/api/2/project/{projectIdOrKey}/version`, `GET /rest/api/2/version/{id}`, `DELETE /rest/api/2/version/{id}`, `PUT /rest/api/2/version/{id}/mergeto/{moveIssuesTo}`, `POST /rest/api/2/version/{id}/move`, `GET /rest/api/2/version/{id}/relatedIssueCounts`, `GET /rest/api/2/version/{id}/relatedwork`, `POST /rest/api/2/version/{id}/relatedwork`, … and 13 more
- Projects, 6 of 26 served: `POST /rest/api/2/project`, `GET /rest/api/2/project/recent`, `PUT /rest/api/2/project/{projectIdOrKey}`, `DELETE /rest/api/2/project/{projectIdOrKey}`, `POST /rest/api/2/project/{projectIdOrKey}/archive`, `POST /rest/api/2/project/{projectIdOrKey}/delete`, `POST /rest/api/2/project/{projectIdOrKey}/restore`, `GET /rest/api/2/project/{projectIdOrKey}/statuses`, … and 12 more
- Sprint, 8 of 12 served: `GET /rest/agile/1.0/sprint/{sprintId}/properties`, `GET /rest/agile/1.0/sprint/{sprintId}/properties/{propertyKey}`, `PUT /rest/agile/1.0/sprint/{sprintId}/properties/{propertyKey}`, `DELETE /rest/agile/1.0/sprint/{sprintId}/properties/{propertyKey}`
- Status, 1 of 18 served: `GET /rest/api/2/statuses`, `POST /rest/api/2/statuses`, `PUT /rest/api/2/statuses`, `DELETE /rest/api/2/statuses`, `GET /rest/api/2/statuses/byNames`, `GET /rest/api/2/statuses/search`, `GET /rest/api/2/statuses/{statusId}/project/{projectId}/issueTypeUsages`, `GET /rest/api/2/statuses/{statusId}/projectUsages`, … and 9 more
- User search, 4 of 16 served: `GET /rest/api/2/user/assignable/multiProjectSearch`, `GET /rest/api/2/user/permission/search`, `GET /rest/api/2/user/picker`, `GET /rest/api/2/user/search/query`, `GET /rest/api/2/user/search/query/key`, `GET /rest/api/2/user/viewissue/search`, `GET /rest/api/3/user/assignable/multiProjectSearch`, `GET /rest/api/3/user/permission/search`, … and 4 more
- Users, 2 of 26 served: `POST /rest/api/2/user`, `DELETE /rest/api/2/user`, `GET /rest/api/2/user/bulk`, `GET /rest/api/2/user/bulk/migration`, `GET /rest/api/2/user/columns`, `PUT /rest/api/2/user/columns`, `DELETE /rest/api/2/user/columns`, `GET /rest/api/2/user/email`, … and 16 more
- Webhooks, 5 of 10 served: `GET /rest/api/2/webhook`, `POST /rest/api/2/webhook`, `DELETE /rest/api/2/webhook`, `GET /rest/api/2/webhook/failed`, `PUT /rest/api/2/webhook/refresh`
- Workflow statuses, 2 of 4 served: `GET /rest/api/2/status/{idOrName}`, `GET /rest/api/3/status/{idOrName}`
- Not served at all: Agents (2), Announcement banner (4), App data policies (4), App migration (3), App properties (8), Application roles (4), Audit records (2), Avatars (14), Classification levels (2), Dashboards (34), Dynamic modules (3), Field schemes (30), Filter sharing (12), Group and user picker (2), Groups (16), Issue comment properties (8), Issue custom field associations (4), Issue custom field configuration (apps) (6), Issue custom field contexts (30), Issue custom field options (14), … and 54 more

**Not tested.** 1035 tests compare the simulator with the real service. 209 documented responses have no test yet, and 160 of them need a credential our test account doesn't hold (401 ×100, 403 ×60). The other 49:

| Operation | Status | Why |
|---|---:|---|
| `DELETE /rest/api/2/issue/{issueIdOrKey}/comment/{id}` | 405 | no known request reaches it |
| `DELETE /rest/api/3/issue/{issueIdOrKey}/comment/{id}` | 405 | no known request reaches it |
| `DELETE /rest/api/3/issue/{issueIdOrKey}/remotelink` | 204 | no known request reaches it |
| `DELETE /rest/api/3/issue/{issueIdOrKey}/remotelink` | 415 | no known request reaches it |
| `GET /rest/api/2/issue/createmeta/{projectIdOrKey}/issuetypes` | 200 | nothing the simulator serves creates the record it needs |
| `GET /rest/api/2/issue/createmeta/{projectIdOrKey}/issuetypes/{issueTypeId}` | 200 | nothing the simulator serves creates the record it needs |
| `GET /rest/api/2/project/{projectIdOrKey}` | 200 | nothing the simulator serves creates the record it needs |
| `GET /rest/api/2/project/{projectIdOrKey}/components` | 200 | nothing the simulator serves creates the record it needs |
| `GET /rest/api/2/project/{projectIdOrKey}/versions` | 200 | nothing the simulator serves creates the record it needs |
| `GET /rest/api/2/user` | 404 | no known request reaches it |
| `GET /rest/api/2/user/assignable/search` | 404 | no known request reaches it |
| `GET /rest/api/2/user/assignable/search` | 429 | no request a client sends provokes it |
| `GET /rest/api/2/user/search` | 429 | no request a client sends provokes it |
| `GET /rest/api/3/attachment/content/{id}` | 303 | no known request reaches it |
| `GET /rest/api/3/attachment/thumbnail/{id}` | 303 | no known request reaches it |
| `GET /rest/api/3/bulk/queue/{taskId}` | 400 | nothing the simulator serves creates the record it needs |
| `GET /rest/api/3/issue/{issueIdOrKey}/remotelink` | 413 | no known request reaches it |
| `GET /rest/api/3/issueLinkType/{issueLinkTypeId}` | 200 | nothing the simulator serves creates the record it needs |
| `GET /rest/api/3/issuetype/project` | 200 | no known request reaches it |
| `GET /rest/api/3/issuetype/project` | 404 | no known request reaches it |
| … and 29 more | | |

**Not modeled.** Behavior the simulator does not model, by operation:

- Documented statuses not reproduced: 401 (3 operations).
- The captured 204 carried Content-Type: text/html;charset=UTF-8 on a bodiless answer; no header policy is declared because one observation does not establish which headers a verdict should hold. (`DELETE /rest/agile/1.0/board/{boardId}`)
- The permission arm likewise needs an account without Jira/board admin rights, which the run's single administrator credential cannot present. (`DELETE /rest/agile/1.0/board/{boardId}`)
- The bodies of the 401, 403 and 404 answers are undocumented and unmeasured; only their statuses are claimed. The two measured 204s carry Content-Type text/html;charset=UTF-8 with an empty body, so no response headers are declared for comparison. (`DELETE /rest/agile/1.0/sprint/{sprintId}`)
- The description states that deleting a sprint moves all open issues in the sprint to the backlog. (`DELETE /rest/agile/1.0/sprint/{sprintId}`)
- Whether deleting a comment restricted by visibility to a group or role draws 404 or 400 for a caller outside that group is undocumented here. (`DELETE /rest/api/2/issue/{issueIdOrKey}/comment/{id}`)
- The 403 and 404 response bodies are undocumented (no content declared in the spec); only the statuses are modelled. (`DELETE /rest/api/3/attachment/{id}`)
- The 400 conflates two triggers (filter not found, filter used by a board). Without a measured body they cannot be split into discriminated branches. (`DELETE /rest/api/3/filter/{id}`)
- Whether an anonymous request (the spec lists an empty security alternative) answers 400 for a private filter or a credential refusal is undocumented. (`DELETE /rest/api/3/filter/{id}/favourite`)
- The spec's third security alternative is anonymous ({}), and the description states the operation can be accessed anonymously; what an anonymous caller is answered on a visible issue is not documented here. (`DELETE /rest/api/3/issue/{issueIdOrKey}`)
- Which answer a credential-less request draws on this route is unmeasured: the spec documents 401 for missing or incorrect credentials and 405 for an anonymous call, and the security list admits the empty alternative. (`DELETE /rest/api/3/issue/{issueIdOrKey}/comment/{id}`)
- Issue linking must be active on the site; the answer when it is disabled is not documented. (`DELETE /rest/api/3/issue/{issueIdOrKey}/remotelink`)
- Deletion by globalId (DELETE /rest/api/3/issue/{issueIdOrKey}/remotelink?globalId=) is a separate operation and is not modeled here. (`DELETE /rest/api/3/issue/{issueIdOrKey}/remotelink/{linkId}`)
- The deprecated username query parameter's behaviour (ignored, or refused) is undocumented. (`DELETE /rest/api/3/issue/{issueIdOrKey}/watchers`)
- The operation requires the site-wide 'Allow users to watch issues' option to be ON; no in-scope operation reads or sets that option, so the refusal when it is OFF is not modeled. (`DELETE /rest/api/3/issue/{issueIdOrKey}/watchers`)
- The spec lists a bare '200 response' beside the 204 with no body schema and no statement of what selects it; no branch is declared for it because neither its request selector nor its body is documented. (`DELETE /rest/api/3/issueLink/{linkId}`)
- Whether an empty webhookIds array is treated as a missing list (400) or as an accepted no-op (202) is not documented. (`DELETE /rest/api/3/webhook`)
- Whether the service refuses unknown body members beside webhookIds is not documented; the request shape is left open. (`DELETE /rest/api/3/webhook`)
- The expand parameter (admins, permissions) and the resulting response members are not modeled as separate branches; no capture shows the expanded shape. (`GET /rest/agile/1.0/board`)
- The spec's `type` query parameter schema is an empty object with additionalProperties false; the wire form (a comma-separated string) is taken from its description, not from a capture. (`GET /rest/agile/1.0/board`)
- The selection semantics of the fields parameter and the inert expand parameter are unresolved beyond the prose. (`GET /rest/agile/1.0/board/{boardId}/backlog`)
- … and 200 more

<!-- END generated: known-gaps -->

**By design.**

- **Few permission checks.** A request can act as any seeded user ([Auth](#auth)), but few routes refuse a user for lack of permission. Reading a saved filter by id is refused to a user it is not shared with, though a filter search and favouriting or unfavouriting by id reach every filter, and only a filter's owner may delete it; editing a filter you do not own answers 501. The webhook routes and the issue-edit override flags are open only to a user whose `accountType` is `app`, except the failed-webhooks list, edit metadata with an override flag and the override flag on adding a v2 worklog or updating any worklog, which refuse every user. Removing another user as a watcher through `/rest/api/2/` is refused when your own user record denies `MANAGE_WATCHER_LIST`. Any other test of what a second user may not do has nothing to run against.
- **Search is a subset of JQL.** Each of the four search routes, `GET` and `POST` on `/rest/api/2/search/jql` and on `/rest/api/3/search/jql`, understands its own fixed set of fields, operators and sort fields. A clause outside it answers 400 with a short message or matches nothing and answers an empty page, where Jira refuses an unknown field. `GET /rest/api/3/search/jql` takes `key`, `project`, `issuetype`, `status`, `statusCategory`, `assignee`, `reporter` and `summary` and has the most sort fields (`created`, `updated`, `key` and `Rank`); a clause on `labels`, `priority`, `resolution`, a date or `text ~` matches nothing there, while `GET /rest/api/2/search/jql` also takes `labels` and `id`.

Release notes: [Changelog](https://www.rystic.ai/docs/simulators/jira/changelog.md)

---

Source: https://www.rystic.ai/docs/simulators/jira · Markdown: https://www.rystic.ai/docs/simulators/jira.md
