Skip to main content
RapidDev - Software Development Agency
retool-tutorial

How to Handle Cross-Origin Requests in Retool

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.

What you'll learn

  • Why JavaScript fetch() in Retool JS Queries triggers CORS errors from the browser
  • How to use Retool REST API resource queries as server-side proxies to bypass CORS
  • How to configure an external API to accept requests from Retool's origin
  • How to set up CORS headers for S3 buckets used in Retool apps
  • When direct fetch() is safe to use and when you need the resource proxy approach
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced9 min read20-25 minRetool Cloud and Self-hostedLast updated March 2026RapidDev Engineering Team
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.

Quick facts about this guide
FactValue
ToolRetool
DifficultyAdvanced
Time required20-25 min
CompatibilityRetool Cloud and Self-hosted
Last updatedMarch 2026

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.

Prerequisites

  • 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.

typescript
1// Typical CORS error message in browser console:
2// Access to fetch at 'https://api.example.com/v1/data'
3// from origin 'https://your-company.retool.com'
4// has been blocked by CORS policy:
5// No 'Access-Control-Allow-Origin' header is present
6// on the requested resource.
7
8// Also look for:
9// 'has been blocked by CORS policy: Response to preflight
10// request doesn't pass access control check'
11// 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.

typescript
1// BEFORE: fetch() in JS Query — CORS error
2try {
3 const response = await fetch('https://api.example.com/v1/users', {
4 headers: { 'Authorization': 'Bearer my-api-key' }
5 });
6 const data = await response.json();
7 return data;
8} catch (err) {
9 console.error('CORS error:', err); // Fails with CORS
10}
11
12// AFTER: Resource query — no CORS issue
13// Create a Resource Query with:
14// Resource: example_api (the REST resource you created)
15// Method: GET
16// URL: /v1/users (relative to resource base URL)
17// Headers: (auth configured in resource settings)
18
19// Then trigger from JS Query if needed:
20await getUsers.trigger(); // Runs server-side
21console.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.

typescript
1// Resource query configuration:
2// Resource: stripe_api
3// Method: POST
4// URL: /v1/payment_intents
5// Body (JSON):
6{
7 "amount": {{ amountInput.value * 100 }},
8 "currency": {{ currencySelect.value || 'usd' }},
9 "customer": {{ table1.selectedRow.data.stripe_customer_id }},
10 "description": {{ descriptionInput.value }},
11 "metadata": {
12 "order_id": {{ orderIdInput.value }},
13 "created_by": {{ current_user.email }}
14 }
15}
16
17// Dynamic URL path (for GET /v1/customers/{id}):
18// URL: /v1/customers/{{ table1.selectedRow.data.stripe_id }}
19
20// Query params (for GET /v1/invoices?customer=cus_xxx&limit=10):
21// URL: /v1/invoices
22// 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.

typescript
1// AWS S3 CORS configuration (JSON format)
2// Add via AWS Console: S3 Bucket → Permissions → CORS configuration
3[
4 {
5 "AllowedHeaders": ["*"],
6 "AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
7 "AllowedOrigins": [
8 "https://your-company.retool.com",
9 "https://your-custom-retool-domain.com"
10 ],
11 "ExposeHeaders": ["ETag", "x-amz-server-side-encryption"],
12 "MaxAgeSeconds": 3000
13 }
14]
15
16// For self-hosted Retool, also allow localhost:
17// "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.

typescript
1// Check if an API supports CORS before using fetch():
2// Run this in a JS Query to test:
3try {
4 const response = await fetch('https://api.example.com/health', {
5 method: 'HEAD'
6 });
7 console.log('CORS headers:');
8 console.log('Allow-Origin:', response.headers.get('Access-Control-Allow-Origin'));
9 console.log('Allow-Methods:', response.headers.get('Access-Control-Allow-Methods'));
10} catch (err) {
11 console.log('CORS not supported — use resource query instead:', err.message);
12}
13
14// APIs that typically support CORS (safe for fetch()):
15// Public REST APIs with Access-Control-Allow-Origin: *
16// APIs specifically designed for browser use
17
18// APIs that typically do NOT support CORS (use resource query):
19// Payment APIs (Stripe, PayPal)
20// Internal company APIs
21// Most B2B SaaS APIs
22// 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 working example

REST Resource Query + JS Query: proxyApiCall
1// === SETUP: Create REST Resource in Settings → Resources ===
2// Name: my_api
3// Base URL: https://api.example.com
4// Auth: Bearer Token → {{ retoolContext.configVars.MY_API_TOKEN }}
5
6// === Resource Query: getApiData ===
7// Resource: my_api
8// Method: GET
9// URL: /v1/records
10// Params:
11// status: {{ statusFilter.value }}
12// page: {{ table1.currentPage || 1 }}
13// per_page: 50
14// Run: Manual (triggered from JS Query)
15
16// === Resource Query: createApiRecord ===
17// Resource: my_api
18// Method: POST
19// URL: /v1/records
20// Body (JSON):
21// {
22// "name": {{ nameInput.value }},
23// "email": {{ emailInput.value }},
24// "metadata": { "created_by": {{ current_user.email }} }
25// }
26// Run: Manual
27
28// === JS Query: apiWorkflow ===
29// This orchestrates the resource queries above
30await isLoading.setValue(true);
31
32try {
33 // Fetch data via server-side resource query (no CORS)
34 await getApiData.trigger();
35 console.log('API data:', getApiData.data?.length, 'records');
36
37 if (!getApiData.data?.records) {
38 throw new Error('Unexpected API response format');
39 }
40
41 // Transform the response
42 const processed = getApiData.data.records.map(r => ({
43 id: r.id,
44 name: r.attributes.name,
45 email: r.attributes.email,
46 status: r.attributes.status,
47 created: new Date(r.attributes.created_at).toLocaleDateString()
48 }));
49
50 await apiRecords.setValue(processed);
51 return processed;
52
53} catch (err) {
54 console.error('API fetch failed:', err);
55 utils.showNotification({
56 title: 'Failed to load data',
57 description: err.message,
58 notificationType: 'error'
59 });
60} finally {
61 await isLoading.setValue(false);
62}

Common mistakes when handling Cross-Origin Requests in Retool

Why it's a problem: Using fetch() in JS Queries and getting CORS errors, then trying to fix it by changing request headers

How to avoid: 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.

Why it's a problem: Creating a resource query but forgetting to switch from fetch() to using the resource query

How to avoid: 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().

Why it's a problem: Configuring S3 CORS with AllowedOrigins: ['*'] in production

How to avoid: 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.

Why it's a problem: Resource query returns network error even though the URL is correct

How to avoid: 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.

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I have a Retool JS Query that uses fetch() to call an external REST API at api.example.com, but it fails with a CORS error: 'Access to fetch has been blocked by CORS policy'. I cannot modify the external API's CORS headers. How do I: (1) create a Retool REST API resource to proxy the call server-side, (2) configure authentication for the resource using a secret configuration variable, (3) create a resource query that calls the same endpoint, and (4) trigger the resource query from my JS Query instead of using fetch()?

Retool Prompt

My Retool app needs to call the HubSpot CRM API to create contacts. Direct fetch() fails with CORS. Help me: (1) create a REST API resource in Retool Settings with base URL 'https://api.hubapi.com' and a Bearer token from a configuration variable called HUBSPOT_API_KEY, (2) create a resource query to POST to /crm/v3/objects/contacts with a JSON body using {{ nameInput.value }} and {{ emailInput.value }}, and (3) write a JS Query that triggers this resource query with error handling.

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Learning is great. Shipping is faster with help.

Our engineers have built 600+ apps on Retool and the tools around it. If your project needs to be live sooner than your learning curve allows — book a free consultation.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.