# HTTP Requests

Make HTTP requests from a workflow step using the global fetch API.

Workflow methods use the standard global `fetch()`. There is no `api.fetch()` wrapper.

`fetch()` is available in normal workflow steps and in lifecycle hooks.

## Shopify Admin GraphQL

When your `fetch()` URL targets your store's Admin API domain, the platform automatically injects the `X-Shopify-Access-Token` header. Do not set that header manually.

Your store domain and Admin API version are available on the global `env` object:

```js
export class Workflow {
  async start(data, headers, api) {
    const orderId = data.admin_graphql_api_id ?? `gid://shopify/Order/${data.id}`;
    const res = await fetch(
      `https://${env.SHOPIFY_STORE}/admin/api/${env.SHOPIFY_API_VERSION}/graphql.json`,
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          query: `mutation UpdateOrderNote($input: OrderInput!) {
            orderUpdate(input: $input) {
              order { id note }
              userErrors { field message }
            }
          }`,
          variables: { input: { id: orderId, note: 'Processed' } },
        }),
      }
    );

    if (!res.ok) {
      throw new Error(`Shopify API ${res.status}: ${await res.text()}`);
    }

    const json = await res.json();
    if (json.errors?.length) {
      console.log('GraphQL errors:', json.errors);
      return;
    }

    const { order, userErrors } = json.data.orderUpdate;
    if (userErrors.length) {
      console.log('User errors:', userErrors);
      return;
    }

    console.log('Updated note:', order.note);
  }
}
```

Read-only queries use the same endpoint and the same automatic token injection:

```js
const res = await fetch(
  `https://${env.SHOPIFY_STORE}/admin/api/${env.SHOPIFY_API_VERSION}/graphql.json`,
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      query: `query GetOrders {
        orders(first: 10) {
          nodes { id name }
        }
      }`,
    }),
  }
);

if (!res.ok) {
  throw new Error(`Shopify API ${res.status}: ${await res.text()}`);
}

const json = await res.json();
if (json.errors?.length) {
  throw new Error(JSON.stringify(json.errors));
}

const orders = json.data.orders.nodes;
```

## OAuth-connected services

Use `api.getOAuthToken(handle)` to get a valid access token for a connected OAuth service, then add it to your request headers manually. The platform automatically refreshes the token if it has expired.

```js
export class Workflow {
  async start(data, headers, api) {
    const { token, error } = await api.getOAuthToken('my-slack');
    if (error) {
      console.log('OAuth error:', error);
      return;
    }

    await fetch('https://slack.com/api/chat.postMessage', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${token}`,
      },
      body: JSON.stringify({ channel: '#orders', text: `New order: ${data.id}` }),
    });
  }
}
```

The `handle` is the name you assigned when connecting the service in **OAuth Connections**.

## External APIs with secrets

Store API keys via **More actions → Manage variables** in the workflow editor and access them via `env.*`:

```js
const res = await fetch('https://api.example.com/notify', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${env.EXAMPLE_API_KEY}`,
  },
  body: JSON.stringify({ event: 'order_paid', orderId: data.id }),
});
```

## Sending JSON bodies

Pass a plain JavaScript object as the body by serialising it with `JSON.stringify()`. Set `Content-Type: application/json` when doing this:

```js
const res = await fetch('https://api.example.com/endpoint', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ key: 'value' }),
});
```