Your first API call in 5 minutes
Create an OAuth integration, sign in once with a ready-to-use script, and read your first hierarchy from the REST API.
On this page
- Before you start
- Step 1: Create the integration
- Step 2: Add TreeCraft's scopes
- Step 3: Set the callback URL
- Step 4: Give the script its settings
- Step 5: Sign in
- Step 6: Make your first call
- Examples to copy
- The script for Node.js
- The script for PowerShell
- If something goes wrong
- Security: why OAuth and not personal tokens
Before you start
You need:
- TreeCraft on a Jira Cloud site, and your own account on it. The person who signs in is the person every call acts as, with their access to each hierarchy.
- Both switches on. A site admin turns on Atlassian's App REST APIs switch. TreeCraft's own switch is on by default. See Turn the API on.
- Node.js 18 or later, which you can check with
node --version, or Windows PowerShell 5.1 or later. The scripts on this page use nothing else. - TreeCraft opened once in Jira, with access allowed. Until the person has done that, calls answer
401 REAUTHORIZE. - The app id and the environment id, which a Jira admin or app admin finds on TreeCraft's REST API page: in Jira's administration area, under Apps, choose TreeCraft, then REST API in the Access group.
Step 1: Create the integration
- Open the Atlassian developer console at
developer.atlassian.com/console/myapps, select Create, then OAuth 2.0 integration, and give it a name, such as "TreeCraft scripts". - Under Settings, copy the Client ID and the Secret. The secret works like a password: keep it out of anything you share.
Step 2: Add TreeCraft's scopes
- In the integration, go to Permissions and select Add Marketplace or custom app. Pick your site, then TreeCraft.
- Add the scopes the integration needs. Start with the first one, and add the others when a script needs them.
| Scope | What it lets the integration do |
|---|---|
read:hierarchy:custom |
Read the hierarchies you can open: their details, rows, rules, settings, planning, baselines, and the views you can use. Changes nothing. |
write:hierarchy:custom |
In hierarchies you can edit: place work items, folders, memos and planning items, move and remove rows, rename folders, add baselines, and mark favorites. |
manage:hierarchy:custom |
Create, rename, archive and delete hierarchies you control, change their access rules, settings and planning, add and change rules, and manage your views. |
read:capacity:custom |
Read the site's working calendar and holidays, the holiday calendars, and the time off of the people you may see it for. Changes nothing. |
write:capacity:custom |
Add, remove and replace time off for the people you may change it for. Jira admins and app admins can also change holidays and holiday calendars. |
Also add the Jira scope read:forge-app:jira. Every call to an app's REST API needs it.
A scope never gives more than you have. The full list is in Scopes.
Step 3: Set the callback URL
Under Authorization, add http://localhost:8765/callback as the callback URL. While you sign in, the Node.js script listens at that address on your own computer, so the browser can hand it the code. The PowerShell script doesn't listen: the page the browser reaches doesn't load, and you copy its address (see step 5).
To use another port, change the address here and set TREECRAFT_CALLBACK to the same value.
Step 4: Give the script its settings
The scripts read their settings from environment variables, so no secret is written in a file you might share.
| Variable | Value |
|---|---|
TREECRAFT_CLIENT_ID |
The Client ID from step 1. |
TREECRAFT_CLIENT_SECRET |
The Secret from step 1. |
TREECRAFT_APP_ID |
The app id from TreeCraft's REST API page. |
TREECRAFT_ENVIRONMENT_ID |
The environment id from the same page. |
TREECRAFT_SITE |
Your site's address, such as your-site.atlassian.net. The script reads your cloud id from it. Instead, set TREECRAFT_CLOUD_ID to the cloud id shown on the REST API page. |
TREECRAFT_SCOPES |
Optional. The scopes to ask for when you sign in, from read, write, manage, capacity-read and capacity-write, separated by commas. The default is read. |
TREECRAFT_TOKEN_FILE |
Optional. Where to keep the refresh token. The default is treecraft-token.json in the folder you run the script from. |
TREECRAFT_CALLBACK |
Optional. The callback URL, if you didn't use http://localhost:8765/callback. |
In bash (macOS, Linux, or Git Bash on Windows), the secret is read without showing it:
export TREECRAFT_CLIENT_ID='your-client-id'
read -s -p 'Secret: ' TREECRAFT_CLIENT_SECRET && export TREECRAFT_CLIENT_SECRET
export TREECRAFT_APP_ID='your-app-id'
export TREECRAFT_ENVIRONMENT_ID='your-environment-id'
export TREECRAFT_SITE='your-site.atlassian.net'
In PowerShell:
$env:TREECRAFT_CLIENT_ID = 'your-client-id'
$env:TREECRAFT_CLIENT_SECRET = [Net.NetworkCredential]::new('', (Read-Host 'Secret' -AsSecureString)).Password
$env:TREECRAFT_APP_ID = 'your-app-id'
$env:TREECRAFT_ENVIRONMENT_ID = 'your-environment-id'
$env:TREECRAFT_SITE = 'your-site.atlassian.net'
These settings last for that terminal window only.
Step 5: Sign in
Save one of the scripts from The script for Node.js or The script for PowerShell, below, then sign in once:
node treecraft-api.mjs login
.\treecraft-api.ps1 login
The script prints an address. Open it in your browser, pick your site and select Accept. With the Node.js script, the browser then shows "Signed in" and the script carries on by itself. With the PowerShell script, the next page doesn't load: copy its whole address from the browser's address bar and paste it where the script asks.
Behind the scenes, Atlassian sends an authorization code to the callback URL, and the script exchanges it for two tokens. The access token is short-lived, and the script never saves it. The refresh token is saved in the token file, and every run uses it to get a fresh access token. This is the OAuth 2.0 (3LO) flow that Get an access token describes step by step.
Atlassian replaces the refresh token each time it is used, and the script saves the newest one. So don't run two copies of the script at the same moment. A refresh token that stays unused for a long time expires: sign in again.
The script prints the scopes that were granted. TreeCraft's appear as <app-id>.<environment-id>:read:hierarchy:custom and so on. If none of them show, the app id or the environment id is wrong.
Step 6: Make your first call
List the hierarchies you can open:
node treecraft-api.mjs hierarchies
.\treecraft-api.ps1 hierarchies
The answer looks like this:
{
"hierarchies": [
{ "id": 12, "name": "Release plan", "description": "", "ownerAccountId": "5b10ac8d82e05b22cc7d4ef5", "access": "control",
"archived": false, "version": 48, "labels": ["Planning"], "favorite": true, "searchable": false }
],
"nextCursor": null
}
That is the whole path: sign-in, token, call. The rest of this page is examples, the scripts, and the security reasons behind OAuth.
Examples to copy
Read the rows of the hierarchy with id 12, 50 at a time. Rows come as ids only, never work item content:
node treecraft-api.mjs rows 12 50
Any other call in the REST API guide works with request. It takes the method, the path and, for a change, a JSON body written on the command line or in a file (@body.json). For example, to mark the hierarchy as a favorite (this needs the write scope, so sign in again with TREECRAFT_SCOPES=read,write). On Windows, node may drop the quotes inside a JSON body typed in PowerShell: put the body in a file and write @body.json instead, or use the PowerShell script:
node treecraft-api.mjs request PATCH /v2/hierarchies/12/favorite '{"favorite":true}'
.\treecraft-api.ps1 request PATCH /v2/hierarchies/12/favorite '{"favorite":true}'
To add holidays to a holiday calendar (here the one with id 10), and to add time off for a person, sign in again with TREECRAFT_SCOPES=read,capacity-read,capacity-write. The first needs a Jira admin or an app admin. The second needs the person themself, a manager of a team they are in, or an admin. The commands take a calendar id and days, or an account id, the first day, the last day (left out: the same day) and the hours away each day (left out: whole days):
node treecraft-api.mjs holidays-add 10 2027-12-24 2027-12-31
node treecraft-api.mjs timeoff-add 5b10ac8d82e05b22cc7d4ef5 2027-08-02 2027-08-13
node treecraft-api.mjs timeoff-add 5b10ac8d82e05b22cc7d4ef5 2027-09-03 2027-09-03 4
.\treecraft-api.ps1 holidays-add 10 2027-12-24 2027-12-31
.\treecraft-api.ps1 timeoff-add 5b10ac8d82e05b22cc7d4ef5 2027-08-02 2027-08-13
.\treecraft-api.ps1 timeoff-add 5b10ac8d82e05b22cc7d4ef5 2027-09-03 2027-09-03 4
These two calls are described in Add time off and Change a holiday calendar.
Every change carries an Idempotency-Key that the script makes up, and a call that Atlassian or TreeCraft answers with 429, 502 or 503 is tried again after the wait they ask for. See Idempotency.
The script for Node.js
Save this as treecraft-api.mjs. It uses Node.js's built-in fetch, http, crypto and fs, and installs nothing. It reads its settings from the environment variables above, keeps only the refresh token in the token file, and never prints a token or the secret.
// treecraft-api.mjs: sign in to TreeCraft's REST API and make calls. Needs Node.js 18 or later and nothing else.
import crypto from 'node:crypto';
import fs from 'node:fs';
import http from 'node:http';
const fail = (message) => { console.error(message); process.exit(1); };
if (typeof fetch !== 'function') fail('This script needs Node.js 18 or later.');
const need = (name) => process.env[name] || fail(`Set ${name} first.`);
const CALLBACK = process.env.TREECRAFT_CALLBACK || 'http://localhost:8765/callback';
const TOKEN_FILE = process.env.TREECRAFT_TOKEN_FILE || 'treecraft-token.json';
const SCOPES = {
read: 'read:hierarchy:custom',
write: 'write:hierarchy:custom',
manage: 'manage:hierarchy:custom',
'capacity-read': 'read:capacity:custom',
'capacity-write': 'write:capacity:custom',
};
async function tokenRequest(body) {
const res = await fetch('https://auth.atlassian.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ client_id: need('TREECRAFT_CLIENT_ID'), client_secret: need('TREECRAFT_CLIENT_SECRET'), ...body }),
});
const answer = await res.json().catch(() => ({}));
if (!res.ok) fail(`Atlassian refused the request (${res.status}): ${answer.error || ''} ${answer.error_description || ''}`);
return answer;
}
function keepRefreshToken(answer) {
// Atlassian replaces the refresh token each time it is used, so the newest one always goes in the file.
if (!answer.refresh_token) return;
fs.writeFileSync(`${TOKEN_FILE}.tmp`, JSON.stringify({ refresh_token: answer.refresh_token }), { mode: 0o600 });
fs.renameSync(`${TOKEN_FILE}.tmp`, TOKEN_FILE);
}
async function login() {
const asked = (process.env.TREECRAFT_SCOPES || 'read').split(/[\s,]+/).filter(Boolean);
const scope = [...asked.map((name) => SCOPES[name] || fail(`Unknown scope "${name}".`)), 'read:forge-app:jira', 'offline_access'].join(' ');
const state = crypto.randomBytes(16).toString('hex');
const query = {
audience: 'api.atlassian.com', client_id: need('TREECRAFT_CLIENT_ID'), scope, redirect_uri: CALLBACK, state,
response_type: 'code', prompt: 'consent', sns: `${need('TREECRAFT_APP_ID')}.${need('TREECRAFT_ENVIRONMENT_ID')}`,
};
console.log('Open this address in your browser, pick your site and select Accept:\n');
console.log('https://auth.atlassian.com/authorize?' + Object.entries(query).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&'));
const code = await new Promise((resolve, reject) => {
const server = http.createServer((req, res) => {
const url = new URL(req.url, CALLBACK);
if (url.pathname !== new URL(CALLBACK).pathname) return res.writeHead(404).end();
const error = url.searchParams.get('error');
const ok = !error && url.searchParams.get('state') === state && url.searchParams.get('code');
res.writeHead(ok ? 200 : 400, { 'Content-Type': 'text/plain', Connection: 'close' }).end(ok ? 'Signed in. You can close this tab.' : 'Sign-in failed. See the terminal.');
server.close();
if (ok) resolve(url.searchParams.get('code'));
else reject(new Error(error || 'The answer did not match this sign-in. Start again.'));
});
server.listen(Number(new URL(CALLBACK).port), 'localhost');
setTimeout(() => reject(new Error('Nobody signed in within 5 minutes.')), 300_000).unref();
}).catch((e) => fail(e.message));
const answer = await tokenRequest({ grant_type: 'authorization_code', code, redirect_uri: CALLBACK });
keepRefreshToken(answer);
const granted = answer.scope || '';
console.log(`\nSigned in. The refresh token is in ${TOKEN_FILE}. Scopes granted:\n${granted}`);
if (!granted.includes(':custom')) console.log("\nNone of TreeCraft's scopes were granted. Check the app id and environment id.");
}
async function accessToken() {
if (!fs.existsSync(TOKEN_FILE)) fail('Not signed in yet. Run: node treecraft-api.mjs login');
const answer = await tokenRequest({ grant_type: 'refresh_token', refresh_token: JSON.parse(fs.readFileSync(TOKEN_FILE, 'utf8')).refresh_token });
keepRefreshToken(answer);
return answer.access_token;
}
async function baseUrl() {
let cloudId = process.env.TREECRAFT_CLOUD_ID;
if (!cloudId) {
const res = await fetch(`https://${need('TREECRAFT_SITE')}/_edge/tenant_info`);
cloudId = res.ok ? (await res.json()).cloudId : fail(`Could not read the cloud id from ${process.env.TREECRAFT_SITE} (${res.status}).`);
}
return `https://api.atlassian.com/svc/jira/${cloudId}/apps/${need('TREECRAFT_APP_ID')}_${need('TREECRAFT_ENVIRONMENT_ID')}`;
}
async function call(method, path, body) {
const headers = { Authorization: `Bearer ${await accessToken()}`, Accept: 'application/json' };
if (body !== undefined) headers['Content-Type'] = 'application/json';
if (method !== 'GET') headers['Idempotency-Key'] = crypto.randomUUID(); // a retry of the same call reuses it
const url = (await baseUrl()) + path;
for (let attempt = 1; ; attempt++) {
const res = await fetch(url, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) });
if ([429, 502, 503].includes(res.status) && attempt < 4) {
const wait = res.headers.get('Retry-After');
const seconds = Math.min(wait === null ? 2 * attempt : Number(wait) || 0, 30);
console.error(`Answered ${res.status}. Trying again in ${seconds} seconds.`);
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
continue;
}
const text = await res.text();
if (!res.ok) console.error(`Answered ${res.status}`);
try { console.log(JSON.stringify(JSON.parse(text), null, 2)); } catch { if (text) console.log(text); }
if (!res.ok) process.exit(1);
return;
}
}
const id = (value) => (/^\d+$/.test(value || '') ? value : fail('That needs a number, such as 12.'));
const [command, ...args] = process.argv.slice(2);
switch (command) {
case 'login': await login(); break;
case 'hierarchies': await call('GET', `/v2/hierarchies?limit=${Number(args[0]) || 100}`); break;
case 'rows': await call('GET', `/v2/hierarchies/${id(args[0])}/rows?limit=${Number(args[1]) || 1000}`); break;
case 'holidays-add': await call('PATCH', `/v2/holidaycalendars/${id(args[0])}`, { addDays: args.slice(1) }); break;
case 'timeoff-add': await call('POST', '/v2/timeoff', { accountId: args[0] || fail('Give an account id.'), timeOff: [{ from: args[1], to: args[2] || args[1], ...(args[3] ? { hours: Number(args[3]) } : {}) }] }); break;
case 'request': { // request METHOD /v2/path [json | @file.json]
if (!args[0] || !args[1]?.startsWith('/')) fail('Use: request <METHOD> </v2/path> [json or @file.json]');
const raw = args[2];
await call(args[0].toUpperCase(), args[1], raw === undefined ? undefined : JSON.parse(raw.startsWith('@') ? fs.readFileSync(raw.slice(1), 'utf8') : raw));
break;
}
default: fail('Commands: login | hierarchies [limit] | rows <hierarchyId> [limit] | holidays-add <calendarId> <YYYY-MM-DD>... | timeoff-add <accountId> <from> [to] [hours] | request <METHOD> </v2/path> [json or @file.json]');
}
The token file is created for your user only where the system supports it. Keep it out of source control, and delete it when you no longer need it: anyone who holds it can call the API as you until the access is revoked.
The script for PowerShell
Save this as treecraft-api.ps1. It works in Windows PowerShell 5.1 and later, uses only what PowerShell includes, and has the same commands. If PowerShell refuses to run scripts, run it with powershell -ExecutionPolicy Bypass -File treecraft-api.ps1 hierarchies.
# treecraft-api.ps1: sign in to TreeCraft's REST API and make calls. Needs Windows PowerShell 5.1 or later and nothing else.
$ErrorActionPreference = 'Stop'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
function Fail($message) { throw $message }
function Setting($name, $default) { $value = [Environment]::GetEnvironmentVariable($name); if ($value) { $value } else { $default } }
function Need($name) { $value = Setting $name $null; if ($value) { $value } else { Fail "Set $name first." } }
$Callback = Setting 'TREECRAFT_CALLBACK' 'http://localhost:8765/callback'
$TokenFile = Setting 'TREECRAFT_TOKEN_FILE' 'treecraft-token.json'
$Scopes = @{
read = 'read:hierarchy:custom'
write = 'write:hierarchy:custom'
manage = 'manage:hierarchy:custom'
'capacity-read' = 'read:capacity:custom'
'capacity-write' = 'write:capacity:custom'
}
function Read-ErrorBody($record) { try { $record.ErrorDetails.Message | ConvertFrom-Json } catch { $null } }
function Get-Token($extra) {
$body = @{ client_id = Need 'TREECRAFT_CLIENT_ID'; client_secret = Need 'TREECRAFT_CLIENT_SECRET' } + $extra
try {
Invoke-RestMethod -Method Post -Uri 'https://auth.atlassian.com/oauth/token' -ContentType 'application/json' -Body ($body | ConvertTo-Json)
} catch {
$why = Read-ErrorBody $_
Fail "Atlassian refused the request ($([int]$_.Exception.Response.StatusCode)): $($why.error) $($why.error_description)"
}
}
function Save-RefreshToken($answer) {
# Atlassian replaces the refresh token each time it is used, so the newest one always goes in the file.
if ($answer.refresh_token) { @{ refresh_token = $answer.refresh_token } | ConvertTo-Json | Set-Content -Path $TokenFile -Encoding UTF8 }
}
function Login {
$scope = @(foreach ($name in ((Setting 'TREECRAFT_SCOPES' 'read') -split '[\s,]+' | Where-Object { $_ })) {
if ($Scopes[$name]) { $Scopes[$name] } else { Fail "Unknown scope ""$name""." }
}) + 'read:forge-app:jira', 'offline_access'
$state = [guid]::NewGuid().ToString('N')
$query = [ordered]@{
audience = 'api.atlassian.com'; client_id = Need 'TREECRAFT_CLIENT_ID'; scope = ($scope -join ' '); redirect_uri = $Callback; state = $state
response_type = 'code'; prompt = 'consent'; sns = "$(Need 'TREECRAFT_APP_ID').$(Need 'TREECRAFT_ENVIRONMENT_ID')"
}
Write-Host "Open this address in your browser, pick your site and select Accept:`n"
Write-Host ('https://auth.atlassian.com/authorize?' + (($query.GetEnumerator() | ForEach-Object { "$($_.Key)=$([uri]::EscapeDataString($_.Value))" }) -join '&'))
Write-Host "`nThe next page will not load, because nothing listens at $Callback. That is expected."
$back = (Read-Host 'Copy the whole address of that page from the address bar and paste it here').Trim()
$answer = @{}
foreach ($pair in ([uri]$back).Query.TrimStart('?').Split('&')) {
$i = $pair.IndexOf('=')
if ($i -gt 0) { $answer[$pair.Substring(0, $i)] = [uri]::UnescapeDataString($pair.Substring($i + 1)) }
}
if ($answer['error']) { Fail "Authorization refused: $($answer['error']) $($answer['error_description'])" }
if ($answer['state'] -ne $state -or -not $answer['code']) { Fail 'The address did not match this sign-in. Start again.' }
$token = Get-Token @{ grant_type = 'authorization_code'; code = $answer['code']; redirect_uri = $Callback }
Save-RefreshToken $token
Write-Host "`nSigned in. The refresh token is in $TokenFile. Scopes granted:`n$($token.scope)"
if ($token.scope -notmatch ':custom') { Write-Host "`nNone of TreeCraft's scopes were granted. Check the app id and environment id." }
}
function Get-AccessToken {
if (-not (Test-Path $TokenFile)) { Fail 'Not signed in yet. Run: ./treecraft-api.ps1 login' }
$answer = Get-Token @{ grant_type = 'refresh_token'; refresh_token = (Get-Content -Raw $TokenFile | ConvertFrom-Json).refresh_token }
Save-RefreshToken $answer
$answer.access_token
}
function Get-BaseUrl {
$cloudId = Setting 'TREECRAFT_CLOUD_ID' $null
if (-not $cloudId) { $cloudId = (Invoke-RestMethod "https://$(Need 'TREECRAFT_SITE')/_edge/tenant_info").cloudId }
"https://api.atlassian.com/svc/jira/$cloudId/apps/$(Need 'TREECRAFT_APP_ID')_$(Need 'TREECRAFT_ENVIRONMENT_ID')"
}
function Call($method, $path, $json) {
$headers = @{ Authorization = "Bearer $(Get-AccessToken)"; Accept = 'application/json' }
if ($method -ne 'GET') { $headers['Idempotency-Key'] = [guid]::NewGuid().ToString() } # a retry of the same call reuses it
$request = @{ Method = $method; Uri = (Get-BaseUrl) + $path; Headers = $headers }
if ($json) { $request.Body = $json; $request.ContentType = 'application/json' }
for ($attempt = 1; ; $attempt++) {
try {
$answer = Invoke-RestMethod @request
if ($null -ne $answer) { $answer | ConvertTo-Json -Depth 20 }
return
} catch {
$status = [int]$_.Exception.Response.StatusCode
$why = Read-ErrorBody $_
if ($status -in 429, 502, 503 -and $attempt -lt 4) {
$seconds = [Math]::Min($(if ($why.error.retryAfterSeconds) { $why.error.retryAfterSeconds } else { 2 * $attempt }), 30)
[Console]::Error.WriteLine("Answered $status. Trying again in $seconds seconds.")
Start-Sleep -Seconds $seconds
continue
}
Fail "Answered $status`n$($_.ErrorDetails.Message)"
}
}
}
function Get-Id($value) { if ($value -match '^\d+$') { $value } else { Fail 'That needs a number, such as 12.' } }
function Invoke-TreeCraft($command, $rest) {
switch ($command) {
'login' { Login }
'hierarchies' { Call GET "/v2/hierarchies?limit=$(if ($rest[0]) { [int]$rest[0] } else { 100 })" }
'rows' { Call GET "/v2/hierarchies/$(Get-Id $rest[0])/rows?limit=$(if ($rest[1]) { [int]$rest[1] } else { 1000 })" }
'holidays-add' { Call PATCH "/v2/holidaycalendars/$(Get-Id $rest[0])" (@{ addDays = @($rest | Select-Object -Skip 1) } | ConvertTo-Json) }
'timeoff-add' {
$entry = @{ from = $rest[1]; to = $(if ($rest[2]) { $rest[2] } else { $rest[1] }) }
if ($rest[3]) { $entry.hours = [double]$rest[3] }
Call POST '/v2/timeoff' (@{ accountId = $rest[0]; timeOff = @($entry) } | ConvertTo-Json -Depth 5)
}
'request' { # request METHOD /v2/path [json | @file.json]
if (-not $rest[0] -or -not "$($rest[1])".StartsWith('/')) { Fail 'Use: request <METHOD> </v2/path> [json or @file.json]' }
$json = $rest[2]; if ($json -and $json.StartsWith('@')) { $json = Get-Content -Raw $json.Substring(1) }
Call $rest[0].ToUpper() $rest[1] $json
}
default { Fail 'Commands: login | hierarchies [limit] | rows <hierarchyId> [limit] | holidays-add <calendarId> <YYYY-MM-DD>... | timeoff-add <accountId> <from> [to] [hours] | request <METHOD> </v2/path> [json or @file.json]' }
}
}
if ($MyInvocation.InvocationName -ne '.') {
try { Invoke-TreeCraft $args[0] @($args | Select-Object -Skip 1) } catch { [Console]::Error.WriteLine($_.Exception.Message); exit 1 }
}
The token file is a normal file in the folder you choose. Keep it in a folder only you can read.
If something goes wrong
| What you see | What to check |
|---|---|
403 with "Missing required scopes" |
The app id or the environment id is wrong, or the scope wasn't added to the integration in step 2, or you didn't ask for it when you signed in. Sign in again. |
401 REAUTHORIZE |
Open TreeCraft in Jira once and allow access, as the person who signed in. |
403 API_TURNED_OFF |
A switch is off: Atlassian's App REST APIs, or TreeCraft's own. See Turn the API on. |
404 on a hierarchy |
The person who signed in can't open it, or it doesn't exist. |
"Atlassian refused the request" with invalid_grant |
The sign-in is used up or expired: run login again. |
| "Not signed in yet" | Run login first, from the same folder, or point TREECRAFT_TOKEN_FILE at the file. |
The other answers are listed in Errors.
Security: why OAuth and not personal tokens
TreeCraft's API uses OAuth 2.0 (3LO) with scopes of its own, rather than a personal token that anyone could paste into a script. It takes a few more steps to start, and it protects you better:
- Consent. The person who signs in sees what each scope lets the integration do, and chooses to accept. The token belongs to that person, and every call acts as them: it is checked against their access to the hierarchy and their Jira permissions, and it never does more than they can.
- Scopes. An integration asks only for what it needs. A script that reads holds a token that can't change anything.
- Revocation. The person can withdraw their consent from their Atlassian account. The integration's owner can change its secret or delete it in the developer console. Access tokens last a short time, and a new one needs the refresh token.
- An admin switch. A site admin can turn off Atlassian's App REST APIs switch, and Jira admins and app admins can turn off TreeCraft's own switch. Either one stops every call at once.
- Nothing for TreeCraft to guard. Atlassian checks the token and its scopes before a call reaches the app, and TreeCraft stores no token or secret.
- Your data stays in Atlassian. Calls go through Atlassian's gateway to the app, which runs on Atlassian's cloud. The app sends no data anywhere else and is built to Atlassian's Runs on Atlassian requirements.
For your own script: keep the client secret and the token file out of source control, ask only for the scopes you use, and use a separate integration for each tool, so that you can revoke one without stopping the others.
Questions about the app? Write to info@plugincraft.ca.