Every API reference has a curl example, and almost every integration starts by turning one into code. It is mechanical work with a few traps: options whose meaning is not obvious, defaults that differ between curl and your HTTP library, and a real API key that ends up in your shell history, your repository or a chat window. This article walks through one realistic example with our curl converter, then covers the traps. All keys below are fictional.
The example
A typical "create a payment" example in the style of many payment APIs:
curl https://api.example-pay.com/v1/payment_intents \
-u "sk_test_FAKE123:" \
-d amount=2000 \
-d currency=usd \
-d "automatic_payment_methods[enabled]"=trueRead it as curl does:
- The URL is the only argument that is not an option.
- -u "sk_test_FAKE123:" is HTTP Basic authentication with the key as the user name and an empty password (that is what the trailing colon means; without it curl would prompt for a password).
- Three -d options are joined with & into one body: amount=2000¤cy=usd&automatic_payment_methods[enabled]=true.
- -d without -X makes it a POST, and curl adds Content-Type: application/x-www-form-urlencoded.
In Python requests the converter produces:
import requests
url = "https://api.example-pay.com/v1/payment_intents"
headers = {
"Content-Type": "application/x-www-form-urlencoded",
}
data = {
"amount": "2000",
"currency": "usd",
"automatic_payment_methods[enabled]": "true",
}
response = requests.post(
url,
headers=headers,
data=data,
auth=("sk_test_FAKE123", ""),
allow_redirects=False,
)
print(response.status_code)
print(response.text)Every line maps to part of the command. The allow_redirects=False is there because curl does not follow redirects unless you pass -L; requests does by default. Most APIs never redirect, but when one does (an http URL moving to https, for example) the difference shows up as a confusing 301 in one tool and success in the other.
Details that change behaviour
- -d vs --data-raw vs --data-binary. -d treats a leading @ as "read this file" and strips newlines from files; --data-raw sends @ literally; --data-binary sends the file byte for byte. The converter keeps file references as file references and tells you so.
- --data-urlencode encodes the value (a space becomes +, é becomes %C3%A9). "q=café & bar" becomes q=caf%C3%A9+%26+bar. Plain -d does not encode anything.
- -G moves all the data into the query string and keeps the method as GET: curl -G URL -d limit=10 requests URL?limit=10.
- --json (curl 7.82 and later) is shorthand for a JSON body plus Content-Type and Accept headers. With plain -d '{…}', curl still says application/x-www-form-urlencoded unless you add the header yourself; many examples in documentation forget, and some servers then refuse the body.
- -k disables certificate checking. It belongs in a test against a local server, never in code that ships.
- Browsers are stricter than curl. fetch in a browser cannot set Cookie, User-Agent or Referer and is subject to CORS. Code that works in Node can fail in the browser for these reasons alone.
Keys in your shell history
The command above, typed with a real key, is now in ~/.bash_history or ~/.zsh_history, in plain text, possibly synced to backups. Options to avoid that:
- Use a variable: export the key once, read with read -s so it is not echoed, then use "$API_KEY" in commands. The history then contains the variable name, not the value. The converter leaves $API_KEY as text and lists it, so your code can read it from the environment too.
- Keep commands out of history: in bash, with HISTCONTROL=ignorespace (or ignoreboth), a command that starts with a space is not saved; in zsh, setopt HIST_IGNORE_SPACE does the same.
- Put headers in a file: since curl 7.55, -H @headers.txt reads headers from a file, which you can keep out of version control.
- Remember the process list: while curl runs, its arguments are visible to other users of the same machine through ps. On a shared server, a file is safer than an argument.
Keys in code and conversations
Generated code is easy to paste: into a repository, an issue, a chat with a colleague or an assistant. The converter flags requests that contain an Authorization header, a -u password, a cookie or a key-like query parameter. Before you save the code:
- Replace the literal key with an environment lookup: os.environ["API_KEY"] in Python, process.env.API_KEY in Node, os.Getenv("API_KEY") in Go.
- Add the file that holds the real value (.env, a secrets manager) to .gitignore.
- If a real key was ever pasted somewhere public, rotate it. Deleting the message or the commit does not un-leak it; automated scanners look for keys in public repositories.
The converter itself never sends the command anywhere: it is parsed in the page, which is why it is safe to paste a command with a test key while you work.
Common mistakes
- Forgetting the empty password in -u "key:" and getting a password prompt or a 401.
- Sending JSON with -d but no Content-Type, then wondering why the server says the body is empty.
- Assuming -d encodes values. Only --data-urlencode does.
- Copying -k into production code because "it worked with curl".
- Testing in Node, deploying in the browser, where forbidden headers are silently dropped and CORS applies.
- Leaving a real key in the example in your own README or wiki. Use an obviously fake value like sk_test_FAKE123.
Once the request works, the JSON formatter makes the response readable.