# How to Handle Cross-Origin Requests in Retool

- Tool: Retool
- Difficulty: Advanced
- Time required: 20-25 min
- Compatibility: Retool Cloud and Self-hosted
- Last updated: March 2026

## TL;DR

CORS errors in Retool JS Queries happen because fetch() runs in a sandboxed browser iframe, and browsers block cross-origin requests without proper CORS headers. The fix: create a Retool REST API resource for the API and use a resource query instead of fetch(). Resource queries run server-side on Retool's backend, completely bypassing browser CORS restrictions.

## Why CORS Happens in Retool and How to Fix It

CORS (Cross-Origin Resource Sharing) is a browser security policy that restricts web pages from making HTTP requests to domains different from the page's own origin. Retool apps run as browser-based web applications, so when a JS Query uses fetch() to call an external API, the browser enforces CORS.

Retool itself runs at a specific origin (e.g., your-company.retool.com). When your JS Query calls a third-party API (e.g., api.example.com), the browser requires the API to include Access-Control-Allow-Origin headers permitting Retool's origin. APIs that do not include these headers return CORS errors.

The solution Retool provides is resource queries — queries that execute on Retool's backend server, not in the user's browser. A resource query to a REST API resource bypasses CORS entirely because the request originates from the server, not the browser. This tutorial covers the complete CORS troubleshooting and resolution workflow.

## Before you start

- A Retool account with an app experiencing CORS errors
- Admin access to create resources in Retool Settings
- The external API's base URL and authentication details
- Basic understanding of HTTP headers and REST APIs

## Step-by-step guide

### 1. Identify the CORS error in the browser console

Open browser DevTools (F12) and switch to the Console tab. CORS errors appear as red messages containing 'Access to fetch at X from origin Y has been blocked by CORS policy'. Also check the Network tab — the failed request will show a status of (failed) with type 'fetch'. Note the exact URL being fetched and the origin reported in the error — you will need both to configure the fix.

```
// Typical CORS error message in browser console:
// Access to fetch at 'https://api.example.com/v1/data'
// from origin 'https://your-company.retool.com'
// has been blocked by CORS policy:
// No 'Access-Control-Allow-Origin' header is present
// on the requested resource.

// Also look for:
// 'has been blocked by CORS policy: Response to preflight
// request doesn't pass access control check'
// This means the OPTIONS preflight request also failed
```

**Expected result:** You have identified the exact URL causing the CORS error and confirmed it is a browser-side CORS block, not a server authentication issue.

### 2. Create a Retool REST API resource for the external API

In Retool, go to Settings → Resources. Click + Create resource and select REST API. Enter the base URL of the external API (e.g., https://api.example.com). Configure authentication in the resource settings (Bearer token, Basic auth, API key header, etc.). Save the resource. All credentials stored here are injected server-side and never sent to the browser.

**Expected result:** A new REST API resource is created in Settings. It can now be selected when creating queries in any app.

### 3. Replace fetch() with a resource query

Instead of using fetch() in a JS Query, create a Resource Query in the Code panel that uses the REST API resource you just created. Resource queries run on Retool's backend server, not in the user's browser. CORS is completely bypassed because the request originates from the server. Configure the HTTP method, path, headers, and body in the query's Inspector settings.

```
// BEFORE: fetch() in JS Query — CORS error
try {
  const response = await fetch('https://api.example.com/v1/users', {
    headers: { 'Authorization': 'Bearer my-api-key' }
  });
  const data = await response.json();
  return data;
} catch (err) {
  console.error('CORS error:', err); // Fails with CORS
}

// AFTER: Resource query — no CORS issue
// Create a Resource Query with:
//   Resource: example_api (the REST resource you created)
//   Method: GET
//   URL: /v1/users  (relative to resource base URL)
//   Headers: (auth configured in resource settings)

// Then trigger from JS Query if needed:
await getUsers.trigger(); // Runs server-side
console.log(getUsers.data); // No CORS
```

**Expected result:** The resource query fetches the API data successfully without CORS errors. The response is available in queryName.data.

### 4. Handle dynamic URLs and request bodies in resource queries

REST API resource queries support {{ }} bindings in the URL path, query parameters, and request body — the same parameterization as SQL queries. Use these to pass dynamic values from components or JS variables into the request. For POST requests, configure the body in the query's Body tab using JSON with {{ }} references.

```
// Resource query configuration:
// Resource: stripe_api
// Method: POST
// URL: /v1/payment_intents
// Body (JSON):
{
  "amount": {{ amountInput.value * 100 }},
  "currency": {{ currencySelect.value || 'usd' }},
  "customer": {{ table1.selectedRow.data.stripe_customer_id }},
  "description": {{ descriptionInput.value }},
  "metadata": {
    "order_id": {{ orderIdInput.value }},
    "created_by": {{ current_user.email }}
  }
}

// Dynamic URL path (for GET /v1/customers/{id}):
// URL: /v1/customers/{{ table1.selectedRow.data.stripe_id }}

// Query params (for GET /v1/invoices?customer=cus_xxx&limit=10):
// URL: /v1/invoices
// Params: customer={{ table1.selectedRow.data.stripe_id }}, limit=10
```

**Expected result:** The resource query sends a POST request with the dynamic body values. The response is available in the query's .data property.

### 5. Configure CORS on an S3 bucket for Retool file access

If you use AWS S3 for file storage and access files directly from Retool, the S3 bucket needs a CORS configuration that allows requests from your Retool origin. This is necessary for direct browser downloads or pre-signed URL access. Add the CORS configuration to your S3 bucket via the AWS Console or CLI.

```
// AWS S3 CORS configuration (JSON format)
// Add via AWS Console: S3 Bucket → Permissions → CORS configuration
[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
    "AllowedOrigins": [
      "https://your-company.retool.com",
      "https://your-custom-retool-domain.com"
    ],
    "ExposeHeaders": ["ETag", "x-amz-server-side-encryption"],
    "MaxAgeSeconds": 3000
  }
]

// For self-hosted Retool, also allow localhost:
// "AllowedOrigins": ["https://retool.company.com", "http://localhost:3000"]
```

**Expected result:** S3 file downloads and uploads from Retool work without CORS errors. The bucket allows requests from your specific Retool domain.

### 6. When direct fetch() is safe to use in Retool

Direct fetch() in JS Queries is safe when the target API explicitly supports CORS by returning the correct Access-Control-Allow-Origin header including your Retool origin. Some public APIs (like OpenWeatherMap, certain public datasets) are configured to accept requests from any origin. You can verify this by checking the API's CORS documentation or testing with curl --head to see the response headers.

```
// Check if an API supports CORS before using fetch():
// Run this in a JS Query to test:
try {
  const response = await fetch('https://api.example.com/health', {
    method: 'HEAD'
  });
  console.log('CORS headers:');
  console.log('Allow-Origin:', response.headers.get('Access-Control-Allow-Origin'));
  console.log('Allow-Methods:', response.headers.get('Access-Control-Allow-Methods'));
} catch (err) {
  console.log('CORS not supported — use resource query instead:', err.message);
}

// APIs that typically support CORS (safe for fetch()):
// Public REST APIs with Access-Control-Allow-Origin: *
// APIs specifically designed for browser use

// APIs that typically do NOT support CORS (use resource query):
// Payment APIs (Stripe, PayPal)
// Internal company APIs
// Most B2B SaaS APIs
// Any API not explicitly designed for browser requests
```

**Expected result:** You know whether direct fetch() is viable or whether you need to use a Retool resource query for the specific API.

## Complete code example

File: `REST Resource Query + JS Query: proxyApiCall`

```javascript
// === SETUP: Create REST Resource in Settings → Resources ===
// Name: my_api
// Base URL: https://api.example.com
// Auth: Bearer Token → {{ retoolContext.configVars.MY_API_TOKEN }}

// === Resource Query: getApiData ===
// Resource: my_api
// Method: GET
// URL: /v1/records
// Params: 
//   status: {{ statusFilter.value }}
//   page: {{ table1.currentPage || 1 }}
//   per_page: 50
// Run: Manual (triggered from JS Query)

// === Resource Query: createApiRecord ===
// Resource: my_api
// Method: POST
// URL: /v1/records
// Body (JSON):
// {
//   "name": {{ nameInput.value }},
//   "email": {{ emailInput.value }},
//   "metadata": { "created_by": {{ current_user.email }} }
// }
// Run: Manual

// === JS Query: apiWorkflow ===
// This orchestrates the resource queries above
await isLoading.setValue(true);

try {
  // Fetch data via server-side resource query (no CORS)
  await getApiData.trigger();
  console.log('API data:', getApiData.data?.length, 'records');
  
  if (!getApiData.data?.records) {
    throw new Error('Unexpected API response format');
  }
  
  // Transform the response
  const processed = getApiData.data.records.map(r => ({
    id: r.id,
    name: r.attributes.name,
    email: r.attributes.email,
    status: r.attributes.status,
    created: new Date(r.attributes.created_at).toLocaleDateString()
  }));
  
  await apiRecords.setValue(processed);
  return processed;
  
} catch (err) {
  console.error('API fetch failed:', err);
  utils.showNotification({
    title: 'Failed to load data',
    description: err.message,
    notificationType: 'error'
  });
} finally {
  await isLoading.setValue(false);
}
```

## Common mistakes

- **Using fetch() in JS Queries and getting CORS errors, then trying to fix it by changing request headers** — undefined Fix: You cannot fix CORS errors by changing request headers from the client side. The fix is server-side: either configure the external API to allow your Retool origin, or use a Retool resource query which runs server-side.
- **Creating a resource query but forgetting to switch from fetch() to using the resource query** — undefined Fix: After creating the REST API resource, create a Resource Query (not a JS Query) in the Code panel that uses that resource. Delete or disable the fetch() call in your JS Query and instead call await resourceQuery.trigger().
- **Configuring S3 CORS with AllowedOrigins: ['*'] in production** — undefined Fix: Wildcard origins allow any website to make authenticated requests to your S3 bucket with pre-signed URLs. Always specify exact origins like your Retool domain.
- **Resource query returns network error even though the URL is correct** — undefined Fix: For self-hosted Retool, check that the Retool backend server has network access to the external API. Firewall rules, VPC peering, and DNS resolution issues affect server-side resource queries, not CORS.

## Best practices

- Default to using Retool REST API resource queries for all external API calls — they run server-side and are CORS-immune by design.
- Store API keys in Retool resource settings or Configuration Variables — never hardcode them in queries or JS Query fetch() calls.
- Test for CORS support before using direct fetch() in JS Queries — check the API's documentation for explicit browser/CORS support.
- For S3 CORS, always specify exact AllowedOrigins (your Retool domain) rather than the wildcard '*'.
- If you control the external API, add the Access-Control-Allow-Origin header for your Retool domain as the correct server-side fix.
- For self-hosted Retool, verify that your Retool backend server can reach the external API (firewall, VPC routing) — resource queries fail if the server cannot make the outbound request.
- Use preflight caching (MaxAgeSeconds in S3 CORS) to reduce OPTIONS preflight requests and improve performance.

## Frequently asked questions

### Why do I get CORS errors in Retool when the same API call works in Postman?

Postman makes requests from a desktop application, not a browser. CORS is a browser security policy — it only applies to requests made from web pages. Postman bypasses CORS entirely. In Retool, JS Query fetch() calls run in the browser's sandboxed context and are subject to CORS. Use a Retool resource query to make the call server-side, which works like Postman.

### Can I disable CORS checking in Retool?

No — CORS is enforced by the browser and cannot be disabled in a web application. The correct solutions are: (1) use Retool resource queries which run server-side and bypass CORS, or (2) configure the external API's server to include the correct Access-Control-Allow-Origin headers for your Retool domain.

### Do Retool resource queries also bypass rate limits and IP-based restrictions?

Resource queries run from Retool's backend servers (for Retool Cloud) or your self-hosted server. All requests from your org's Retool instance share the same outbound IP. If the external API has IP allowlisting, you need to allowlist Retool's server IP(s). For Retool Cloud, check Retool's documentation for the current outbound IP ranges. For self-hosted, use your server's IP.

---

Source: https://www.rapidevelopers.com/retool-tutorial/how-to-handle-cross-origin-requests-in-retool
© RapidDev — https://www.rapidevelopers.com/retool-tutorial/how-to-handle-cross-origin-requests-in-retool
