GuidesJSONP

JSONP vs CORS: how JSONP works and how to replace it

JSONP loads data from another origin through a <script> tag: you pass a callback name, the server wraps the JSON in a call to that function, and because scripts may load from anywhere, no CORS check happens. CORS replaced it. Of 16 public APIs we tested on 26 October 2026, 12 still answer a callback, but 14 send CORS headers, so fetch() works for most of them and a proxy covers the rest.

How JSONP works

The page adds <script src="https://api.example.com/items?callback=handle">. The server answers JavaScript instead of JSON, handle({...}), and the browser runs it, which calls your function with the data.

Express 5.3.0 shows the server side. With ?callback=handle, res.jsonp() sends Content-Type: text/javascript, X-Content-Type-Options: nosniff and the body /**/ typeof handle === 'function' && handle([...]);. Without the parameter it sends plain JSON. It strips unsafe characters from the name, so callback=alert(document.cookie)// became alertdocument.cookie. The Express source calls the /**/ prefix a mitigation for "Rosetta Flash JSONP abuse"; X, Wikipedia, Stack Exchange, GitHub, Vimeo and Gravatar start their answers with it too.

JSONP vs CORS: what JSONP can't do

JSONP behaved the same in Chromium 153, Firefox 155 and WebKit 26.6:

  • GET only: a script tag cannot send POST, PUT or DELETE.
  • No request headers: no Authorization header and no custom headers.
  • No status codes: a 404 or 500 fires only the script error event and the callback never runs. Chromium and WebKit print the status in the console, but your code cannot read it, and jQuery 4.0.0 reported a 500 as status 404.
  • No timeout: a request that never answered fired no event after 5,000 ms. jQuery 4.0.0 never called back without a timeout option; with timeout: 2000 it failed with status 0 after about 2,001 ms.
  • Full page access: the answer runs as your page's own JavaScript. A malicious JSONP answer we served read localStorage in all 3 engines.
  • CSP: the JSONP host must be allowed in script-src. With script-src 'self' 'unsafe-inline' the script was blocked in all 3 engines.

With CORS, fetch() can use any method and headers, reads the status code, and gets the body as data instead of running it.

Why browsers block JSON loaded as a script

When an API drops JSONP or ignores your callback, it answers plain JSON, and a script tag cannot use that:

  • With nosniff: application/json plus X-Content-Type-Options: nosniff was blocked in all 3 engines. Chromium fails with net::ERR_BLOCKED_BY_ORB, Firefox logs was blocked due to MIME type ("application/json") mismatch (X-Content-Type-Options: nosniff), and WebKit logs Refused to execute ... as script because "X-Content-Type-Options: nosniff" was given.
  • Without nosniff: Chromium still fails with net::ERR_BLOCKED_BY_ORB, and Firefox blocks it with the warning "A resource is blocked by OpaqueResponseBlocking". WebKit runs the JSON, throws "Unexpected token ':'", and the callback never runs.

We hit this with Open Library, which ignores callback and answers plain JSON. Our loader got the error event after about 1 second in Chromium and Firefox; in WebKit only the 2,000 ms timeout ended the call. A real JSONP body served as application/json still ran in all 3 engines, unless nosniff was set.

Which APIs still support JSONP

We sent each API a callback request and a plain JSON request. 12 of 16 still answer a callback, some with their own parameter: jsoncallback on Flickr, output=jsonp on Deezer, format=jsonp on ipify. YouTube oEmbed, Open Library, Spotify oEmbed and ipinfo.io ignore it. 14 of 16 send CORS headers on the plain JSON URL, Wikipedia and Wikidata only with origin=*; Flickr and Deezer send none.

JSONP callback support and CORS headers on 16 public APIs, tested with a callback request and a plain JSON request, checked October 2026
APICallback parameterJSONP answerCORS on the JSON URLUse instead
X (Twitter) oEmbedcallbackYes, application/javascriptEchoes your originfetch()
Flickr feeds and oEmbedjsoncallbackYes, application/javascriptNoneProxy, nojsoncallback=1 for feeds
Wikipedia and Wikidata api.phpcallbackYes, text/javascriptOnly with origin=*fetch() with origin=*
Deezer APIoutput=jsonp plus callbackYes, text/javascriptNoneProxy with output=json
iTunes Search APIcallbackYes, text/javascript*fetch()
SoundCloud oEmbedcallback with format=jsYes, text/javascript*fetch() with format=json
Vimeo oEmbedcallbackYes, application/javascript*fetch()
Gravatar profile JSONcallbackYes, text/javascript*fetch()
Stack Exchange APIcallbackYes, application/javascript*fetch()
GitHub REST APIcallbackYes, wrapped in meta and data*fetch()
ipifyformat=jsonp plus callbackYes, application/javascript*fetch() with format=json
JSONPlaceholdercallbackYes, Express res.jsonp formatEchoes your originfetch()
YouTube oEmbedcallbackNo, plain JSONEchoes your originfetch()
Open Library searchcallbackNo, plain JSON*fetch()
Spotify oEmbedcallbackNo, plain JSON*fetch()
ipinfo.iocallbackName ignored, console.log wrapper*fetch()

Replace JSONP with fetch()

Pick the route by what the API sends:

  • CORS headers: drop the callback, ask for JSON (format=json on SoundCloud and ipify, origin=* on the Wikipedia API) and call fetch(). Most oEmbed providers work this way.
  • No CORS headers, GET returning text: ask for JSON (nojsoncallback=1 on Flickr feeds, output=json on Deezer) and prefix https://proxy.cors.dev/. Both returned 200 with CORS headers through the free CORS proxy, which needs no signup or key and relays text up to 1 MiB, with a 10-second deadline and 600 requests per minute per IP.
  • POST, custom headers or Authorization: use Pro ($5 per month for 500,000 requests) or your own backend.

For jQuery's dataType: 'jsonp', see the jQuery AJAX guide. The snippet below calls iTunes directly and Flickr through the proxy; it ran in all 3 engines.

Replace JSONP with fetch(), directly or through the proxy
// iTunes Search sends Access-Control-Allow-Origin: *, so plain fetch() works
const itunes = await fetch('https://itunes.apple.com/search?term=radiohead&entity=album&limit=3');
const albums = (await itunes.json()).results;

// Flickr sends no CORS headers, so we go through the proxy and ask for plain JSON
const proxy = 'https://proxy.cors.dev/';
const flickr = await fetch(
  `${proxy}https://www.flickr.com/services/feeds/photos_public.gne?format=json&nojsoncallback=1&tags=cat`,
  { credentials: 'omit' },
);
if (!flickr.ok) throw new Error(flickr.headers.get('X-Cors-Error') ?? `HTTP ${flickr.status}`);
const photos = (await flickr.json()).items;

console.table([
  ...albums.map((album) => ({ source: 'iTunes', title: album.collectionName })),
  ...photos.slice(0, 3).map((photo) => ({ source: 'Flickr', title: photo.title })),
]);

If you must keep JSONP for an old API, use a loader with a timeout that removes the script tag and the global callback. This one returned the Flickr feed in all 3 engines, rejected a 404 within 10 ms, and rejected a hanging request when its timeout ran out.

Minimal JSONP loader with timeout and cleanup, for legacy APIs only
function jsonp(url, { param = 'callback', timeout = 10000 } = {}) {
  return new Promise((resolve, reject) => {
    const name = `jsonp_${Math.random().toString(36).slice(2)}`;
    const script = document.createElement('script');

    const cleanup = () => {
      clearTimeout(timer);
      script.remove();
      delete window[name];
    };

    const timer = setTimeout(() => {
      cleanup();
      // A late answer still calls the global, so we leave a no-op that removes itself
      window[name] = () => delete window[name];
      reject(new Error(`JSONP: no answer after ${timeout} ms`));
    }, timeout);

    window[name] = (data) => {
      cleanup();
      resolve(data);
    };

    script.onerror = () => {
      cleanup();
      reject(new Error('JSONP: script failed to load, no status code available'));
    };

    const target = new URL(url);
    target.searchParams.set(param, name);
    script.src = target.href;
    document.head.append(script);
  });
}

// Flickr's public feed sends no CORS headers but still answers jsoncallback
const feed = await jsonp('https://www.flickr.com/services/feeds/photos_public.gne?format=json&tags=cat', {
  param: 'jsoncallback',
});
console.table(feed.items.slice(0, 5).map((item) => ({ title: item.title, taken: item.date_taken })));

Server side: res.jsonp versus CORS headers

On your own server, send CORS headers instead of JSONP; the Express CORS guide covers the options. With cors({ origin: 'https://app.example.com' }), Express sent Access-Control-Allow-Origin: https://app.example.com and Vary: Origin, and answered the preflight with 204 and Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE.

Any page on any site can load a JSONP URL and read the answer, so a JSONP endpoint must only serve public data. With CORS, the server decides which origins may read the response.

Express: res.jsonp() versus the cors middleware
import express from 'express';
import cors from 'cors';

const app = express();
const items = [{ id: 1, name: 'first' }, { id: 2, name: 'second' }];

// Legacy JSONP: GET /legacy/items?callback=handle answers text/javascript
app.get('/legacy/items', (req, res) => res.jsonp(items));

// CORS: fetch() from the allowed origin gets any method, the status code and the body
app.use('/api', cors({ origin: 'https://app.example.com' }));
app.get('/api/items', (req, res) => res.json(items));

app.listen(3000);

Good questions.

What is the difference between JSONP and CORS?

JSONP loads data as a script with a callback, so it works without CORS headers but is limited to GET and runs the server's code in your page. CORS is a set of response headers that let fetch() read another origin's response as data, with any method, headers and status codes.

Is JSONP still used?

Less and less. 12 of the 16 public APIs we tested on 26 October 2026 still answer a callback, but 14 of the 16 also send CORS headers, so fetch() is the better choice for nearly all of them.

Why does my JSONP request fail with ERR_BLOCKED_BY_ORB?

The server answered plain JSON instead of JavaScript, usually because the API dropped JSONP or ignores your callback parameter. Chromium blocks JSON loaded as a script with net::ERR_BLOCKED_BY_ORB and Firefox blocks it too, so call the URL with fetch() instead.

Is JSONP safe?

Only as safe as the server you load it from: the answer runs as JavaScript in your page with full access, and our test answer read localStorage. On the server side, any site can read a JSONP endpoint, so it must never return private data.

Can JSONP send POST requests or headers?

No. A script tag only makes GET requests and cannot send custom headers or an Authorization header.

How do I replace JSONP with fetch()?

Remove the callback parameter, ask for JSON, and call the URL with fetch(). If the API sends no CORS headers, like Flickr or Deezer, prefix https://proxy.cors.dev/ for public GET requests or call it from your backend.