# Requests to other systems

> How an Oktul Code Studio script calls other APIs with get, post, put, patch and delete, and reaches Atlassian endpoints the jira client cannot.
>
> Documentation for Oktul Code Studio, for Jira Cloud and Confluence Cloud, by Oktul: https://oktul.com/docs/plugins/code-studio/requests-to-other-systems/

An Oktul Code Studio script calls any HTTPS API with `http.get`, `http.post`, `http.put`,
`http.patch` and `http.delete`:

- **through a connection** that holds the credential, or
- **straight to a host** an administrator approved.

`fetch` works too. This page also covers the Atlassian endpoints the `jira` and `confluence`
clients do not reach.

## Three ways out

| Call | Goes to | Credential |
|---|---|---|
| `http.get('crm', '/api/tickets')` | The connection with the key `crm`, its base URL plus the path | Added by Code Studio after the script built the request |
| `http.get(null, 'https://api.example.com/rates')` | That URL, on a host an administrator approved | None, unless the script writes a header itself |
| `fetch('https://api.example.com/rates')` | The same, with the Web API's `Response` | None |

Set up a [connection](../connections-and-variables/#connections) for anything that needs a
credential, and the script never holds a token. A host with no credential, such as a public rates
API, is approved under **Hosts scripts may call directly** in the site settings.

## The verbs

```ts
const groups = await http.get('graph', '/groups', { query: { $top: 50 } });
const ticket = await http.post('crm', '/api/tickets', { reference: 'FIN-12', priority: 'high' });
await http.put('crm', `/api/tickets/${ticket.id}`, { status: 'open' });
await http.patch('crm', `/api/tickets/${ticket.id}`, { priority: 'low' });
await http.delete('crm', `/api/tickets/${ticket.id}`);
```

| Verb | Arguments |
|---|---|
| `http.get`, `http.delete` | `(connection, url, options?)` |
| `http.post`, `http.put`, `http.patch` | `(connection, url, body, options?)` |

- **`connection`** is a connection's key, or `null` for a full URL on an approved host.
- **`url`** is a path inside the connection's base URL: `/groups` under
  `https://graph.microsoft.com/v1.0` is sent to `https://graph.microsoft.com/v1.0/groups`. A full
  URL is accepted only when it starts with that base, so a script cannot send a connection's
  credential to another host or a sibling path. The request is refused before the credential is
  read.
- **`options.query`** is an object of parameters, encoded for you. A list value repeats the
  parameter: `{ id: ['1', '2'] }` sends `id=1&id=2`.
- **`options.headers`** are sent as written. The connection's credential and its secret headers
  win over a header of the same name.
- **The answer** is the parsed body: an object when the response says JSON, text otherwise, `null`
  when it is empty. A binary response, such as a PDF, arrives as base64 text.
- **A status of 400 or more throws `HttpError`**, whose `status`, `headers` and `body` hold the
  response. Its message names the method, the URL and the status.

## Bodies and headers

An object or an array is sent as JSON with `content-type: application/json`. A string is sent as
written, with no content type, so name one:

```ts
const xml = '<order><id>17</id></order>';
await http.post('erp', '/orders', xml, { headers: { 'content-type': 'application/xml' } });
```

A form body, as most token endpoints and older APIs take, is a string from `URLSearchParams`:

```ts
const form = new URLSearchParams({ grant_type: 'client_credentials', scope: 'read' }).toString();
await http.post('legacy', '/oauth/token', form, { headers: { 'content-type': 'application/x-www-form-urlencoded' } });
```

## Handling the answer yourself

`http.request` sends any method and never throws on a status, so a 404 or a 409 is an answer the
script reads:

```ts
const answer = await http.request({
    connection: 'crm',
    method: 'GET',
    url: `/api/customers/${customerId}`,
    query: { expand: 'contacts' },
});
if (answer.status === 404) return { created: false };
const customer = JSON.parse(answer.body);
```

The answer has `status`, `headers` (lower-cased names), `body` as text, `bodyEncoding`
(`'base64'` for a binary body) and `simulated`. Use it for retries too: on a 429 or a 503, read
`answer.headers['retry-after']`, wait, and send again, as
[writing scripts](../writing-scripts/#requests-time-and-parallel-work) shows.

## fetch

`fetch` is the Web API, for a script written against it, on an approved host and with no
credential added:

```ts
const response = await fetch('https://api.example.com/orders', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ reference: 'FIN-12' }),
});
if (!response.ok) throw new Error(`The order service answered ${response.status}`);
const order = await response.json();
```

The body is a string, `URLSearchParams` (sent as a form, with its content type set) or bytes
(`ArrayBuffer` or `Uint8Array`). The URL must be `https`, and a URL with a user name or password in
it is refused: put a credential in a connection.

## Atlassian endpoints the jira client does not reach

`jira` takes paths that start with `/rest/` or `/jsm/`, and `confluence` paths that start with
`/wiki/rest/` or `/wiki/api/`. Code Studio refuses any other path, because Atlassian's Forge proxy
routes the products' REST APIs and nothing else. Everything outside that goes through a
connection:

| What you need | How |
|---|---|
| This site through `api.atlassian.com/ex/jira/<this cloud id>/…` | Nothing. Code Studio sends it through Forge as the run's principal, with no token, and it does not count as an external request. Drop any code that asks `auth.atlassian.com` for a token first |
| Jira or Confluence on another site | The **Another Atlassian site** connector, with a service account. The base URL is `https://api.atlassian.com/ex/jira/<cloud id>`, and paths are the product's own, `http.get('other-site', '/rest/api/3/myself')` |
| Users, groups and the audit log of the organisation | The **Atlassian Administration** connector, with an organisation API key: `http.get('atlassian-admin', '/v1/orgs/<organisation ID>/users')` |

- **Out of reach:** a path outside the REST APIs, or an endpoint Atlassian does not document.
  Only a person's API token reached those, and Atlassian does not allow a Marketplace app to
  collect, send or store one.
- **Refused:** `/rest/api/3/uiModifications`, Code Studio's own dynamic forms path, however it is
  called through `jira`.

## In the sandbox

A write never leaves a sandbox run. A `POST`, `PUT`, `PATCH` or `DELETE` through a connection or to
an approved host is listed under Planned changes and answered with a simulated response:

| Method | Simulated answer |
|---|---|
| `POST` | 201 with `{ "id": "sandbox-1", "key": "SANDBOX-1", "simulated": true }` |
| `PUT`, `PATCH` | 200 with the JSON body the script sent, or 204 |
| `DELETE` | 204 |

- **Reads** (`GET`, `HEAD`, `OPTIONS`) go to an approved host. Through a connection, only when an
  administrator allowed sandbox reads on it; otherwise the read throws and says so.
- **A write's answer** is simulated. Keep what you sent rather than reading it back from the
  response.

## Limits

A run sends at most 1,500 requests outside this site, 100 per minute of runtime, which is Forge's
budget for an app's outbound requests. Requests to this site's own gateway are routed through
Forge and do not count. See [limits](../limits/).

## When a request is refused

| Message | What to do |
|---|---|
| There is no connection named … | Use a key from `connections.list()`, or ask an administrator to add the connection |
| … is not an approved host | An administrator approves the host under Connections, or adds a connection for it |
| … is not a Jira REST path | Use a path that starts with `/rest/` or `/jsm/`, or a connection as in the table above |
| This request names the site with cloud id … | The URL names another site's gateway with no token. Use `jira` with the path after the cloud id, or correct the cloud id the script uses |
| Sandbox runs do not read through … | Allow sandbox reads on the connection, or run in production |

## Common questions

**How does a script call an API outside Atlassian?**

With `http.get`, `http.post`, `http.put`, `http.patch` or `http.delete` and a connection's key, or with `fetch` to a host an administrator approved.

**Does a failed request throw?**

The verbs throw `HttpError` on a status of 400 or more. `http.request` and `fetch` return the answer instead.

**Do requests to other systems run in the sandbox?**

Reads do, where allowed. Writes never leave a sandbox run and are answered with a simulated response.

## Related

- [Connections and variables](../connections-and-variables/)
- [Writing scripts](../writing-scripts/)
- [Other systems snippets](../snippets/other-systems/)
- [Script API reference](../script-api/)
- [Sandbox and production](../sandbox-and-production/)

---

A problem or a question? Write to [support@oktul.com](mailto:support@oktul.com) or open a request in the [Help Center](https://oktul.atlassian.net/servicedesk/customer/portals). Both open a request with a reference and an SLA.
