---
title: "Send an HTTP request from Shopify Flow on any plan"
description: "Step-by-step guide with video: call an external API from Shopify Flow using Workflow Transactional Email, including on Shopify Basic. Worked Airtable example."
canonical: "https://docs.workflow-transactional-email.app/make-an-http-request"
---

# Make an HTTP request from Shopify Flow

## Why use this for HTTP requests

Shopify Flow includes a built-in **Send HTTP request** action, but it is only available on certain Shopify plans. If your store is on **Basic**, you can still call external APIs by using Workflow Transactional Email instead.

You create the request once in the app, then select it inside your workflow using the **HTTP Request** action. Keeping the request in the app rather than in the workflow also means credentials never sit in your Flow configuration, and you get full request and response logging for free.

## What you will build

A workflow that runs every 10 minutes, finds products that are not yet in Airtable, and sends them there.

This uses:

- **Secrets** to hold the Airtable token and base id
- **HTTP Requests** to define the request template
- The **HTTP Request** action inside Shopify Flow to run it
- **History** to check the result

## Video guide

The whole setup: creating the secrets, the HTTP request, and the workflow. **The video has no sound.**

https://www.youtube.com/watch?v=PJL5Z0yGlFw

> [!NOTE]
> Near the end of the recording the product fetch returns nothing - there is a stray space in the search query. The written steps below have it right.

## Before you start

- **Workflow Transactional Email** is installed.
- You have access to **Shopify Flow**.
- You have an Airtable base id and a table ready to receive product data.
- You have an [Airtable access token](https://airtable.com/create/tokens) with the right permissions.

## Step 1: Store the credentials as secrets

Rather than typing credentials into the request, save them once as secrets and reference them as `{{ secrets.yourKeyName }}`. They are encrypted at rest, reusable across requests, and redacted from your history.

Open **Secrets** and click **Create secret**.

![The Secrets page with the Create secret button](https://cdn-dev.eu.codecreationlabs.cloud/crm-tool/uploads/SjJ78gKLnLlocopCui3Cs62dJbGZ7F1n/e45ae9d7206b7dac.png)

Enter the key, the value and a description, then save.

![Entering a secret key, value and description](https://cdn-dev.eu.codecreationlabs.cloud/crm-tool/uploads/SjJ78gKLnLlocopCui3Cs62dJbGZ7F1n/53048ce7631d1235.png)

This example stores two secrets: the personal access token, and the Airtable base id.

## Step 2: Create the request

This example uses [Airtable's create-records endpoint](https://airtable.com/developers/web/api/create-records).

Open **HTTP Requests** and click **Create request**.

![The HTTP Requests page with the Create request button](https://cdn-dev.eu.codecreationlabs.cloud/crm-tool/uploads/SjJ78gKLnLlocopCui3Cs62dJbGZ7F1n/01e003d164fe7e2c.png)

Give it a clear name such as **Create Airtable product record** - this is what you will look for later inside Shopify Flow.

Set the method. For creating Airtable records that is **POST**.

Enter the endpoint in the URL field. The `https://` prefix is fixed, so type the rest:

```
api.airtable.com/v0/{{ secrets.airTableProductsBaseId }}/Products
```

This image shows how the Airtable endpoint maps to your base, so you can adjust it for your own:

![How the Airtable endpoint URL maps to the base](https://cdn-dev.eu.codecreationlabs.cloud/crm-tool/uploads/SjJ78gKLnLlocopCui3Cs62dJbGZ7F1n/70cefd1290f22fd4.png)

Add the headers Airtable requires:

```
Authorization: Bearer {{ secrets.airtableAccessToken }}
Content-Type: application/json
```

In the request body, use a variable for the records so Flow can supply them:

```
{
  "records": {{ variables.records }}
}
```

Airtable expects that variable to resolve to an array shaped like this:

```
"records": [
  {
    "fields": {
      "Title": "Classic White T-Shirt",
      "Handle": "classic-white-tshirt",
      "Product ID": 1001
    }
  },
  {
    "fields": {
      "Title": "Eco-Friendly Water Bottle",
      "Handle": "eco-water-bottle",
      "Product ID": 1002
    }
  }
]
```

We will build that array in the workflow in the next step.

The finished request looks like this:

![The completed Airtable HTTP request](https://cdn-dev.eu.codecreationlabs.cloud/crm-tool/uploads/SjJ78gKLnLlocopCui3Cs62dJbGZ7F1n/45b9a16c93e3de68.png)

> [!TIP]
> Use the **Test** button before saving. It shows the resolved URL and body alongside the live response, which catches a wrong header or a malformed body immediately. Untested variables are filled with placeholder values automatically.

Save the request. It is now selectable in the **HTTP Request** action in Shopify Flow.

## Step 3: Build the workflow

Open **Shopify Flow** and create a new workflow.

Choose a schedule-based trigger. This example runs every 10 minutes for demonstration purposes - pick something sensible for production.

![A scheduled trigger in Shopify Flow](https://cdn-dev.eu.codecreationlabs.cloud/crm-tool/uploads/SjJ78gKLnLlocopCui3Cs62dJbGZ7F1n/c407147a74a6a0f2.png)

Fetch the products that are not yet in Airtable. Products already sent get the tag `added-to-airtable` in the final step, so this query excludes them. See Shopify's [query argument reference](https://shopify.dev/docs/api/admin-graphql/latest/queries/products#arguments-query).

![Fetching untagged products in Shopify Flow](https://cdn-dev.eu.codecreationlabs.cloud/crm-tool/uploads/SjJ78gKLnLlocopCui3Cs62dJbGZ7F1n/ac067087fffc969c.png)

> [!TIP]
> Add a condition here so the HTTP request only runs when there is at least one product to send. It saves an action against your plan on every empty cycle.

Now format the data. The request body expects a `records` variable, so use Flow's **Run code** action to shape the fetched products into that array.

![Adding the Run code action](https://cdn-dev.eu.codecreationlabs.cloud/crm-tool/uploads/SjJ78gKLnLlocopCui3Cs62dJbGZ7F1n/3c20c18d65bd01e3.png)

Configure the step like this:

![Configuring the Run code step](https://cdn-dev.eu.codecreationlabs.cloud/crm-tool/uploads/SjJ78gKLnLlocopCui3Cs62dJbGZ7F1n/a5d43f86a035e1df.png)

The query:

```
query {
  getProductData {
    id
    handle
    title
  }
}
```

The code:

```
export default function main(input) {
  const records = input.getProductData.map((data) => {
    const numericId = parseInt(data.id.replace("gid://shopify/Product/", ""));
    return {
      fields: {
        "Product ID": numericId,
        "Title": data.title,
        "Handle": data.handle
      }
    };
  });

  return {
    records: JSON.stringify(records)
  };
}
```

The output schema:

```
"The output of Run Code"
type Output {
  "The message returned by the script"
  records: String
}
```

You can preview the result on the right:

![Previewing the Run code output](https://cdn-dev.eu.codecreationlabs.cloud/crm-tool/uploads/SjJ78gKLnLlocopCui3Cs62dJbGZ7F1n/1573a77a308aa677.png)

Add the **HTTP Request** action provided by this app.

![Adding the HTTP Request action](https://cdn-dev.eu.codecreationlabs.cloud/crm-tool/uploads/SjJ78gKLnLlocopCui3Cs62dJbGZ7F1n/ec68ecd0599d9aec.png)

When prompted, select the request you saved - **Create Airtable product record** - then save and go back to the workflow.

![Selecting the saved HTTP request](https://cdn-dev.eu.codecreationlabs.cloud/crm-tool/uploads/SjJ78gKLnLlocopCui3Cs62dJbGZ7F1n/751e8fb1153f2299.png)

In the action's variables field, pass the records you just formatted:

```
{
  "records": "{{runCode.records}}"
}
```

![Passing the formatted records into the request](https://cdn-dev.eu.codecreationlabs.cloud/crm-tool/uploads/SjJ78gKLnLlocopCui3Cs62dJbGZ7F1n/8fe2ed5eb26d4b1e.png)

Finally, iterate over the fetched products and tag them `added-to-airtable`, so the next run does not pick them up again.

![Tagging the products after sending](https://cdn-dev.eu.codecreationlabs.cloud/crm-tool/uploads/SjJ78gKLnLlocopCui3Cs62dJbGZ7F1n/d4c70cc4d937ce69.png)

Save and turn on the workflow.

## Step 4: Confirm it worked

In **History** you can see whether the request succeeded and what Airtable sent back.

![The History list showing the request](https://cdn-dev.eu.codecreationlabs.cloud/crm-tool/uploads/SjJ78gKLnLlocopCui3Cs62dJbGZ7F1n/a3d6920b903732d5.png)

Open the entry to inspect the request data, the response data and the timing.

![The request detail view](https://cdn-dev.eu.codecreationlabs.cloud/crm-tool/uploads/SjJ78gKLnLlocopCui3Cs62dJbGZ7F1n/3214136f331dddf7.png)

From the request's edit page, the **Request history** button jumps straight to History filtered to that one request.

## What the app will not let you call

Merchant-configured requests go through a safety guard. Requests to internal or private network addresses are refused, including `localhost`, cloud metadata endpoints, and anything resolving to a private range - even via a redirect. If you see an error mentioning `SSRF blocked`, the destination is on that list. Requests also follow at most 5 redirects and cap the response at 10 MB.

Shopify Flow itself caps an action response at 50 KB. If a test response is larger, the app blocks saving rather than letting the step fail inside a live workflow.

## Best practices

- Use clear request names so they are easy to find in Shopify Flow.
- Store API tokens as secrets rather than typing them into headers.
- Keep the body focused on the fields the destination actually needs.
- Check **History** after any change, to confirm the request still succeeds.
- Add a condition before the request so it does not run on empty cycles.

## Related

- [Secrets](https://docs.workflow-transactional-email.app/secrets.md) - storing credentials.
- [Variables and Liquid](https://docs.workflow-transactional-email.app/variables-and-liquid.md) - the variable reference, including body override.
- [History and troubleshooting](https://docs.workflow-transactional-email.app/history-and-troubleshooting.md) - reading a failed request.
