All of the portfolio
Portfolio · Library / SDK reference

API and SDK reference

A sample documentation set for libcurl, The multiprotocol file-transfer C library behind curl.

Why this structure

libcurl is called from code, so its documentation is a reference first: a first transfer to copy, then the functions and options laid out for lookup, with the harder parts (error handling and parallel transfers) written up as short guides. It is the shape almost every SDK reference takes.

Get docs like this
Applibcurl
AboutThe multiprotocol file-transfer C library behind curl.
StructureLibrary / SDK reference
AudienceC and systems developers embedding transfers.
Size5 sections · 15 pages
Browse the documentation

A real, navigable sample

This is the actual structure, with example articles. Click any page in the sidebar to read it.

curl.se/libcurl
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:

  1. Create a handle with curl_easy_init().
  2. Configure it with one or more curl_easy_setopt() calls.
  3. 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:

  1. curl_multi_add_handle enrolls each easy handle.
  2. curl_multi_perform moves every transfer forward as far as it can right now, and reports how many are still running.
  3. 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.