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.
| Fact | Value |
|---|---|
| Tool | Retool |
| Difficulty | Advanced |
| Time required | 20-25 min |
| Compatibility | Retool Cloud and Self-hosted |
| Last updated | March 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
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.
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 present6// on the requested resource.78// Also look for:9// 'has been blocked by CORS policy: Response to preflight10// request doesn't pass access control check'11// This means the OPTIONS preflight request also failedExpected 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.
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.
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.
1// BEFORE: fetch() in JS Query — CORS error2try {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 CORS10}1112// AFTER: Resource query — no CORS issue13// Create a Resource Query with:14// Resource: example_api (the REST resource you created)15// Method: GET16// URL: /v1/users (relative to resource base URL)17// Headers: (auth configured in resource settings)1819// Then trigger from JS Query if needed:20await getUsers.trigger(); // Runs server-side21console.log(getUsers.data); // No CORSExpected result: The resource query fetches the API data successfully without CORS errors. The response is available in queryName.data.
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.
1// Resource query configuration:2// Resource: stripe_api3// Method: POST4// URL: /v1/payment_intents5// 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}1617// Dynamic URL path (for GET /v1/customers/{id}):18// URL: /v1/customers/{{ table1.selectedRow.data.stripe_id }}1920// Query params (for GET /v1/invoices?customer=cus_xxx&limit=10):21// URL: /v1/invoices22// Params: customer={{ table1.selectedRow.data.stripe_id }}, limit=10Expected result: The resource query sends a POST request with the dynamic body values. The response is available in the query's .data property.
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.
1// AWS S3 CORS configuration (JSON format)2// Add via AWS Console: S3 Bucket → Permissions → CORS configuration3[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": 300013 }14]1516// 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.
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.
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}1314// APIs that typically support CORS (safe for fetch()):15// Public REST APIs with Access-Control-Allow-Origin: *16// APIs specifically designed for browser use1718// APIs that typically do NOT support CORS (use resource query):19// Payment APIs (Stripe, PayPal)20// Internal company APIs21// Most B2B SaaS APIs22// Any API not explicitly designed for browser requestsExpected 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
1// === SETUP: Create REST Resource in Settings → Resources ===2// Name: my_api3// Base URL: https://api.example.com4// Auth: Bearer Token → {{ retoolContext.configVars.MY_API_TOKEN }}56// === Resource Query: getApiData ===7// Resource: my_api8// Method: GET9// URL: /v1/records10// Params: 11// status: {{ statusFilter.value }}12// page: {{ table1.currentPage || 1 }}13// per_page: 5014// Run: Manual (triggered from JS Query)1516// === Resource Query: createApiRecord ===17// Resource: my_api18// Method: POST19// URL: /v1/records20// Body (JSON):21// {22// "name": {{ nameInput.value }},23// "email": {{ emailInput.value }},24// "metadata": { "created_by": {{ current_user.email }} }25// }26// Run: Manual2728// === JS Query: apiWorkflow ===29// This orchestrates the resource queries above30await isLoading.setValue(true);3132try {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 response42 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.
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()?
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation