> For the complete documentation index, see [llms.txt](https://docs.ativo.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ativo.io/rest-api/cloud-api-setup.md).

# Cloud API Setup

The Ativo Programs REST API lets your own scripts and tools read the Ativo configuration (teams, ARTs, PIs) and the PI plan data (objectives) of your Jira Cloud site. Jira issue data is never processed in the Ativo backend; read it from Jira's own API.

On Jira Cloud the API is a **Forge app REST API**: Atlassian handles the authentication, with OAuth 2.0. You create an **OAuth 2.0 integration** in the Atlassian developer console, consent once in the browser, and your script then works unattended with a refresh token. Ativo never sees or stores your credentials.

This guide follows Atlassian's own procedure, [Access REST APIs exposed by a Forge app](https://developer.atlassian.com/platform/forge/access-rest-apis-exposed-by-a-forge-app/), and adds the Ativo specifics and the refresh-token step that Atlassian documents separately in [OAuth 2.0 (3LO) apps](https://developer.atlassian.com/cloud/jira/platform/oauth-2-3lo-apps/).

## Who does what

| Step                                | Who                                                        | How often            |
| ----------------------------------- | ---------------------------------------------------------- | -------------------- |
| 1. Enable the API on the site       | Site or organisation admin                                 | Once per site        |
| 2. Create the OAuth 2.0 integration | The person who owns the integration (a member of the site) | Once per integration |
| 3. Consent and get the first tokens | Same person, in a browser                                  | Once                 |
| 4. Refresh and call                 | Your script                                                | Every run            |

## Step 1: Enable the API on your site

Forge app REST APIs are off by default for every site.

{% stepper %}
{% step %}

### Open Connected apps

Open [Atlassian Administration](https://admin.atlassian.com/) → **Apps** → your site → **Connected apps**.
{% endstep %}

{% step %}

### Open Ativo Programs details

Open **Ativo Programs** → **View app details** → **Details** tab.
{% endstep %}

{% step %}

### Enable App REST APIs

Switch **App REST APIs** on.<br>

<figure><img src="https://2916206105-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FBAGNud2YIYXdGXre7dQK%2Fuploads%2FKJzRKqo5r4dxSkDbNqVq%2Fimage.png?alt=media&amp;token=561e6bb3-aaa7-4f8b-9950-abd9de656bed" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}

The **base URL** of the API for your site is shown under **Jira settings → Apps → Ativo Programs → REST API**. It has this form (notice the `/rest/api/1` at the end) :

```
https://<your-site>.atlassian.net/gateway/api/svc/jira/apps/<app-id>_<environment-id>/rest/api/1
```

Copy it; every API call is this base URL plus an endpoint path such as `/team`. Atlassian's Connected apps screen shows the same URL without the `/rest/api/1` ending.

## Step 2: Create your OAuth 2.0 integration

This is done once in the [Atlassian developer console](https://developer.atlassian.com/console). You must be a member of the site where Ativo Programs is installed.

{% stepper %}
{% step %}

### Create the integration

Select **Create** → **OAuth 2.0 integration** → give it a name, for example "Ativo API - reporting script".
{% endstep %}

{% step %}

### Add Ativo Programs permissions

Open **Permissions**. On the right, click **Add Marketplace or custom app**.

In the dialog, select your site, then **Ativo Programs**, then the scopes your integration needs and click **Add**:

* `read:configuration:custom` — teams, ARTs (programs), PIs (periods), API version
* `read:plan:custom` — PI objectives\ <br>

  <figure><img src="https://2916206105-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FBAGNud2YIYXdGXre7dQK%2Fuploads%2Fj4cPpUAt4s6XzZiPvOcN%2Fimage.png?alt=media&amp;token=770f810c-7fc3-4503-a6c0-13a14534020b" alt=""><figcaption></figcaption></figure>

{% endstep %}

{% step %}

### Add the Jira product scope

Still under **Permissions**, find the **Jira API** row → **Add** → **Configure** → **Granular scopes** tab  →  **Edit** →  add `read:forge-app:jira`.

<figure><img src="https://2916206105-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FBAGNud2YIYXdGXre7dQK%2Fuploads%2FcHLZSQSw7DWMqqHTDN8l%2Fimage.png?alt=media&amp;token=ba688169-b90e-4ed1-b3d0-80503124aead" alt=""><figcaption></figcaption></figure>

{% hint style="warning" %}
Step 4 is easy to miss. Without `read:forge-app:jira` the API answers `3LO app with oauthClientId=... is not installed on site`.
{% endhint %}
{% endstep %}

{% step %}

### Note your credentials

Open **Settings** and note the **Client ID** and **Secret**. Treat the secret like a password.

<figure><img src="https://2916206105-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FBAGNud2YIYXdGXre7dQK%2Fuploads%2F6JxJjgWlSZyuzVG6EAfZ%2Fimage.png?alt=media&amp;token=c32eb2e7-47c9-4a4e-b573-b500f0a3d489" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}

## Step 3: Consent once and get your first tokens

OAuth 2.0 gives you two tokens, and it helps to know their roles before you start:

* The **access token** is what you send with every API call. It expires after one hour.
* The **refresh token** is what your script uses to get a new access token, without any browser. It stays valid for up to 90 days, or until it is refreshed.

Getting the first pair needs one browser step, done by a person. After that, everything can be done by your script (step 4).

{% stepper %}
{% step %}

### Set the callback URL

Open **Authorization** → **Add** next to OAuth 2.0 (3LO). Set the **Callback URL** to:

```
http://localhost:8080/callback
```

This is where Atlassian sends the browser after consent. Nothing has to run there; you only read the address bar. Do not use a real page such as your Jira site: it redirects and loses the code.\
\
The "Add classic scopes" warnings as the bottom of the page can be ignored.<br>

<figure><img src="https://2916206105-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FBAGNud2YIYXdGXre7dQK%2Fuploads%2Fm7m5amiVQRWwJ6OJrnbM%2Fimage.png?alt=media&amp;token=1de8408e-cfb8-426f-bffb-f696b73bd3cf" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Copy and update the authorization URL

{% hint style="success" %}
**Easier way:** use the [Sample script](/rest-api/cloud-api-setup/sample-token-exchange-script.md) for the authorization, tokens exchange and refresh
{% endhint %}

Under **Authorization URL generator** ("Use this URL to install your 3LO app"), copy the URL labelled **Marketplace App: Ativo Programs (support SAFe PI Planning Dependencies)**.

**Append** `%20offline_access` **to the** `scope` **parameter.** Insert `%20offline_access` just before `&redirect_uri` so it looks like:

```
...read%3Aforge-app%3Ajira%20offline_access&redirect_uri=...
```

`offline_access` is what gives you a refresh token. Without it the flow works only once.

<figure><img src="https://2916206105-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FBAGNud2YIYXdGXre7dQK%2Fuploads%2FQMxdjvcD4HlBnKoZLxZO%2Fimage.png?alt=media&amp;token=2969b2f1-a81e-47fb-89ce-ec57d4a01c09" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Consent and copy the code

{% hint style="success" %}
**Easier way:** use the [Sample script](/rest-api/cloud-api-setup/sample-token-exchange-script.md) for the authorization, tokens exchange and refresh
{% endhint %}

Paste the updated URL in your browser, choose your site and click **Accept**. You are then installing your 3LO integration app on the site.

<figure><img src="https://2916206105-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FBAGNud2YIYXdGXre7dQK%2Fuploads%2FJ6YTQKdD4suhL9maGvKU%2Fimage.png?alt=media&amp;token=bb7be966-d83c-4908-afd2-638dbf5e57e9" alt=""><figcaption></figcaption></figure>

The browser then lands on `http://localhost:8080/callback?code=...&state=...` and shows a connection error (we just want to read the URL). Copy the full value of `code` from the address bar (around 1600 characters). It is valid for a few minutes and can be used once.

<figure><img src="https://2916206105-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FBAGNud2YIYXdGXre7dQK%2Fuploads%2FRmcEWAM6XhOb4GHCzxPa%2Fimage.png?alt=media&amp;token=9e2c66e6-c3d7-4dfa-ac14-ad33d8616b9a" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Exchange the code for tokens

{% hint style="success" %}
**Easier way:** use the [Sample script](/rest-api/cloud-api-setup/sample-token-exchange-script.md) for the authorization, tokens exchange and refresh
{% endhint %}

Exchange the code for tokens, within a few minutes:

```http
POST https://auth.atlassian.com/oauth/token
Content-Type: application/json

{
  "grant_type": "authorization_code",
  "client_id": "<client id>", // From console > settings
  "client_secret": "<secret>",  // From console > settings
  "code": "<code from the address bar>", 
  "redirect_uri": "http://localhost:8080/callback"
}
```

The answer contains an `access_token` (valid one hour) and a `refresh_token`. **Store the refresh token securely**; it is the key your script uses from now on. The access token is only valid for one hour.
{% endstep %}

{% step %}

### Test the access token

Make your first call to test the access token Replace \<base url> with the value from ) :

```sh
curl "<base URL>/team" \
  -H "Authorization: Bearer <access_token>" \
  -H "Accept: application/json"
```

You get the list of teams as JSON and the header `X-API-Model-Version`.
{% endstep %}
{% endstepper %}

## Step 4: Keep it running with the refresh token

{% hint style="success" %}
**Easier way:** use the [Sample script](/rest-api/cloud-api-setup/sample-token-exchange-script.md) for the authorization, tokens exchange and refresh
{% endhint %}

Refresh tokens are valid up to 90 days, and needs to be rotated. Rotate the refresh token and access token at the start of your script.

```http
POST https://auth.atlassian.com/oauth/token
Content-Type: application/json

{
  "grant_type": "refresh_token",
  "client_id": "<client id>",
  "client_secret": "<secret>",
  "refresh_token": "<stored refresh token>"
}
```

The answer contains a new `access_token` **and a new `refresh_token`**. To keep this working:

* **Store the new refresh token.** Each refresh token can be used once; the one you just used stops working.
* **Refresh at least once every 90 days.** Every refresh gives the new token another 90 days. A script that stays silent longer must go through step 3 again.

A typical run is therefore: refresh → save the new refresh token → call the API with the new access token. A complete, copy-paste script that also handles the one-time consent of step 3 is in [Sample script](broken://pages/e4675fdafadcb30181dcdd5bf9f790aab4ec436b). Any OAuth 2.0 client library does the same, and Postman or Insomnia handle the whole flow, including refresh, if you only want to explore the API.

<figure><img src="https://2916206105-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FBAGNud2YIYXdGXre7dQK%2Fuploads%2FPLdfORfgeskriDJ2quAH%2Fimage.png?alt=media&amp;token=eac2c838-e614-4b6f-a417-a1e03be0477a" alt=""><figcaption></figcaption></figure>

## Troubleshooting

| Symptom                                                                                               | Cause                                                                                                                 | Fix                                                                                                            |
| ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `3LO app with oauthClientId=... is not installed on site`                                             | The Jira product scope is missing                                                                                     | Add `read:forge-app:jira` under Permissions → Jira API, consent again                                          |
| `403` from the API                                                                                    | App REST APIs are off for the site, or the token lacks the scope of that endpoint                                     | Check step 1 and the scope column in [Endpoints](broken://pages/7c89db55ed5dadbb48dfd0c6cf3167b977ebcde4)      |
| `Missing required scopes ... [read:configuration:custom]`, and the consent screen asked for Jira only | The authorization URL lacked the `sns=<app-id>.<environment-id>` parameter, so the Ativo scopes could not be resolved | Use the **Marketplace App: Ativo Programs** URL from the developer console, which carries `sns`; consent again |
| `authorization_code is invalid`                                                                       | Code expired, already used, or `redirect_uri` differs from the callback URL                                           | Run step 3 again, exchange within minutes, same callback URL                                                   |
| `403` HTML page from CloudFront on the token request                                                  | The token request was sent as GET                                                                                     | Use POST                                                                                                       |
| No `refresh_token` in the answer                                                                      | `offline_access` was not in the authorization URL                                                                     | Add it and consent again                                                                                       |
| `invalid_grant` on refresh                                                                            | Refresh token already rotated, or 90 days unused                                                                      | Use the latest stored token; otherwise run step 3 again                                                        |

## Rate limits

Atlassian's Forge platform limits apply to the API. Keep usage moderate, do not poll every minute, and cache responses: Ativo configuration changes slowly. A rate limited response contains the code `429` and can be retried after waiting.

## Next

* [Sample script](/rest-api/cloud-api-setup/sample-token-exchange-script.md): a Node.js script doing steps 2, 3 and 4 for you.
* [Endpoints](/rest-api/rest-api-endpoints.md): what you can fetch and which scope each endpoint needs.
* [Version](/rest-api/api-version.md): how to detect breaking changes in the data model.
* Still using legacy API tokens (`cloud-api.ativo.io`)? See [Migrating from legacy API tokens](/rest-api/cloud-api-setup/migrating-from-legacy-api-tokens.md).

## References

* Atlassian, [Access REST APIs exposed by a Forge app](https://developer.atlassian.com/platform/forge/access-rest-apis-exposed-by-a-forge-app/): enabling the API and creating the integration.
* Atlassian, [OAuth 2.0 (3LO) apps](https://developer.atlassian.com/cloud/jira/platform/oauth-2-3lo-apps/): the authorization URL, token exchange, refresh tokens and their lifetimes.
* Atlassian, [Forge app REST APIs](https://developer.atlassian.com/platform/forge/app-rest-apis/): the feature overview, currently in Preview.
