REST API
Scripts, integrations and other tools outside Jira can read and change TreeCraft hierarchies through the app's REST API. This page is for the developers who build them. It covers turning the API on, getting a token, the scopes, every endpoint, and the rules every call follows.
On this page
What the API is for
The API lets a program do what a person does on the app's pages, as that person. For example, it can:
- list the hierarchies someone can open, and read their rows;
- keep a hierarchy in step with another tool, by adding, moving and removing rows;
- create, rename, archive or delete hierarchies;
- read a hierarchy's rules and the saved views.
Every call acts as the person who authorized it, with the same checks as the app's own pages:
- their access level on the hierarchy: View, Edit, Automate or Control;
- the site's App access and Automation permissions;
- their Jira permissions for every work item.
A row never shows a work item the caller can't see in Jira. The API gives work item ids, not work item fields: read the work items themselves from Jira's own REST API with the same token, so Jira's permissions apply there too.
This is version 1, in beta. The API is built on Forge app REST APIs, which Atlassian offers as a Preview. Atlassian may change them at shorter notice than usual, so the API stays in beta until they are generally available.
It runs on Atlassian
Calls go through Atlassian's API gateway to the app, which runs on Atlassian's cloud like the rest of TreeCraft. The app sends no data outside Atlassian. What the API keeps, such as the record of a retried call, stays in the app's storage on Atlassian with the rest of the site's data. With the API on, TreeCraft is still built to Atlassian's Runs on Atlassian requirements.
The gateway checks the token and its scopes before the call reaches the app. A token without the scope a route needs never reaches TreeCraft.
Turn the API on
Two switches must be on for a call to reach the app. Both belong to the site, and turning off either one stops every call at once.
The first version of TreeCraft with the API is a major update of the app. A site admin approves that update once before the API is available.
Atlassian's switch
Atlassian's switch is off by default. A site admin or an organization admin turns it on:
- Go to Atlassian Administration and select your organization.
- Under Apps, go to Sites and select the site where TreeCraft is installed.
- Select Connected apps, find TreeCraft, and select View app details.
- On the Details tab, turn on App REST APIs.
TreeCraft's switch
TreeCraft's own switch is on by default. Jira admins and app admins find it in the app's settings, in Jira administration under Apps: select REST API in the Access group. The page has:
- Allow calls to this app's REST API on this site, the switch. A change reaches every call within a minute. While it is off, every call answers
403 API_TURNED_OFF. - Base URL, both forms, filled in for your site, each with a copy button.
- Identifiers: the app id, the environment id and the cloud id.
- Scopes: the three scopes and what each one allows.
Get an access token
The API takes an OAuth 2.0 (3LO) access token. A token belongs to one person, and every call made with it acts as that person. Only members of the site where TreeCraft is installed can set up the integration.
- Open the Atlassian Developer Console. Select Create, then OAuth 2.0 integration, and give it a name.
- Under Permissions, select Add Marketplace or custom app. Pick your site, then TreeCraft, then the scopes your integration needs. Pick only those.
- Also add the Jira scope
read:forge-app:jira. Every call to an app's REST API needs it. - Under Authorization, set a callback URL. The console then shows an authorization URL.
- Check that the URL's
scopeparameter lists the TreeCraft scopes andread:forge-app:jira. Addoffline_accessto get a refresh token. Add any missing scope by hand, separated by%20. - Open the URL, pick the site and select Accept. Atlassian sends you to the callback URL with a
code. - Exchange the code for a token, as shown below.
An authorization URL that asks for the read scope looks like this:
https://auth.atlassian.com/authorize?audience=api.atlassian.com&client_id=YOUR_CLIENT_ID&scope=read%3Ahierarchy%3Acustom%20read%3Aforge-app%3Ajira%20offline_access&redirect_uri=YOUR_CALLBACK&state=RANDOM&response_type=code&prompt=consent
Exchange the code for a token like this:
curl -s -X POST https://auth.atlassian.com/oauth/token \
-H 'Content-Type: application/json' \
-d '{"grant_type":"authorization_code","client_id":"YOUR_CLIENT_ID","client_secret":"YOUR_CLIENT_SECRET","code":"THE_CODE","redirect_uri":"YOUR_CALLBACK"}'
The answer holds access_token, which is short-lived, and refresh_token if you asked for offline_access. To get a new access token, send the refresh token in place of the code:
curl -s -X POST https://auth.atlassian.com/oauth/token \
-H 'Content-Type: application/json' \
-d '{"grant_type":"refresh_token","client_id":"YOUR_CLIENT_ID","client_secret":"YOUR_CLIENT_SECRET","refresh_token":"THE_REFRESH_TOKEN"}'
Send the token on every call, in the Authorization header:
curl -s "$BASE/v1/hierarchies" -H "Authorization: Bearer $TOKEN" -H 'Accept: application/json'
If the token lacks the scope a route needs, Atlassian's gateway refuses the call with 401 Unauthorized; scope does not match.
Base URL
Two forms of base URL reach the same API:
https://api.atlassian.com/svc/jira/<cloud-id>/apps/<app-id>_<environment-id>
https://<your-site>.atlassian.net/gateway/api/svc/jira/apps/<app-id>_<environment-id>
The parts in angle brackets are placeholders. Your own values are on TreeCraft's REST API page, which shows both URLs filled in, ready to copy.
| Part | Where to find it |
|---|---|
cloud-id |
The Identifiers on TreeCraft's REST API page. You can also open https://<your-site>.atlassian.net/_edge/tenant_info, or call GET https://api.atlassian.com/oauth/token/accessible-resources with a token, which lists the sites it may reach with their ids. |
app-id |
The Identifiers on TreeCraft's REST API page. |
environment-id |
The Identifiers on TreeCraft's REST API page. |
your-site |
Your site's address, as in your-site.atlassian.net. |
Once Atlassian's switch is on, Atlassian Administration also shows the base URL under App REST APIs.
The examples on this page use two shell variables:
BASE='https://api.atlassian.com/svc/jira/<cloud-id>/apps/<app-id>_<environment-id>'
TOKEN='<access-token>'
Scopes
TreeCraft has three scopes. Each endpoint needs exactly one of them.
| Scope | Lets the integration |
|---|---|
read:hierarchy:custom |
Read the hierarchies the person can open: their details, rows and rules, and the views the person can use. Changes nothing. |
write:hierarchy:custom |
Add work items and folders, and move or remove rows, in hierarchies the person can edit. Changes no Jira work item. |
manage:hierarchy:custom |
Create hierarchies, and rename, label, archive, restore or delete those the person controls. |
A scope never gives more than the person has. An integration that changes rows usually asks for read:hierarchy:custom too, so it can read what it changes. Every call also needs the Jira scope read:forge-app:jira.
Endpoints
Every path starts with the version, /v1. {id} is a hierarchy id and {viewId} a view id. The level is the person's access level on the hierarchy. Every call also needs App access to let the person use the app.
| Method | Path | Scope | TreeCraft permission |
|---|---|---|---|
GET |
/v1/hierarchies |
read:hierarchy:custom |
Lists only hierarchies where the person has View or more |
POST |
/v1/hierarchies |
manage:hierarchy:custom |
App access lets the person create hierarchies |
GET |
/v1/hierarchies/{id} |
read:hierarchy:custom |
View |
PATCH |
/v1/hierarchies/{id} |
manage:hierarchy:custom |
Control |
DELETE |
/v1/hierarchies/{id} |
manage:hierarchy:custom |
Control |
GET |
/v1/hierarchies/{id}/rows |
read:hierarchy:custom |
View |
POST |
/v1/hierarchies/{id}/rows/batch |
write:hierarchy:custom |
Edit, and Automate to move or remove rules |
GET |
/v1/hierarchies/{id}/rules |
read:hierarchy:custom |
Automate |
GET |
/v1/views |
read:hierarchy:custom |
None on a hierarchy: the person's own views and the shared views they may use |
GET |
/v1/views/{viewId} |
read:hierarchy:custom |
None on a hierarchy: one of the person's own views, or a shared view they may use |
A hierarchy the person can't open answers 404, the same as one that doesn't exist. A level that is too low answers 403.
List hierarchies
GET /v1/hierarchies · Scope: read:hierarchy:custom · Needs: View on each hierarchy listed
Lists the hierarchies the person can open, ordered by id, one page at a time.
| Parameter | In | Description |
|---|---|---|
limit |
query | 1 to 500. Default 100. |
cursor |
query | The nextCursor of the previous page. |
includeArchived |
query | true to include archived hierarchies. Default false. |
label |
query | Only hierarchies with this label, in any case. |
curl -s "$BASE/v1/hierarchies?limit=2" -H "Authorization: Bearer $TOKEN"
{
"hierarchies": [
{ "id": 12, "name": "Release plan", "description": "", "ownerAccountId": "5b10ac8d82e05b22cc7d4ef5", "access": "control",
"archived": false, "version": 48, "labels": ["Planning"], "favorite": true, "searchable": false },
{ "id": 15, "name": "Support backlog", "description": "Open requests by team", "ownerAccountId": "5b10ac8d82e05b22cc7d4ef5",
"access": "view", "archived": false, "version": 7, "labels": [], "favorite": false, "searchable": true }
],
"nextCursor": "aDoxNQ"
}
Each hierarchy has these fields:
| Field | Meaning |
|---|---|
id |
The hierarchy's id. |
name, description |
Its name and description. |
ownerAccountId |
The owner's Atlassian account id. |
access |
The person's own level on it: view, edit, automate or control. |
archived |
true if it is archived. |
version |
Goes up with every change to its rows. |
labels |
Its labels. |
favorite |
true if it is one of the person's favorites. |
searchable |
true if its work items can be found with Jira search. |
Create a hierarchy
POST /v1/hierarchies · Scope: manage:hierarchy:custom · Needs: App access lets the person create hierarchies
Creates a hierarchy. The person becomes its owner, with Control, and it is one of their favorites. Like any new hierarchy, it starts private.
| Field | In | Description |
|---|---|---|
name |
body | Required. Up to 255 characters. |
description |
body | Up to 20,000 characters. |
Idempotency-Key |
header | Optional. See Idempotency. |
curl -s -X POST "$BASE/v1/hierarchies" -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -H 'Idempotency-Key: create-release-plan-1' \
-d '{"name":"Release plan","description":"Everything planned for the next release"}'
Answers 201 with the new hierarchy:
{ "id": 21, "name": "Release plan", "description": "Everything planned for the next release", "ownerAccountId": "5b10ac8d82e05b22cc7d4ef5",
"access": "control", "archived": false, "version": 0, "labels": [], "favorite": true, "searchable": false }
Get a hierarchy
GET /v1/hierarchies/{id} · Scope: read:hierarchy:custom · Needs: View
Reads one hierarchy's details. This call is cheap, so it is a good way to check version before reading the rows again.
| Parameter | In | Description |
|---|---|---|
id |
path | The hierarchy's id. |
curl -s "$BASE/v1/hierarchies/12" -H "Authorization: Bearer $TOKEN"
Answers 200 with the hierarchy, with the same fields as in the list:
{ "id": 12, "name": "Release plan", "description": "", "ownerAccountId": "5b10ac8d82e05b22cc7d4ef5", "access": "control",
"archived": false, "version": 48, "labels": ["Planning"], "favorite": true, "searchable": false }
Change a hierarchy
PATCH /v1/hierarchies/{id} · Scope: manage:hierarchy:custom · Needs: Control
Renames, describes, labels, archives or restores a hierarchy. Give at least one field.
The fields are applied one after another, as in the app. So a 400 for a bad name or description can come after the labels of the same request were already changed. Where that matters, send one field at a time.
| Field | In | Description |
|---|---|---|
id |
path | The hierarchy's id. |
name |
body | Up to 255 characters. |
description |
body | Up to 20,000 characters. |
labels |
body | Up to 10 labels, each up to 40 characters. Replaces the current labels. |
archived |
body | true archives the hierarchy, false restores it. |
Idempotency-Key |
header | Optional. See Idempotency. |
curl -s -X PATCH "$BASE/v1/hierarchies/12" -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"name":"Release plan 2027","labels":["Planning","2027"]}'
Answers 200 with the hierarchy as it is now:
{ "id": 12, "name": "Release plan 2027", "description": "", "ownerAccountId": "5b10ac8d82e05b22cc7d4ef5", "access": "control",
"archived": false, "version": 48, "labels": ["Planning", "2027"], "favorite": true, "searchable": false }
Delete a hierarchy
DELETE /v1/hierarchies/{id} · Scope: manage:hierarchy:custom · Needs: Control
Deletes the hierarchy with its rows, rules and write-backs. The Jira work items are not changed. This can't be undone.
| Parameter | In | Description |
|---|---|---|
id |
path | The hierarchy's id. |
Idempotency-Key |
header | Optional. See Idempotency. |
curl -s -X DELETE "$BASE/v1/hierarchies/12" -H "Authorization: Bearer $TOKEN"
Answers 204 with no body.
Read rows
GET /v1/hierarchies/{id}/rows · Scope: read:hierarchy:custom · Needs: View
Reads the tree, or the part below one row. Rows come in the order the grid shows them with nothing sorted: each row, then the rows below it.
| Parameter | In | Description |
|---|---|---|
id |
path | The hierarchy's id. |
under |
query | A row id. Only the rows below that row. |
depth |
query | Levels below the start, 1 to 64. Default 64. 1 gives only the top level, or only the children of under. |
limit |
query | 1 to 5,000. Default 1,000. |
cursor |
query | The nextCursor of the previous page. |
curl -s "$BASE/v1/hierarchies/12/rows?depth=2&limit=5000" -H "Authorization: Bearer $TOKEN"
{
"hierarchyId": 12,
"version": 48,
"building": false,
"limitsReached": [],
"rows": [
{ "id": 4711, "parentId": null, "level": 1, "kind": "issue", "byRule": false, "hasChildren": true, "issueId": "10001" },
{ "id": 4712, "parentId": 4711, "level": 2, "kind": "folder", "byRule": false, "hasChildren": false, "folderId": "7", "name": "Next quarter" },
{ "id": 4713, "parentId": 4711, "level": 2, "kind": "restricted", "byRule": false, "hasChildren": true },
{ "id": 4800, "parentId": null, "level": 1, "kind": "group", "byRule": true, "hasChildren": true, "groupId": "status:3", "label": "In Progress" }
],
"nextCursor": null
}
Every row has these fields:
| Field | Meaning |
|---|---|
id |
The row's id. |
parentId |
The row above it, or null at the top level. |
level |
1 is directly below the start. |
kind |
What the row shows (see the next table). |
byRule |
true if a rule placed the row. The rule decides where it goes. |
hasChildren |
true if rows sit below it. Read them later with under. |
Then, by kind:
kind |
Fields |
|---|---|
issue |
issueId. Read the work item itself from Jira's REST API with your own token. |
folder |
folderId, name. |
planningItem |
planningItemId, name, estimateHours, ownerAccountId. |
memo |
memoId, text (in wiki markup), revision. |
group |
groupId, label. |
rule |
ruleId. Only people with Automate see rule rows. |
restricted |
Nothing else. A work item the person can't see, placed by hand above rows they can see. See What the API never shows. |
The answer also has:
version: the hierarchy's version when the page was read.building:truewhile the rules' output is being rebuilt. Read again shortly for the newest rows.limitsReached: the rule limits that cut the rules' output, as the app's notice names them.
Each page reads the tree again, so ask for large pages. A hierarchy of 20,000 rows takes 4 calls of 5,000.
Change rows
POST /v1/hierarchies/{id}/rows/batch · Scope: write:hierarchy:custom · Needs: Edit, and Automate to move or remove rules
Adds, moves and removes rows in one batch. The app checks the whole batch first, then writes it as one change, or not at all. It is the same change, with the same checks, as editing in the app.
| Field | In | Description |
|---|---|---|
id |
path | The hierarchy's id. |
changes |
body | Required. 1 to 200 changes, applied in order. |
Idempotency-Key |
header | Optional. See Idempotency. |
Each change has an action:
action |
Fields |
|---|---|
add |
items: each one is {"issueId": "10001"}, {"folderId": "7"} for an existing folder, or {"folderName": "New folder"} for a new folder the app makes for you. parentId: the row to add under (null or left out: the top level). afterId: the sibling to add after (null or left out: first). |
move |
rowIds, parentId, afterId. |
remove |
rowIds. keepChildren: true moves the rows below each removed row up to its place. By default they are removed with it. |
curl -s -X POST "$BASE/v1/hierarchies/12/rows/batch" -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -H 'Idempotency-Key: sync-2026-10-24-0900' \
-d '{"changes":[
{"action":"add","parentId":null,"afterId":4711,"items":[{"issueId":"10001"},{"folderName":"Next quarter"}]},
{"action":"move","rowIds":[4712],"parentId":4800,"afterId":null},
{"action":"remove","rowIds":[4713],"keepChildren":true}]}'
{
"hierarchyId": 12,
"version": 49,
"added": [
{ "changeIndex": 0, "rowId": 6620417819, "kind": "issue", "issueId": "10001" },
{ "changeIndex": 0, "rowId": 2299815532, "kind": "folder", "folderId": "8" }
],
"removedRowIds": [4713]
}
added lists the new rows, with the index of the change that added each one. The app chooses new row ids. removedRowIds lists only rows the person could see.
- The person must be able to see every row the batch names, the rows above it, and every work item it adds. Otherwise the batch answers
403and changes nothing. - A row that no longer exists answers
409 CONFLICT. - A batch holds at most 200 changes, 2,000 rows and 50 new folders.
- A hierarchy holds at most 20,000 work items.
- Rows the person can't see below a removed row stay where they were, as in the app.
- New folders named by
folderNameare made before the change is written. If the batch is refused, they are deleted again. - Changes inside the rules' output need the hierarchy's setting for it. See Changes inside rule output.
- A hierarchy with rules rebuilds their output afterwards, in the background.
List rules
GET /v1/hierarchies/{id}/rules · Scope: read:hierarchy:custom · Needs: Automate
Lists the hierarchy's rules in tree order, with their settings. It needs Automate, as rule settings do in the app.
| Parameter | In | Description |
|---|---|---|
id |
path | The hierarchy's id. |
curl -s "$BASE/v1/hierarchies/12/rules" -H "Authorization: Bearer $TOKEN"
{
"hierarchyId": 12,
"version": 48,
"building": false,
"limitsReached": [],
"rules": [
{ "ruleId": "31", "rowId": 4800, "parentId": null, "position": 0, "type": "insertJql", "settings": { "type": "insertJql", "jql": "project = REL" } }
]
}
Each rule has its ruleId, its rowId in the tree, its parentId, its position among its siblings (from 0), its type, and its settings as the app keeps them. Later versions may add fields to settings.
List views
GET /v1/views · Scope: read:hierarchy:custom · Needs: no level on a hierarchy
Lists all of the person's own views and up to 100 shared views they may use.
| Parameter | In | Description |
|---|---|---|
search |
query | Narrows the shared views by name. Up to 100 characters. |
curl -s "$BASE/v1/views?search=release" -H "Authorization: Bearer $TOKEN"
{
"views": [
{ "id": 4, "name": "Release columns", "mine": true, "shared": true, "editable": true,
"columns": [ { "id": "c1", "type": "field", "field": "status" } ], "wrap": false }
],
"moreShared": false
}
mine is true for the person's own views, shared for views shared with others, and editable when the person may change the view in the app. columns lists its columns in order, as the app keeps them. moreShared is true when more shared views exist than were listed: search by name to find them.
Get a view
GET /v1/views/{viewId} · Scope: read:hierarchy:custom · Needs: no level on a hierarchy
Reads one view: one of the person's own, or a shared view they may use. Any other view answers 404.
| Parameter | In | Description |
|---|---|---|
viewId |
path | The view's id. |
curl -s "$BASE/v1/views/4" -H "Authorization: Bearer $TOKEN"
{ "id": 4, "name": "Release columns", "mine": true, "shared": true, "editable": true,
"columns": [ { "id": "c1", "type": "field", "field": "status" } ], "wrap": false }
What the API never shows
- Work items the person can't see in Jira. Rows that rules placed and that the person can't see are left out, with everything below them. A work item placed by hand that the person can't see shows as a
restrictedrow only where it holds rows they can see. That row says nothing about the work item. This holds for everyone, app admins included. - Work item fields. The API gives work item ids. Read the work items from Jira's REST API with the same token.
- Email addresses. People appear by Atlassian account id only.
- Rules and their settings, to people without Automate on the hierarchy.
Errors
Every error has the same shape:
{ "error": { "status": 409, "code": "CONFLICT", "message": "row no longer exists" } }
| Status | Code | Meaning and what to do |
|---|---|---|
| 400 | INVALID_REQUEST |
A parameter or the body is wrong. The message says which. |
| 401 | UNAUTHENTICATED |
The call came without a person's account, or the account the call acts as in Jira could not be confirmed. Use a person's 3LO token. |
| 401 | IDENTITY_MISMATCH |
Atlassian named more than one account or Jira site for the call, or Jira named another account for the token. Get a new token. |
| 401 | REAUTHORIZE |
The person must allow the app again: they open TreeCraft in Jira once and allow access. |
| 403 | FORBIDDEN |
The person's level on the hierarchy, App access or the Automation permissions don't allow it. The message says which. |
| 403 | API_TURNED_OFF |
An admin turned TreeCraft's switch off for this site. See Turn the API on. |
| 403 | JIRA_REFUSED |
Jira refused a request the app made as the person. Jira's own message follows. When Jira refuses a request the app makes as itself, the answer is 500 INTERNAL instead, without Jira's words. |
| 404 | NOT_FOUND |
No such hierarchy, row or view, or one the person can't see. |
| 409 | CONFLICT |
A row changed since you read it. Read again, then retry. |
| 409 | CURSOR_STALE |
The row a page continues from is gone. Read from the first page again. |
| 409 | IDEMPOTENCY_IN_PROGRESS |
A call with the same Idempotency-Key is still running. Wait retryAfterSeconds, then retry with the same key. |
| 409 | IDEMPOTENCY_UNFINISHED |
A call with the same Idempotency-Key ended without an answer, after it may have changed something. Read the hierarchy to see whether the change took effect. If it didn't, use a new key. Retrying with this key answers the same. |
| 422 | IDEMPOTENCY_KEY_REUSED |
The Idempotency-Key was used for a different request. Use a new key. |
| 429 | RATE_LIMITED |
Past the API's rate limits. Wait retryAfterSeconds. |
| 429 | JIRA_BUSY |
Jira is limiting the app's requests on this site. Wait retryAfterSeconds. |
| 500 | INTERNAL |
Something went wrong in the app. Retry. If it goes on, contact support with the time of the call. |
| 502 | JIRA_ERROR |
Jira failed to answer, or could not be reached as the person (a network error, a timeout, Atlassian's platform failing the request). Retry after retryAfterSeconds. |
| 503 | BUSY |
The hierarchy or the app's database is busy. Retry after retryAfterSeconds. |
| 503 | UPDATING |
The app is applying an update. Retry in a minute. |
429, 502 and 503 answers, and 409 IDEMPOTENCY_IN_PROGRESS, carry retryAfterSeconds in the body and a Retry-After header, both in seconds.
A token without the scope a route needs is refused by Atlassian's gateway, before the app, with 401 Unauthorized; scope does not match. That answer doesn't have the shape above.
Paging
Lists answer one page and a nextCursor. Pass it back as cursor to get the next page, until nextCursor is null. Cursors are opaque: don't build or change them.
| List | Default page | Largest page |
|---|---|---|
GET /v1/hierarchies |
100 hierarchies | 500 |
GET /v1/hierarchies/{id}/rows |
1,000 rows | 5,000 |
If the row a page continues from is gone, the answer is 409 CURSOR_STALE. Start again from the first page.
Rate limits
| Limit | Value |
|---|---|
| Requests per person | 120 a minute |
| Requests per site, all callers together | 1,000 a minute |
| Changes in one batch | 200, with 2,000 rows and 50 new folders |
| Request body | 800 kB |
Past a limit, the answer is 429 RATE_LIMITED with retryAfterSeconds. Atlassian applies its own rate limits to app REST APIs on top of these.
Each call also runs on your site's copy of the app. What it asks Jira counts against the app's hourly allowance of Jira API points on your site, which the app's pages share. Reading rows costs about the same as opening the hierarchy in the app. To keep the cost low:
- read with large pages;
- check a hierarchy's
versionwithGET /v1/hierarchies/{id}, and read its rows again only when it changed; - don't poll more often than you need.
Idempotency
POST, PATCH and DELETE take an optional Idempotency-Key header: 1 to 64 letters, digits, - and _. It makes a retry safe.
- The same key with the same request within 24 hours answers the first answer again, with the header
Idempotent-Replayed: true. Nothing is done twice. - The same key with a different request answers
422 IDEMPOTENCY_KEY_REUSED. - The same key while the first call is still running answers
409 IDEMPOTENCY_IN_PROGRESSwithRetry-After. Wait, then retry with the same key. - A first try that failed with
401,429or a5xxbefore it changed anything keeps nothing, so a retry with the same key runs it. After a401, retry once the person has signed in or allowed the app again. - A first try that failed after it changed something keeps its key. A retry with that key answers
409 IDEMPOTENCY_IN_PROGRESSfor the first minute, then409 IDEMPOTENCY_UNFINISHED: the first try ended without an answer. Read the hierarchy to see whether the change took effect. If it didn't, use a new key. - A replay is the person's own first answer to that request, sent as it was. It is not checked against their access again.
Use a new key for each change you mean to make, and the same key to retry that change. Without a key, a retried POST may do its work twice: a second hierarchy, or rows added twice.
Versioning
- Paths carry the version:
/v1. Within v1, changes only add things: new fields, optional parameters and new endpoints. Ignore fields you don't know. - A change that would break callers makes
/v2. v1 keeps working for at least 12 months after v2 is released. In that time, its answers carryDeprecationandSunsetheaders. - The scopes keep their meaning. A new scope means each integration asks its people to consent again.
Changelog
| Version | Date | Change |
|---|---|---|
| 1.0.0-beta | October 2026 | First version, in beta: hierarchies (list, get, create, change, delete), rows (read, batch changes), rules (list) and views (list, get). |
Planned next, in no promised order: computed column values for a view, work item keys in rows, rule changes, memos and planning items, history and tree queries.
Questions about the app? Write to info@plugincraft.ca.