Get started
Introduction
libcurl is a free, thread-safe, client-side URL transfer library written in C. It is the engine inside the curl command, and it speaks HTTP, HTTPS, FTP, SMTP and many other protocols through one consistent API.
Include and link
Add the header to your source, and link against the library when you build:
#include <curl/curl.h>
cc program.c -lcurl -o program
Two interfaces
libcurl offers two ways to drive transfers:
- The easy interface is synchronous and straightforward: one handle, one transfer at a time. It is where almost everyone starts.
- The multi interface drives many transfers concurrently in a single thread, for high throughput or event loops.
This reference covers the easy interface first, then the options you will set most, then error handling and the multi interface.
libcurl is widely deployed and stable. Code written against the easy interface years ago still compiles and runs today, which is part of why so many languages build their HTTP support on top of it.
Get started
Your first transfer
A complete program that fetches a URL and writes the response to standard output.
#include <curl/curl.h>
#include <stdio.h>
int main(void) {
CURL *curl = curl_easy_init();
if (curl) {
curl_easy_setopt(curl, CURLOPT_URL, "https://example.com");
CURLcode res = curl_easy_perform(curl);
if (res != CURLE_OK)
fprintf(stderr, "error: %s\n", curl_easy_strerror(res));
curl_easy_cleanup(curl);
}
return 0;
}
The three steps
Every easy-interface transfer follows the same shape:
- Create a handle with
curl_easy_init(). - Configure it with one or more
curl_easy_setopt() calls. - Perform the transfer with
curl_easy_perform(), then clean up.
By default the response body is written to standard output, which is why this minimal program already prints a page. The next pages show how to capture the body yourself and set the options you actually need.
Always check the return of curl_easy_perform. A transfer can fail for many reasons outside your control, and ignoring the CURLcode hides them.
Get started
Global init and cleanup
libcurl has a one-time global setup step for parts of the environment, such as the SSL and networking libraries it uses, that are not safe to initialise lazily from multiple threads.
The pattern
curl_global_init(CURL_GLOBAL_DEFAULT);
/* ... create handles and run transfers ... */
curl_global_cleanup();
Call curl_global_init() once, early, from a single thread before you start any transfers, and call curl_global_cleanup() once at the very end.
Why it matters
curl_easy_init() will trigger the global init for you if you have not called it. That is fine for a small single-threaded program, but in a threaded application the lazy path can race. Calling it yourself, explicitly, avoids that.
CURL_GLOBAL_DEFAULT initialises everything libcurl normally needs, including SSL. It is the right flag unless you have a specific reason to choose otherwise.
Easy interface
curl_easy_init and cleanup
The easy handle is the object that holds all the settings for a transfer.
Create
CURL *curl = curl_easy_init();
if (!curl) { /* out of memory or init failure */ }
It returns a CURL *, or NULL on failure.
Reuse
A handle can be used for many transfers. Reusing one is not just convenient; it lets libcurl keep the connection alive and reuse it, which is much faster than a fresh handle per request.
To reuse a handle for an unrelated transfer, reset it first:
curl_easy_reset(curl); /* back to defaults, keeps connections */
Destroy
curl_easy_cleanup(curl);
This frees the handle and closes any connections it owned. Do not use the handle after cleaning it up.
If you need many similar handles, curl_easy_duphandle() copies an existing one with its options already set.
Easy interface
curl_easy_setopt
You configure a transfer by setting options on the handle, one call per option.
curl_easy_setopt(handle, CURLOPT_URL, "https://example.com");
curl_easy_setopt(handle, CURLOPT_TIMEOUT, 30L);
curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 1L);
Argument types matter
The option name dictates the type of the third argument, and passing the wrong type is the most common libcurl bug:
- String options take a
char *, for example CURLOPT_URL. - Long options take a
long. Write the L suffix, as in 30L, so the right type is passed. - Callback options take a function pointer, paired with a data-pointer option.
- Offset options take a
curl_off_t for large sizes.
When options apply
Options are remembered on the handle and take effect on the next curl_easy_perform(). Set them in any order before you perform.
The option reference is large, but you will use a small core set day to day. The following pages cover the ones almost every program needs.
Easy interface
curl_easy_perform
curl_easy_perform() runs the transfer you have configured and blocks until it finishes.
CURLcode res = curl_easy_perform(curl);
What it does
It carries out the whole transfer: resolving the host, connecting, doing any TLS handshake, sending the request, and delivering the response to your write callback, or to standard output.
The return value
It returns a CURLcode. CURLE_OK, which is zero, means success. Any other value is an error you should handle. See the error-handling pages for the common codes.
It blocks
Because the call blocks until the transfer completes, a slow server holds up your thread. For a responsive program, either run the easy interface on its own thread, or switch to the multi interface, which does not block.
After a transfer you can read information about it, such as the HTTP response code, with curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code).
Common options
Receiving the response body
By default libcurl writes the response body to standard output. To capture it in your program, provide a write callback.
The callback
size_t write_cb(char *ptr, size_t size, size_t nmemb, void *userdata) {
size_t total = size * nmemb;
/* append `total` bytes starting at `ptr` to your own buffer */
return total; /* tell libcurl how many bytes you handled */
}
libcurl calls this function repeatedly as data arrives, in chunks. The real number of bytes is size * nmemb.
Wire it up
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &my_buffer);
CURLOPT_WRITEDATA is the pointer passed to your callback as userdata, so you can append to a struct, a file, or a growing buffer.
Signalling an error
Return a value different from total to tell libcurl something went wrong. It aborts the transfer with CURLE_WRITE_ERROR.
A common pattern is a small struct holding a char * and a length, grown with realloc inside the callback. That gives you the whole body in memory when the transfer finishes.
Common options
Following redirects
libcurl does not follow HTTP redirects unless you ask it to. A 301 or 302 is delivered to you as-is by default.
Turn it on
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
Now libcurl follows the Location header automatically to the final destination.
Guard against loops
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 10L);
This caps the number of hops, so a misbehaving server cannot send libcurl round in circles. Exceeding the cap fails with CURLE_TOO_MANY_REDIRECTS.
By default libcurl keeps the same method across a redirect except where the HTTP spec says otherwise. CURLOPT_POSTREDIR gives finer control if you need it for a POST that redirects.
Common options
Sending a POST
Set a request body and libcurl sends an HTTP POST.
curl_easy_setopt(curl, CURLOPT_URL, "https://api.example.com/items");
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "name=widget&qty=3");
What happens
CURLOPT_POSTFIELDS takes the request body as a string. Setting it switches the method to POST and sends a default Content-Type of application/x-www-form-urlencoded.
Sending JSON
For a JSON body, set the fields and override the content type with a custom header (see the next page):
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "{\"name\":\"widget\"}");
Larger or binary bodies
If the body is not a null-terminated string, also set CURLOPT_POSTFIELDSIZE so libcurl knows the exact length.
libcurl does not copy the POST body by default, so it must stay valid until the transfer completes. Use CURLOPT_COPYPOSTFIELDS if you want libcurl to take its own copy.
Common options
Custom HTTP headers
Add or override request headers with a string list.
struct curl_slist *headers = NULL;
headers = curl_slist_append(headers, "Content-Type: application/json");
headers = curl_slist_append(headers, "Authorization: Bearer TOKEN");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_perform(curl);
curl_slist_free_all(headers);
How it works
Each curl_slist_append adds one header line. Attach the finished list with CURLOPT_HTTPHEADER. libcurl merges yours with the headers it would send anyway, and a header you set replaces libcurl's default for that name.
Removing a default
To suppress a header libcurl adds automatically, append it with no value, such as "Accept:".
Free the list with curl_slist_free_all after the transfer, never before. The handle keeps a pointer to it while the transfer runs.
Common options
Authentication
For servers that use HTTP authentication, set the credentials and the scheme.
curl_easy_setopt(curl, CURLOPT_USERPWD, "user:password");
curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
Choosing a scheme
CURLAUTH_BASIC: sends the credentials base64-encoded. Use only over HTTPS.CURLAUTH_DIGEST: a challenge-response scheme.CURLAUTH_ANY: let libcurl negotiate the strongest the server offers.
Token APIs
Most modern APIs do not use HTTP auth. They expect a bearer token in a header, so skip CURLOPT_USERPWD and send an Authorization: Bearer ... header as shown on the previous page.
Never put secrets in the URL. Credentials belong in CURLOPT_USERPWD or a header, both of which libcurl keeps out of the request line and out of most logs.
Error handling
CURLcode and curl_easy_strerror
Every easy-interface function that can fail returns a CURLcode. Checking it is not optional.
CURLcode res = curl_easy_perform(curl);
if (res != CURLE_OK)
fprintf(stderr, "libcurl: %s\n", curl_easy_strerror(res));
Human-readable messages
curl_easy_strerror() turns a code into a short English description. It is enough to know what class of thing went wrong.
More detail
For a specific message about a particular failure, give libcurl an error buffer before the transfer:
char errbuf[CURL_ERROR_SIZE];
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf);
errbuf[0] = '\0'; /* in case there is no message */
res = curl_easy_perform(curl);
if (res != CURLE_OK && errbuf[0])
fprintf(stderr, "%s\n", errbuf);
The buffer often holds a more precise reason than the generic code, such as which certificate check failed.
CURLE_OK is guaranteed to be zero, so if (res) is a valid shorthand for "did it fail", though the explicit comparison reads better.
Error handling
Common errors
A few CURLcode values you will meet early, and what they usually mean.
Connection and DNS
CURLE_COULDNT_RESOLVE_HOST: DNS lookup failed. Check the host name and the machine's DNS.CURLE_COULDNT_CONNECT: DNS worked but the connection could not be opened. Check the port and any firewall.CURLE_OPERATION_TIMEDOUT: the transfer exceeded CURLOPT_TIMEOUT, or the connect timeout.
TLS
CURLE_SSL_CONNECT_ERROR: the TLS handshake failed.CURLE_PEER_FAILED_VERIFICATION: the server's certificate could not be verified against your CA bundle.
Handling
Group errors by what the user can do: retry transient network failures, but surface configuration errors (a bad host, an untrusted certificate) so they get fixed.
Never "fix" a verification error by turning off CURLOPT_SSL_VERIFYPEER. That disables the security TLS exists to provide. Point libcurl at the correct CA bundle instead, or install the missing certificate.
Going further
The multi interface
The multi interface runs many transfers at once in a single thread, without blocking.
CURLM *multi = curl_multi_init();
curl_multi_add_handle(multi, easy1);
curl_multi_add_handle(multi, easy2);
int running;
do {
curl_multi_perform(multi, &running); /* non-blocking */
curl_multi_poll(multi, NULL, 0, 1000, NULL); /* wait for activity */
} while (running);
curl_multi_cleanup(multi);
How it fits together
You still create easy handles and set options on each exactly as before. The multi handle just drives several of them together:
curl_multi_add_handle enrolls each easy handle.curl_multi_perform moves every transfer forward as far as it can right now, and reports how many are still running.curl_multi_poll waits efficiently until at least one transfer needs attention.
When to use it
Reach for the multi interface when you need real concurrency, dozens of requests in flight, or when libcurl has to coexist with an existing event loop.
Check completed transfers with curl_multi_info_read, which reports each handle's final CURLcode as it finishes.
Going further
Language bindings
You rarely call the C API directly from another language, because libcurl already has bindings for most of them. They wrap the same easy and multi interfaces, so everything in this reference carries over.
A few well-known bindings
- Python: PycURL
- PHP: the bundled cURL extension (
curl_init, curl_setopt, curl_exec) - Ruby: Curb and Typhoeus
- Rust: the
curl crate - Node.js: node-libcurl
Why use a binding
A binding gives you libcurl's protocol support, connection reuse and battle-tested behaviour, with an API that feels native to your language. The concepts are identical: create a handle, set options, perform, read the result.
Many higher-level HTTP clients are built on libcurl under the hood, so you may already be using it without calling it directly. The full, current list of bindings lives at curl.se/libcurl/bindings.html.