GuidesOFFICE ADD-INS

Office add-in CORS errors

An Office add-in task pane is a web page in a webview, so fetch() carries the origin of the page you host (https://localhost:3000 in the default dev setup) and fails against APIs that send no Access-Control-Allow-Origin. Microsoft points to CORS on the API or a server-side proxy, and Office Scripts need APIs that answer Access-Control-Allow-Origin: *, because their runtime origin can change. https://proxy.cors.dev/ covers public GET requests in both, since it echoes whichever origin calls it (free, about 600 requests a minute per IP); POST and Authorization from a task pane need cors.dev Pro at $5 a month for 500,000 requests.

Why an add-in is subject to CORS

A request that works in Postman can fail in a task pane, because Postman is not a browser and Office add-ins run in one. Microsoft's "Addressing same-origin policy limitations in Office Add-ins": "Because Office Add-ins are hosted in a webview control, the same-origin policy applies to script running in their web pages as well." When the API on another domain sends no Access-Control-Allow-Origin, the webview blocks your JavaScript from reading the response.

Microsoft describes four techniques:

  • Token-based authorization: Server-side code that uses OAuth 2.0 flows to reach resources on another domain.
  • CORS: The API sends Access-Control-Allow-Origin for your add-in's origin.
  • iframe and postMessage: Cross-window messaging with a page loaded in an iframe from another domain.
  • JSONP: A legacy technique, in Microsoft's words, with "higher security risk than modern alternatives". For new development it says to "prefer CORS or a server-side proxy."

The origin your add-in sends

The task pane is the web page at the SourceLocation URL in your add-in manifest, and it must use HTTPS. Office on the web loads it in an iframe that runs with the HTML5 sandbox attribute, Microsoft 365 on Windows uses WebView2 (Microsoft Edge, Chromium), and Office on Mac and iPad uses WKWebView (Safari). In every client, requests carry the origin of the server that hosts that page.

Add-in projects created with the Yeoman generator for Office Add-ins serve the task pane from https://localhost:3000 with a development certificate from office-addin-dev-certs. During development, every API you call sees Origin: https://localhost:3000.

Where Office add-in code runs, checked October 2026
ClientRuntimeOrigin an API sees
Office on the webiframe with the HTML5 sandbox attributeYour SourceLocation origin
Microsoft 365 on WindowsWebView2, Microsoft EdgeYour SourceLocation origin
Office on Mac and iPadWKWebView, SafariYour SourceLocation origin
Local developmentThe client's runtimehttps://localhost:3000
Office ScriptsExcel's script runtimeNot documented, may change

AppDomains in the manifest lists domains the add-in may navigate to inside its window. It adds no CORS headers to anyone's API and does not fix a CORS error.

Load API data into an Excel range

Check the API first. On 28 October 2026 we called four endpoints with Origin: https://localhost:3000. Microsoft Graph answered its OPTIONS preflight with 200 and Access-Control-Allow-Origin: *, and a GET without a token answered 401 with the same header, so call Graph directly. The GitHub REST API also sends *.

The ECB daily euro rates XML (https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml, 1,547 bytes) and Yahoo Finance's chart API (https://query1.finance.yahoo.com/v8/finance/chart/MSFT?range=1mo&interval=1d, 3,553 bytes) sent no Access-Control-Allow-Origin. Through https://proxy.cors.dev/ both answered 200 with the header echoing https://localhost:3000. The free tier needs no signup and no key.

APIs called from a task pane on https://localhost:3000, checked October 2026
APIAccess-Control-Allow-OriginWhat to do
Microsoft Graph*Call it directly
GitHub REST API*Call it directly
ECB euro rates XMLNoneUse the proxy, 200
Yahoo Finance chartNoneUse the proxy, 200
  1. Fetch through the proxy

    Put https://proxy.cors.dev/ in front of the ECB URL and parse the text/xml response with DOMParser.

  2. Build rows

    Microsoft's docs say to set range.values with a two-dimensional array, even for a single cell, where each inner array is one row. On 28 October 2026 the file held 29 currencies, so a header plus 29 rates fills A1:B30 (USD 1.1133, JPY 175.86 that day).

  3. Write the range

    Assign the array inside Excel.run and call context.sync().

taskpane.js: ECB euro rates into a worksheet range
const FEED = 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml';

export async function loadEuroRates() {
  const response = await fetch('https://proxy.cors.dev/' + FEED);
  if (!response.ok) {
    throw new Error(response.headers.get('X-Cors-Error') ?? `HTTP ${response.status}`);
  }

  const xml = new DOMParser().parseFromString(await response.text(), 'text/xml');
  const rows = [...xml.querySelectorAll('Cube[currency]')].map((cube) => [
    cube.getAttribute('currency'),
    Number(cube.getAttribute('rate')),
  ]);

  await Excel.run(async (context) => {
    const sheet = context.workbook.worksheets.getActiveWorksheet();
    sheet.getRange(`A1:B${rows.length + 1}`).values = [['Currency', 'Per 1 EUR'], ...rows];
    await context.sync();
  });
}

The free tier sends GET and HEAD only and allows about 600 requests per minute per IP, with a readable Retry-After on 429. It refuses Authorization, and header names containing token, apikey, api-key, api_key, secret or password or ending in -key, with 400 header_not_allowed; names containing csrf or xsrf, such as X-CSRF-Token, are dropped and listed in X-Cors-Dropped-Headers like other custom headers. When the proxy refuses a request, the X-Cors-Error response header names the reason.

Office Scripts and fetch

Office Scripts, Excel's TypeScript automation, can call fetch() with limits from Microsoft's "External API call support in Office Scripts". Calls work only when the script runs in the Excel application, not through Power Automate "under normal circumstances", and not for scripts stored on a SharePoint site. Microsoft adds: "There is no way to sign in or use OAuth2 type of authentication flows." and "There is no infrastructure to store API credentials and keys."

The CORS rule is stricter than for add-ins: "make sure that the Access-Control-Allow-Origin header of that external resource uses the * directive. If the Access-Control-Allow-Origin header uses a specific <origin> directive, then your fetch call from Office Scripts may fail. The origin of the Office Scripts runtime may change without notice."

The free tier echoes whatever origin a request carries and answers * when there is none, so a script that calls an API through https://proxy.cors.dev/ keeps matching when the runtime origin changes. On 28 October 2026 the Yahoo Finance request below returned 22 daily MSFT closes, written as 22 rows of date and price.

Office Script: a month of MSFT closing prices
interface ChartResponse {
  chart: { result: { timestamp: number[]; indicators: { quote: { close: number[] }[] } }[] };
}

async function main(workbook: ExcelScript.Workbook): Promise<void> {
  const target = 'https://query1.finance.yahoo.com/v8/finance/chart/MSFT?range=1mo&interval=1d';
  const response = await fetch('https://proxy.cors.dev/' + target);
  if (!response.ok) {
    throw new Error(`Proxy answered HTTP ${response.status}`);
  }

  const data: ChartResponse = await response.json();
  const { timestamp, indicators } = data.chart.result[0];
  const rows = timestamp.map((seconds, i) => [
    new Date(seconds * 1000).toISOString().slice(0, 10),
    indicators.quote[0].close[i],
  ]);

  workbook.getActiveWorksheet().getRange(`A1:B${rows.length}`).setValues(rows);
}

When you need cors.dev Pro

A task pane that sends POST, PUT, PATCH or DELETE, a body up to 1 MiB, or Authorization and custom headers needs cors.dev Pro. It fits add-ins where each user enters their own API key in the task pane. A key your company owns belongs on your server: cors.dev never hides a key, and anything in the task pane is visible to the user.

cors.dev Pro costs $5 a month for 500,000 requests, with a 7-day trial of 1,000 requests and no card. Throughput is 3,000 requests a minute per account and 100 in flight; the 6 MiB response cap and 30 second upstream deadline stay the same. An X-Cors-Cache header caches a GET for up to 300 seconds when the upstream response allows it; cache hits still count as requests.

  1. Create a Connection

    Sign up with Google and add up to 10 exact page origins and up to 20 target hosts.

  2. Register both origins

    The origin of your production SourceLocation, for example https://addin.contoso.com, and https://localhost:3000.

  3. Send the key

    Add the publishable key in an X-Cors-Key header. An unlisted origin answers 403 origin_not_allowed.

Pro needs every exact origin registered, so it is a poor fit for Office Scripts, whose origin can change without notice.

Good questions.

Does adding a domain to AppDomains fix a CORS error?

No. AppDomains tells Office which domains the add-in may navigate to inside its window. It adds no Access-Control-Allow-Origin header to any API response, so fetch() still fails until the API sends CORS headers or the request goes through a proxy.

Can an Office Script call an API when it runs from Power Automate?

No. Microsoft says external calls work only when the script runs in the Excel application, and not for scripts stored on a SharePoint site. Run through Power Automate, a script with fetch() fails with "Runtime error: Line X: fetch is not defined".

Should I use JSONP in an Office add-in?

Only to maintain an old integration. Microsoft calls JSONP a legacy technique with higher security risk than modern alternatives and says new development should prefer CORS or a server-side proxy.

Which origins do I register for an add-in on cors.dev Pro?

The exact origin of the SourceLocation URL in your production manifest, for example https://addin.contoso.com, and https://localhost:3000 for local development. Requests from any other origin answer 403 origin_not_allowed.

Does Microsoft Graph need a CORS proxy?

No. On 28 October 2026 Graph answered an OPTIONS preflight from https://localhost:3000 with 200 and Access-Control-Allow-Origin: *, and sent the same header on a 401 response to a GET without a token. Call it directly with your access token.