A curl command is the lingua franca of API documentation and bug reports, but sooner or later it has to become real code. Translating it by hand means knowing which flags matter, which are terminal noise, and where fetch or requests quietly behave differently than curl. This page explains what the converter does with your command and where the traps sit.
From curl command to running code
The converter reads your input in two stages, the same way your shell and curl would. First a shell tokenizer splits the command into arguments: single and double quotes, backslash escapes, $'…' ANSI quoting, and the backslash-newline continuations that make pasted multi-line commands work. The Windows variants are covered too, so a Copy as cURL (cmd) command with caret escapes parses without manual cleanup.
Then the arguments go through curl's own option grammar: bundled short flags like -sSL, values attached directly as in -XPOST, and the --option=value spelling. Out of that comes one request model, and all five targets render from it. Switching tabs re-renders instantly because nothing is parsed twice.
The part we consider non-negotiable: flags the converter cannot translate are named, not swallowed. An unknown --frobnicate produces a warning in the findings strip, -s and -v are listed as terminal-only flags that change nothing about the request, and a -b cookies.txt pointing at a cookie jar file tells you plainly that code cannot load one. Converters that drop what they do not understand produce requests that differ from the original in ways you find out at 2 a.m.
Everything runs in this browser tab. That matters more here than on most tool pages, because a copied curl command routinely contains a live session cookie or an Authorization header; ours never crosses the network.
What each target gets right
The goal is code a reviewer would wave through, not a string-by-string transliteration of the command line.
- fetch (browser). When the body is JSON, you get
JSON.stringifyover a real object literal instead of an escaped string, so the payload is editable the way you would have written it. Headers the browser refuses to let scripts set are handled instead of copied blindly:Cookieis on the forbidden header list, so the code carries a comment pointing atcredentials: 'include', and aRefererheader becomes the supportedreferreroption.-uturns into anAuthorizationheader built withbtoa. - Node.js. The same fetch, native since Node 18, without the browser restrictions:
Cookieheaders pass through,-d @payload.jsonbecomesreadFile, and a multipart file part usesopenAsBlobfromnode:fsso the upload streams instead of loading into memory. - Python. requests has idiomatic parameters for almost every curl flag, and the converter uses them:
json=for JSON bodies (dropping the now-redundant Content-Type header),data=as a dict for form bodies,params=for-Gquery data,auth=as a tuple for-u,cookies=as a dict,files=withopen(…, "rb")for-F. Python literals come out as Python:truebecomesTrue,nullbecomesNone. - PHP. Plain
curl_setopt_arraywith no Composer dependency, because PHP's curl extension is literally libcurl:-xmaps straight toCURLOPT_PROXY,--compressedtoCURLOPT_ENCODING, file parts toCURLFile. - HTTPie. The bonus direction for people who want a shorter command line back: JSON fields become
key=valueandkey:=rawpairs, query parameters use==, and-ubecomes-a.
Dangerous flags stay visible. -k becomes verify=False or CURLOPT_SSL_VERIFYPEER => false with a warning comment attached, and in browser fetch, where disabling TLS verification is simply impossible, the code says so instead of inventing an option that does not exist.
Copy as cURL, straight from devtools
The most common source of a curl command today is not a terminal but the browser. In Chrome: Network tab, right-click a request, Copy → Copy as cURL. Firefox has the same menu entry. What you get is a faithful replay of the request the page made, and it is the fastest way to move a misbehaving API call into code you can iterate on: paste it here, pick a target, and start editing the object literal instead of a quoted string.
Two things about the copied command are worth knowing. First, it is long. Chrome includes every header the browser sent: user-agent, accept-language, the whole family of sec-ch-* client hints and sec-fetch-* metadata. Most of them are irrelevant to reproducing the request, and pruning the generated headers object down to the two or three that matter is usually the first edit. Second, it is a credential. The Cookie header carries your session, an authorization header carries your token. The sample button above loads a realistic Chrome-style command so you can see the shape without pasting your own.
On Windows, Chrome's Copy as cURL (cmd) variant quotes with ^" carets and continues lines with ^. The tokenizer reads that dialect too, so there is no need to hunt for the bash variant hidden in the submenu.
How the common flags map
| curl | fetch | Python requests | PHP curl |
|---|---|---|---|
-X PUT | method: 'PUT' | requests.put(…) | CURLOPT_CUSTOMREQUEST |
-H 'Name: value' | headers object | headers= dict | CURLOPT_HTTPHEADER |
-d 'a=1' | body: new URLSearchParams(…) | data= dict | http_build_query(…) |
--json '…' | body: JSON.stringify(…) | json= | raw string + header |
-F 'f=@x.jpg' | FormData | files= | CURLFile |
-u user:pass | Authorization + btoa | auth=(user, pass) | CURLOPT_USERPWD |
-b 'k=v' | comment (browser) / header (Node) | cookies= dict | CURLOPT_COOKIE |
-G -d 'q=x' | URLSearchParams in the URL | params= | http_build_query in the URL |
-L | default, noted | default, noted | CURLOPT_FOLLOWLOCATION |
--compressed | automatic, noted | automatic, noted | CURLOPT_ENCODING => '' |
--max-time 5 | AbortSignal.timeout(5000) | timeout=5 | CURLOPT_TIMEOUT |
-k | impossible in browsers, comment | verify=False + warning | CURLOPT_SSL_VERIFYPEER + warning |
-I | method: 'HEAD', prints headers | requests.head(…) | CURLOPT_NOBODY |
The full flag list is three hundred entries long; the curl manpage documents them all. The converter covers the ones that shape an HTTP request. Anything outside that set lands in the findings strip by name, so you always know what was left behind.
The four kinds of request body
Most conversion mistakes are body mistakes, because curl expresses four different body formats through flags that look alike.
Form-urlencoded is what a bare -d 'user=ada&plan=pro' sends, with Content-Type: application/x-www-form-urlencoded set implicitly. Several -d flags concatenate with &. The converter decodes the pairs and hands them to the mechanism each target encodes with: URLSearchParams, a data= dict, http_build_query. --data-urlencode is the variant that encodes for you, so --data-urlencode 'q=hello world' arrives as q=hello%20world; the generated code keeps the decoded value and lets the runtime re-encode it, which is the same bytes on the wire without the double-encoding trap.
JSON is form-urlencoded's flag twin and its Content-Type opposite. -d '{"a":1}' alone sends JSON bytes labelled as a form; only the -H 'Content-Type: application/json' next to it, or the --json shortcut, makes it JSON to the server. When the converter sees the JSON Content-Type and a body that parses, it upgrades the output: an object literal with JSON.stringify in JavaScript, json= in Python. If the body does not parse as JSON despite the header, that becomes a warning instead of a silent pass-through.
Multipart comes from -F and is the only format that carries files with names and MIME types. Each target builds it with its native mechanism, and in every one of them the boundary is generated at runtime, which is why the converter deliberately drops a copied Content-Type: multipart/form-data; boundary=… header and says so: replaying the old boundary with a new body is a request no server can parse.
Raw file bodies are --data-binary @dump.bin and -T file.bin. The generated code reads the same path at runtime where the runtime can (readFile in Node, open in Python, file_get_contents in PHP); browser fetch gets an honest placeholder, because a browser cannot read a local path. One byte-level detail survives translation: plain -d @file strips CR and LF from the file, --data-binary does not, and the code comments say which one you had.
If the request you are converting is a webhook replay, the webhook payload viewer is the companion tool for inspecting the body you are about to send.
Where fetch and requests differ from curl
A correct translation is not a mirror image, because the runtimes have different defaults. These are the ones that bite:
- Redirects. curl stops at the first 301 unless you pass
-L; fetch and requests follow up to their limits by default. The converter notes this rather than adding a flag, but it means a curl command without-Land its translation can see different responses from the same URL. - Timeouts. curl waits forever unless told otherwise, and so does requests, which has no default timeout at all. fetch inherits browser-level limits of around 300 seconds. When your command has
--max-time, it is translated; when it does not, adding one to the requests code is our standing recommendation anyway. - Decompression.
--compressedis an opt-in for curl and a built-in for fetch and requests. Response bodies in the generated code are already decompressed text. - Forbidden headers. Browser fetch refuses to set
Cookie,Host,Referer,Originand theSec-*family no matter what the headers object says: the browser drops them silently. The converter is one of the few that knows the list and routes around it where a supported option exists. - Authentication.
-uin curl negotiates; the translation pins Basic, which is nothing more than base64 ofuser:passin anAuthorizationheader. If the token in your command is a JWT you want to inspect, the JWT decoder reads it without sending it anywhere. - TLS verification. Every runtime verifies certificates by default, like curl. The difference is how loudly you can turn it off:
verify=Falseper request in Python, process-wide only in Node's fetch, not at all in browsers. Where the command had-k, the generated code carries the risk in a comment instead of hiding it.
We converted a few hundred of these by hand before building the tool, and the habit that stuck is to read the findings strip first, then the comments in the code. The lines that did not translate cleanly are exactly the lines that made the original command interesting.
Porting a curl command
What HTTP method does curl use by default?
GET. Three flags change it implicitly: any -d or --data variant switches to POST, -I switches to HEAD, and -T uploads with PUT. -X overrides everything, so curl -X DELETE sends DELETE even with a -d body. A surprising consequence: curl -X GET -d "a=1" really sends a GET with a body, which many servers and proxies drop or reject, so if you meant a query string use -G instead of -X GET.
Does curl -d imply POST?
Yes. The moment any --data flag appears, curl switches from GET to POST and sets Content-Type: application/x-www-form-urlencoded unless a -H header overrides it. Several -d flags on one command are joined with & into a single body, so -d "a=1" -d "b=2" sends a=1&b=2. Adding -G reverses the implication: the same data moves into the URL as a query string and the method stays GET.
How do I copy a request as a curl command from Chrome DevTools?
Open the Network tab, right-click the request, then Copy → Copy as cURL. Firefox has the same entry, and on Windows Chrome offers a second variant, Copy as cURL (cmd), which quotes with carets instead of backslashes. The copied command contains everything the browser sent: the Cookie header with your session, Authorization tokens, and the full set of sec-ch-* client-hint headers. Treat it like a credential, because whoever runs it is you as far as the server can tell.
What is the difference between --data, --data-raw and --data-binary?
They differ only in how they treat @ and newlines. --data (-d) reads @file as "take the body from this file" and strips CR and LF characters from it, a legacy of posting form fields. --data-binary also reads @file but keeps every byte intact, which is what you want for file payloads. --data-raw never interprets @ at all, so it is the safe flag for JSON bodies that might legitimately start with an @ sign. For a literal body without an @, all three send the same bytes.
How do I send a JSON body with curl?
Two ways. The classic spelling is two flags: curl -d '{"name":"ada"}' -H "Content-Type: application/json". Since curl 7.82 there is a shortcut: curl --json '{"name":"ada"}' sets Content-Type: application/json and Accept: application/json in one go and implies POST. Without the Content-Type header the server receives the JSON labelled as application/x-www-form-urlencoded, which is the cause of a large share of "works in Postman, fails in curl" reports.
What is the difference between curl -F and curl -d?
-d sends application/x-www-form-urlencoded, one flat string of key=value pairs, the same format an HTML form without files submits. -F sends multipart/form-data: each field becomes its own part with headers, a generated boundary separates them, and a part can be a file with its own filename and MIME type. Use -F whenever a file is involved or the receiving API documents multipart; use -d for plain field data, where it is smaller and simpler to debug.
How do I upload a file with curl?
Pick the flag by what the server expects. -F "file=@photo.jpg" sends multipart/form-data, the format upload endpoints in web frameworks usually parse; add ;type=image/jpeg inside the quotes to set the part's MIME type. --data-binary @backup.tar.gz puts the raw bytes directly into the request body, which suits APIs that read the body as the file. -T backup.tar.gz does the same but with PUT and is the classic spelling for WebDAV and S3-style endpoints.
Why can't fetch set a Cookie header in the browser?
Cookie is on the Fetch specification's forbidden header list, together with Host, Referer, Content-Length and others: browsers silently drop attempts to set them through fetch or XMLHttpRequest. The browser attaches its own cookie jar instead, controlled by the credentials option ("same-origin" by default, "include" for cross-site requests). In Node the same fetch call may set a Cookie header freely, because there is no browser cookie jar to protect, which is why the browser and Node outputs of a converter legitimately differ.
Does fetch follow redirects like curl -L?
fetch follows redirects by default, up to 20 of them, so -L needs no translation at all. It is curl that is the odd one out: without -L a curl request answers with the 301 or 302 itself and stops. Python requests also follows by default with one exception, HEAD requests, where you must pass allow_redirects=True explicitly. PHP's curl extension inherits curl's behaviour and needs CURLOPT_FOLLOWLOCATION set to true.
What is the requests equivalent of curl --compressed?
Nothing, and that is the point: requests always sends Accept-Encoding: gzip, deflate and decompresses the response transparently, so response.text is already plain text. Install the brotli package and br joins the list. fetch behaves the same for gzip and br in every browser and in Node. Only PHP's curl extension needs an explicit opt-in: CURLOPT_ENCODING set to an empty string offers every encoding the local libcurl supports and decodes the answer.
How do I set a timeout in fetch like curl --max-time?
Pass an abort signal: fetch(url, { signal: AbortSignal.timeout(5000) }) cancels the request after five seconds with a TimeoutError, matching curl --max-time 5. AbortSignal.timeout has been in every major browser since 2022 and in Node since 17.3. In Python, requests has no default timeout at all: a hung server hangs your program forever unless you pass timeout=5 on every call, which is the most consequential difference between a pasted curl command and its requests translation.
Is it safe to paste a curl command with tokens into an online converter?
Assume not, unless the tool says otherwise and you have a reason to believe it. A copied curl command routinely carries a session cookie or a bearer token, and most online converters submit your input to their server to build the code, where it can be logged. This converter parses and generates entirely in your browser tab; the command never leaves it, which you can confirm by loading the page, going offline, and converting anyway.