Half the integrations I’ve written started the same way: an internal web app does something useful, there’s no API documentation, and I need a .NET service to do the same thing. The fastest way to find out exactly what the browser sends is DevTools. Right-click the request in the Network tab, choose Copy → Copy as cURL, and you have the URL, method, every header and the body in one command.
Turning that command into C# is where time disappears. This guide covers the workflow I use and the mistakes I kept making before I automated it with the cURL to C# converter.
Copy the right flavour
On Windows, Chrome and Edge offer two options: “Copy as cURL (cmd)” and “Copy as cURL (bash)”. They escape things completely differently. The cmd version is full of ^" because ^ is cmd’s escape character:
curl ^"https://api.example.com/v1/search^" ^
-H ^"accept: */*^" ^
-H ^"content-type: application/json^" ^
--data-raw ^"^{^\^"query^\^":^\^"usb hub^\^",^\^"limit^\^":10^}^"
Most converters only understand bash syntax and fail on that. The ByteKiln converter reads bash, cmd and PowerShell (curl.exe with backtick continuations) and detects which one you pasted. It still pays to know the difference: if a conversion looks garbled, the input style is the first thing to check, and you can set it manually.
What the C# looks like
Take this request, copied from a dashboard that creates invoices:
curl 'https://api.example.com/v1/invoices?status=open' \
-H 'accept: application/json' \
-H 'authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiI0MiJ9.sig' \
-H 'content-type: application/json' \
--data-raw '{"customerId":42,"lines":[{"sku":"KB-104","qty":2}]}' \
--compressed
The converter produces:
public static class ApiClient
{
// Create one HttpClient and reuse it for the life of the app — a new HttpClient per request
// exhausts sockets under load. In ASP.NET Core, inject IHttpClientFactory instead.
private static readonly HttpClient Client = new(new HttpClientHandler
{
AutomaticDecompression = DecompressionMethods.All,
});
public static async Task<string> SendAsync()
{
// Note: curl without -L does not follow redirects; HttpClient follows them by default.
using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.example.com/v1/invoices?status=open");
request.Headers.TryAddWithoutValidation("accept", "application/json");
request.Headers.TryAddWithoutValidation("authorization", "Bearer REDACTED");
request.Content = new StringContent("{\"customerId\":42,\"lines\":[{\"sku\":\"KB-104\",\"qty\":2}]}");
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
using var response = await Client.SendAsync(request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
Every line there reflects something that has bitten me at least once.
The HttpClient traps
One shared client. new HttpClient() inside a method looks harmless and works fine in testing. Under load, each disposed client leaves its socket in TIME_WAIT, and eventually you run out. The generated code keeps a single static client and creates a new HttpRequestMessage per call. Inside ASP.NET Core, go one step further and register a typed client with IHttpClientFactory, which also handles DNS changes.
Content-Type lives on the content. In .NET, Content-Type, Content-Length, Content-Encoding and friends are content headers. request.Headers.Add("Content-Type", ...) throws “Misused header name” — one of the most-searched HttpClient errors there is. The converter moves those headers onto request.Content. If the curl command sets a Content-Type but sends no body, the header is dropped with a comment explaining why, instead of generating code that throws.
TryAddWithoutValidation. Browsers send headers such as sec-ch-ua: "Chromium";v="128", "Not;A=Brand";v="24" that fail .NET’s strict header parsing. TryAddWithoutValidation sends them exactly as captured. You’ll probably delete most of those browser headers anyway — APIs rarely need them — but the code compiles and runs as-is first.
Compression. DevTools copies add --compressed. If you instead set Accept-Encoding: gzip by hand without enabling decompression, the server sends gzip and ReadAsStringAsync returns binary garbage. The converter turns on AutomaticDecompression and leaves out the manual header, since the handler adds it.
Cookies. A request copied from a logged-in session often carries a Cookie header. HttpClientHandler has its own cookie container, and while UseCookies is true it manages the Cookie header itself. When the curl command has cookies, the generated handler sets UseCookies = false so the header you copied is the one that gets sent.
Redirects. curl doesn’t follow redirects unless you pass -L; HttpClient does by default. Usually that’s what you want, but when an API answers with a 302 to a login page, it explains why you’re parsing HTML instead of JSON.
Forms, uploads and files
-d bodies default to application/x-www-form-urlencoded, which the converter emits as FormUrlEncodedContent with key-value pairs — including duplicate keys, which a Dictionary would silently drop. -F becomes MultipartFormDataContent. If the copied command carries Content-Type: multipart/form-data; boundary=----WebKitFormBoundary..., that header is removed, because hard-coding the browser’s boundary guarantees a malformed body.
File references like -F file=@invoice.pdf or -d @body.json are kept as paths the generated code opens when it runs. ByteKiln can’t read files on your disk, and it doesn’t pretend to.
-k (skip certificate validation) is the one flag I never want translated into working code. It’s included as a commented-out line with a warning, so turning it on is a deliberate decision, not a paste accident.
Anything with no equivalent — --retry, --proxy, client certificates — is listed under the output as ignored rather than silently dropped.
Don’t paste tokens into shared places
A request copied from a logged-in browser contains your credentials: the Authorization bearer token, session cookies, CSRF tokens, sometimes an api_key in the query string. The converter runs in your browser, and nothing is sent to a server — but the output tends to end up in a pull request, a Slack thread, or a ticket.
That’s why “Redact credentials” is on by default. Authorization and cookie values, API-key headers, basic-auth passwords, token-like query parameters and password fields in JSON or form bodies become REDACTED in the generated code. The Share link is always built from a redacted curl command reconstructed from the parsed request, never from your original paste. If you turn redaction off to get runnable code with real values, sharing is disabled. If you need to see what’s inside a bearer token before replacing it with a proper token flow, the JWT decoder shows the claims locally.
Other languages
The same parsed request generates JavaScript fetch, Node.js axios, Python requests, PHP cURL and Go net/http. Each target has its own rules. Browsers silently drop Cookie, Host, User-Agent and sec-* headers from fetch, so the fetch output lists the ones it can’t send. Python moves cookies into a cookies dict and JSON bodies into json=. Go omits Accept-Encoding so the transport can decompress transparently.
From response to model
Once the request works, the next step is usually a model for the response. Paste a real response body into JSON to C# and you’ll get classes you can deserialize into — the JSON to C# guide covers the edge cases there.
My loop now is: reproduce the action in the browser, Copy as cURL, paste it into the cURL converter, delete the browser-only headers, swap the copied token for real authentication, and move the code into a typed client. It takes about five minutes, and I haven’t debugged a “Misused header name” exception since.