Skip to content
Fresh Jots
· 20 min read
Write to Fresh Jots from C — `libcurl`, with or without `cJSON`

Write to Fresh Jots from C — `libcurl`, with or without `cJSON`

A spin-off from Write a note from any project, focused on C. Three flavors:

- **`libcurl` + hand-rolled JSON** — one `.c` file, ~140 lines. The recommended path for most.
- **`libcurl` + `cJSON`** — single-header JSON when you want to parse error-response `code` fields.
- **Embedded / no-libcurl** — vendor HTTPS stack (mbedTLS, ESP-IDF, lwIP) for MCU contexts.

All three hit the same `/api/v1/notes/by-filename/<name>/append` endpoint. You'll need a Fresh Jots API token (`FRESHJOTS_TOKEN`). If you don't have one, see Get & set your Fresh Jots API token.

C has no package manager and no JSON in the standard library — the snippets below are MIT-licensed; copy `freshjots.h` and `freshjots.c` into your tree and add them to your Makefile or CMakeLists. C99 with one library dependency: libcurl.

1. `libcurl` — the recommended path (C99+)

The header:

```c
/* freshjots.h */
#ifndef FRESHJOTS_H
#define FRESHJOTS_H

#include <stddef.h>

typedef struct {
    char *token;  /* private — do not touch directly */
} freshjots_client;

typedef enum {
    FRESHJOTS_OK            = 0,
    FRESHJOTS_TOKEN_MISSING = 1,
    FRESHJOTS_NETWORK       = 2,
    FRESHJOTS_API           = 3,
    FRESHJOTS_OOM           = 4
} freshjots_status;

/* Call once at program start before any client init. Wraps curl_global_init. */
freshjots_status freshjots_global_init(void);

/* Call once at program shutdown after all clients are freed. */
void              freshjots_global_cleanup(void);

/* If token is NULL, reads FRESHJOTS_TOKEN env var. Zero-initializes c. */
freshjots_status freshjots_init(freshjots_client *c, const char *token);

/* Free internal resources. Safe to call on a zero-initialized struct. */
void              freshjots_free(freshjots_client *c);

/* Append text to a note (creates the note on first append).
 * Writes HTTP status to *out_status when non-NULL.
 * text must be valid UTF-8 with no embedded NUL bytes.
 *
 * Thread-safety: a single freshjots_client may be shared across threads
 * calling freshjots_append concurrently (each call creates its own
 * libcurl handle and reads c->token only). freshjots_free is NOT safe
 * to call while another thread is in freshjots_append. */
freshjots_status freshjots_append(freshjots_client *c,
                                  const char *filename,
                                  const char *text,
                                  long *out_status);

#endif /* FRESHJOTS_H */
```

The implementation:

```c
/* freshjots.c */
#include "freshjots.h"

#include <curl/curl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BASE_URL "https://freshjots.com/api/v1"

freshjots_status freshjots_global_init(void) {
    return curl_global_init(CURL_GLOBAL_DEFAULT) == CURLE_OK
        ? FRESHJOTS_OK : FRESHJOTS_NETWORK;
}

void freshjots_global_cleanup(void) {
    curl_global_cleanup();
}

freshjots_status freshjots_init(freshjots_client *c, const char *token) {
    c->token = NULL;
    if (!token) token = getenv("FRESHJOTS_TOKEN");
    if (!token || !*token) return FRESHJOTS_TOKEN_MISSING;
    size_t n = strlen(token) + 1;
    c->token = malloc(n);
    if (!c->token) return FRESHJOTS_OOM;
    memcpy(c->token, token, n);
    return FRESHJOTS_OK;
}

void freshjots_free(freshjots_client *c) {
    if (!c) return;
    free(c->token);
    c->token = NULL;
}

/* RFC 3986 unreserved-set percent-encoder.
 * Returns a malloc'd buffer the caller must free; NULL on OOM. */
static char *url_encode(const char *s) {
    static const char hex[] = "0123456789ABCDEF";
    size_t in_len = strlen(s);
    if (in_len > (SIZE_MAX - 1) / 3) return NULL;  /* defensive overflow guard */
    char *out = malloc(in_len * 3 + 1);
    if (!out) return NULL;
    char *p = out;
    for (size_t i = 0; i < in_len; i++) {
        unsigned char ch = (unsigned char)s[i];
        if ((ch >= '0' && ch <= '9') ||
            (ch >= 'A' && ch <= 'Z') ||
            (ch >= 'a' && ch <= 'z') ||
            ch == '-' || ch == '_' || ch == '.' || ch == '~') {
            *p++ = (char)ch;
        } else {
            *p++ = '%';
            *p++ = hex[ch >> 4];
            *p++ = hex[ch & 0x0F];
        }
    }
    *p = '\0';
    return out;
}

/* Build {"text": "<escaped>"} into a malloc'd buffer.
 * Caller frees. NULL on OOM. The buffer is sized for worst-case escape
 * (6 bytes per input char for "\u00xx") plus the wrapper, so every
 * sprintf below cannot overflow. */
static char *build_body(const char *text) {
    size_t text_len = strlen(text);
    if (text_len > (SIZE_MAX - 16) / 6) return NULL;  /* defensive overflow guard */
    size_t cap = text_len * 6 + 16;
    char *body = malloc(cap);
    if (!body) return NULL;
    char *p = body;
    p += sprintf(p, "{\"text\":\"");
    for (const unsigned char *q = (const unsigned char *)text; *q; q++) {
        switch (*q) {
            case '"':  p += sprintf(p, "\\\""); break;
            case '\\': p += sprintf(p, "\\\\"); break;
            case '\b': p += sprintf(p, "\\b");  break;
            case '\f': p += sprintf(p, "\\f");  break;
            case '\n': p += sprintf(p, "\\n");  break;
            case '\r': p += sprintf(p, "\\r");  break;
            case '\t': p += sprintf(p, "\\t");  break;
            default:
                if (*q < 0x20) {
                    p += sprintf(p, "\\u%04x", *q);
                } else {
                    /* UTF-8 continuation bytes (>= 0x80) pass through verbatim. */
                    *p++ = (char)*q;
                }
        }
    }
    p += sprintf(p, "\"}");
    return body;
}

freshjots_status freshjots_append(freshjots_client *c,
                                  const char *filename,
                                  const char *text,
                                  long *out_status) {
    if (out_status) *out_status = 0;
    if (!c || !c->token) return FRESHJOTS_TOKEN_MISSING;

    /* 1. URL: encode filename, snprintf into a 1KB stack buffer. */
    char *encoded = url_encode(filename);
    if (!encoded) return FRESHJOTS_OOM;

    char url[1024], auth[256];
    int url_n = snprintf(url, sizeof url,
        "%s/notes/by-filename/%s/append", BASE_URL, encoded);
    free(encoded);
    if (url_n < 0 || (size_t)url_n >= sizeof url) return FRESHJOTS_OOM;

    /* 2. Authorization header. */
    int auth_n = snprintf(auth, sizeof auth,
        "Authorization: Bearer %s", c->token);
    if (auth_n < 0 || (size_t)auth_n >= sizeof auth) return FRESHJOTS_OOM;

    /* 3. JSON body: must live through curl_easy_cleanup — see note below. */
    char *body = build_body(text);
    if (!body) return FRESHJOTS_OOM;

    /* 4. Build curl_slist header list. curl_slist_append returns NULL on OOM;
     *    don't silently end up with a half-populated list. */
    struct curl_slist *headers = curl_slist_append(NULL, auth);
    if (!headers) { free(body); return FRESHJOTS_OOM; }
    struct curl_slist *next = curl_slist_append(headers, "Content-Type: application/json");
    if (!next) { curl_slist_free_all(headers); free(body); return FRESHJOTS_OOM; }
    headers = next;

    CURL *curl = curl_easy_init();
    if (!curl) {
        curl_slist_free_all(headers);
        free(body);
        return FRESHJOTS_NETWORK;
    }

    curl_easy_setopt(curl, CURLOPT_URL,            url);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER,     headers);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS,     body);    /* see note: body must outlive cleanup */
    curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE,  (long)strlen(body));
    curl_easy_setopt(curl, CURLOPT_TIMEOUT,        30L);
    curl_easy_setopt(curl, CURLOPT_NOSIGNAL,       1L);     /* don't fire SIGALRM on timeout */

    long status   = 0;
    CURLcode rc   = curl_easy_perform(curl);
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);

    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    free(body);  /* safe to free AFTER cleanup, not before */

    if (out_status) *out_status = status;
    if (rc != CURLE_OK) return FRESHJOTS_NETWORK;
    if (status >= 400)  return FRESHJOTS_API;
    return FRESHJOTS_OK;
}
```

Use it:

```c
#include "freshjots.h"
#include <stdio.h>

int main(void) {
    if (freshjots_global_init() != FRESHJOTS_OK) {
        fprintf(stderr, "libcurl init failed\n");
        return 1;
    }

    freshjots_client c;
    if (freshjots_init(&c, NULL) != FRESHJOTS_OK) {
        fprintf(stderr, "FRESHJOTS_TOKEN not set\n");
        freshjots_global_cleanup();
        return 1;
    }

    long status;
    freshjots_status rc = freshjots_append(
        &c, "deploy-log", "deploy ok — sha=abc123", &status);

    freshjots_free(&c);
    freshjots_global_cleanup();
    return rc == FRESHJOTS_OK ? 0 : 2;
}
```

Build:

```sh
cc -std=c99 -Wall -Wextra freshjots.c main.c -lcurl -o app
```

That's the whole dependency surface — libcurl. No JSON library, no string library, no allocator surgery. The hand-rolled JSON escape handles the single string field we ever send (`{"text": "..."}`); the URL encoder follows RFC 3986's unreserved-set rule (alphanumerics and `-_.~` pass through, everything else is `%XX`).

Five design notes worth knowing:

- **Global init is separated from per-client init.** libcurl's `curl_global_init` is documented as not-thread-safe for the *first* call. Splitting it out — caller invokes `freshjots_global_init` once in `main` before any threads — sidesteps that. Per-client `_init`/`_free` only manage the token buffer.
- **Stack buffers for URL (1 KB) and auth header (256 B), heap for the body.** URLs and Bearer tokens have known small bounds (1024 bytes accommodates ~320 chars of post-encoding filename; Fresh Jots tokens are ~42 chars). `snprintf` with overflow checks prevents truncation surprises.
- **`CURLOPT_POSTFIELDS` does NOT copy the body** — libcurl uses the pointer directly through `curl_easy_perform`. The body must outlive `curl_easy_cleanup`. The function's ordering (build body → setopt → perform → cleanup → free body) is load-bearing. If you restructure, use `CURLOPT_COPYPOSTFIELDS` instead — libcurl copies and owns the duplicate.
- **`CURLOPT_NOSIGNAL=1`** prevents libcurl from setting an alarm + signal handler on timeout, which would interfere with apps that have their own SIGALRM handlers (including most CRT-using daemons).
- **`text` must be UTF-8 with no embedded NUL.** The escape function passes bytes ≥ 0x80 through unmodified, which is valid for UTF-8 but produces invalid JSON for other encodings. Convert non-UTF-8 input via `iconv` before calling. Embedded NUL bytes are silently truncated (the API is null-terminated C strings).

2. `libcurl` + `cJSON` — parsed error codes

For richer error handling, capture the response body and parse the `{"error": {"code": "..."}}` envelope. cJSON ships as one `.c` + one `.h`; drop both in your tree.

```c
/* Additions / changes to freshjots.c for the cJSON variant */
#include "cJSON.h"  /* https://github.com/DaveGamble/cJSON */

/* Accumulate the response body. Tracks length explicitly to avoid O(n^2)
 * strlen on every chunk for large responses. */
typedef struct {
    char  *buf;
    size_t len;
    size_t cap;
} response_buf;

static size_t write_cb(void *data, size_t size, size_t nmemb, void *userp) {
    response_buf *r = (response_buf *)userp;
    size_t add = size * nmemb;
    if (r->len + add + 1 < r->len) return 0;  /* overflow guard */
    if (r->len + add + 1 > r->cap) {
        size_t ncap = r->cap ? r->cap : 256;
        while (ncap < r->len + add + 1) {
            if (ncap > SIZE_MAX / 2) return 0;
            ncap *= 2;
        }
        char *p = realloc(r->buf, ncap);
        if (!p) return 0;
        r->buf = p; r->cap = ncap;
    }
    memcpy(r->buf + r->len, data, add);
    r->len += add;
    r->buf[r->len] = '\0';
    return add;
}

/* New: returns the parsed error code in *out_code (caller frees, may be NULL). */
freshjots_status freshjots_append_ex(freshjots_client *c,
                                     const char *filename,
                                     const char *text,
                                     long *out_status,
                                     char **out_code) {
    /* ... same URL/body/header setup as freshjots_append ... */

    response_buf resp = {0};
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA,     &resp);

    CURLcode rc = curl_easy_perform(curl);
    /* ... getinfo, cleanup ... */

    if (out_code) *out_code = NULL;
    if (rc == CURLE_OK && status >= 400 && resp.buf && out_code) {
        cJSON *j = cJSON_Parse(resp.buf);
        if (j) {
            cJSON *err  = cJSON_GetObjectItemCaseSensitive(j, "error");
            cJSON *code = err ? cJSON_GetObjectItemCaseSensitive(err, "code") : NULL;
            if (cJSON_IsString(code) && code->valuestring) {
                size_t n = strlen(code->valuestring) + 1;
                *out_code = malloc(n);
                if (*out_code) memcpy(*out_code, code->valuestring, n);
            }
            cJSON_Delete(j);
        }
    }
    free(resp.buf);
    /* ... return value depends on rc and status as before ... */
}
```

Build:

```sh
cc -std=c99 -Wall freshjots.c cJSON.c main.c -lcurl -o app
```

Use:

```c
long  status;
char *code = NULL;
freshjots_status rc = freshjots_append_ex(&c, "deploy-log", entry, &status, &code);
if (rc == FRESHJOTS_API && code && strcmp(code, "rate_limited") == 0) {
    sleep(60);
    freshjots_append(&c, "deploy-log", entry, NULL);  /* retry once */
}
free(code);  /* safe on NULL — free(3) accepts it */
```

Reach for this when you want to programmatically branch on the API's `code` field. For systems that just log-and-retry without parsing, the hand-rolled `freshjots_append` is enough.

3. Embedded — vendor HTTPS stack, no libcurl

For MCU / RTOS contexts, libcurl is usually too heavy — it pulls in ~200 KB of code and assumes a full POSIX environment. Use the vendor HTTPS client instead; the wrapper-function pattern is the same:

- **ESP-IDF (ESP32):** `esp_http_client_perform` with `HTTP_METHOD_POST`, `esp_http_client_set_header` for `Authorization`.
- **mbedTLS direct:** open a TLS socket, write a raw HTTP/1.1 request line plus headers and body, read the response line-by-line. ~150 lines, no dependencies beyond mbedTLS.
- **lwIP + mbedTLS:** as above but using lwIP's TCP socket abstraction. Common pattern for Zephyr and RIOT.

The `freshjots_append(token, filename, text)` signature can stay the same — only the body of the function changes to use the vendor transport. Skip the JSON parsing at this layer; you usually only care whether the append succeeded (`return 0` or `return -1`).

Three embedded-specific concerns worth raising:

- **TLS root certificate** — your device must trust the CA that signed freshjots.com's certificate. As of this writing, freshjots.com is served via Let's Encrypt: leaf signed by the `E7` ECDSA intermediate, chaining to `ISRG Root X1` (RSA-4096, valid through June 2035). Shipping ISRG Root X1 (~1.4 KB DER) as a build-time constant is enough today; for forward-compatibility add `ISRG Root X2` (ECDSA P-384, also through 2035), since some future intermediates may chain only there. Verify the current chain before each firmware cut: `openssl s_client -showcerts -servername freshjots.com -connect freshjots.com:443 </dev/null | openssl x509 -noout -issuer`. Pinning the leaf's public key is tempting but breaks every ~60 days when Let's Encrypt auto-renews — pin at the root instead.
- **Battery / power budget** — the radio (Wi-Fi, cellular, LoRa) is usually the dominant energy cost. Batch multiple log lines into one append, or compress before sending. Heartbeats every 15 minutes consume battery; consider longer intervals with deeper deadlines on the Fresh Jots side.
- **Stack vs heap** — embedded toolchains often have small heaps. The hand-rolled body builder above does one `malloc(text_len * 6 + 16)`, which can be 6× the input. For predictable behavior, replace `malloc` with a fixed stack buffer and refuse appends larger than `STACK_BUFFER_SIZE / 6`.

4. Branching on errors

With the `cJSON` variant above, switch on the parsed code:

```c
long  status;
char *code = NULL;
freshjots_status rc = freshjots_append_ex(&c, "deploy-log", entry, &status, &code);

if (rc == FRESHJOTS_API && code) {
    if (strcmp(code, "rate_limited") == 0) {
        sleep(60);
        /* caller's retry policy */
    } else if (strcmp(code, "cap_exceeded") == 0 ||
               strcmp(code, "storage_cap_exceeded") == 0) {
        ops_alert(code);  /* drop the log entry, page someone */
    } else if (strcmp(code, "unauthenticated") == 0 ||
               strcmp(code, "forbidden") == 0) {
        fprintf(stderr, "token bad — rotate at /settings/api_tokens\n");
        exit(2);
    }
}
free(code);
```

For systems that only care whether the call succeeded, `rc == FRESHJOTS_OK` is enough — no JSON parsing required, no cJSON dependency.

5. Patterns that work

A. Background heartbeat with `pthread`

```c
#include <pthread.h>
#include <time.h>
#include <unistd.h>

typedef struct {
    freshjots_client *fj;
    int               stop;
    pthread_mutex_t   mtx;
    pthread_cond_t    cv;
    pthread_t         tid;
} heartbeat_t;

static void *heartbeat_run(void *arg) {
    heartbeat_t *h = (heartbeat_t *)arg;
    pthread_mutex_lock(&h->mtx);
    while (!h->stop) {
        pthread_mutex_unlock(&h->mtx);
        freshjots_append(h->fj, "worker-heartbeat", "alive", NULL);
        pthread_mutex_lock(&h->mtx);

        struct timespec ts;
        clock_gettime(CLOCK_REALTIME, &ts);
        ts.tv_sec += 15 * 60;  /* 15 minutes */
        pthread_cond_timedwait(&h->cv, &h->mtx, &ts);
    }
    pthread_mutex_unlock(&h->mtx);
    return NULL;
}

void heartbeat_start(heartbeat_t *h, freshjots_client *fj) {
    h->fj = fj; h->stop = 0;
    pthread_mutex_init(&h->mtx, NULL);
    pthread_cond_init(&h->cv,   NULL);
    pthread_create(&h->tid, NULL, heartbeat_run, h);
}

void heartbeat_stop(heartbeat_t *h) {
    pthread_mutex_lock(&h->mtx);
    h->stop = 1;
    pthread_cond_signal(&h->cv);
    pthread_mutex_unlock(&h->mtx);
    pthread_join(h->tid, NULL);
    pthread_mutex_destroy(&h->mtx);
    pthread_cond_destroy(&h->cv);
}
```

`pthread_cond_timedwait` wakes both on timeout *and* on signal — `heartbeat_stop` interrupts the 15-minute sleep instead of waiting for it to elapse. Pair with `append_deadline_hours: 1` on the `worker-heartbeat` note in the Fresh Jots UI — when the heartbeat goes silent, you get an email within the hour. See [Everything you can do here](/blog/everything-you-can-do-here).

Two subtleties worth knowing:

- **`CLOCK_REALTIME` can jump.** NTP adjustments, `date -s` from an admin, or a VM resume can move the wall clock forward or backward — and `pthread_cond_timedwait` uses an absolute deadline against that clock. For a 15-minute heartbeat the schedule drift is harmless. For sub-minute intervals or hard latency budgets, use `pthread_condattr_setclock(&attr, CLOCK_MONOTONIC)` + `pthread_cond_init(&cv, &attr)` and `clock_gettime(CLOCK_MONOTONIC, &ts)`. **macOS doesn't support `pthread_condattr_setclock`**, so cross-platform code that needs monotonicity uses `pthread_cond_timedwait_relative_np` on macOS and the attr setter elsewhere.
- **Spurious wakeups.** POSIX permits `pthread_cond_timedwait` to return early without a signal. The worst-case effect here is an extra heartbeat (idempotent on the server side), not a missed shutdown — the `while (!h->stop)` re-check at the top of the loop handles it.

B. Reusing one `CURL*` handle for high-throughput callers

Each call to `freshjots_append` above does a fresh `curl_easy_init` + TLS handshake. For one-off log writes that's fine; for hot loops that push thousands of appends per minute it's wasteful. Two options:

- **Single-threaded reuse:** stash a `CURL*` inside `freshjots_client`, call `curl_easy_reset(curl)` between calls. libcurl's connection pool keeps the TCP/TLS session warm; the second call onward skips the handshake.
- **Multi-threaded reuse:** stash a `CURLSH *` (share handle) inside the client and call `curl_share_setopt(sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT)`. Each thread still owns its own `CURL*` but they share the connection cache.

Skip this until profiling shows the handshake cost; for ops logging at human rates, a fresh handle per call is the right default.

C. Async with libcurl's multi handle

For multiple concurrent appends without spinning up threads, libcurl's multi handle drives several easy handles from a single event loop:

```c
CURLM *multi = curl_multi_init();
curl_multi_add_handle(multi, handle1);
curl_multi_add_handle(multi, handle2);

int still_running;
do {
    curl_multi_perform(multi, &still_running);
    int n;
    if (curl_multi_poll(multi, NULL, 0, 1000, &n) != CURLM_OK) break;
} while (still_running);

curl_multi_cleanup(multi);
```

Useful for draining a queue of log writes during shutdown without thread overhead.

D. CMake integration

```cmake
find_package(CURL REQUIRED)
add_library(freshjots STATIC freshjots.c)
target_link_libraries(freshjots PUBLIC CURL::libcurl)
target_include_directories(freshjots PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

# In your consumer:
target_link_libraries(my_app PRIVATE freshjots)
```

E. Make integration

```makefile
CFLAGS  += -std=c99 -Wall -Wextra
LDLIBS  += -lcurl

my_app: my_app.c freshjots.c
    $(CC) $(CFLAGS) $^ -o $@ $(LDLIBS)
```

F. FFI from other languages

The `freshjots` C surface is small enough to wrap from any FFI-capable language — three functions per client (`_init`, `_append`, `_free`) plus the global pair. Use cases:

- **Go:** `cgo` — `// #include "freshjots.h"` then `C.freshjots_init(...)`.
- **Rust:** `bindgen` against `freshjots.h` or hand-written `extern "C"` declarations.
- **Python:** `ctypes` (`CDLL("libfreshjots.so")`) or `cffi`.
- **Lua:** `ffi.cdef` from LuaJIT, or a small `luaopen_`-style binding.

For these languages, prefer the native library where one exists (`pip install freshjots`, the JS/Rust crates, ...) — the C SDK is the fallback when you need to statically link or you're running inside an embedded host that doesn't have a runtime for the higher-level language.

6. Loading the token

- **Shell profile** (`~/.bashrc`, `~/.zshrc`) — see Get & set your Fresh Jots API token. `getenv("FRESHJOTS_TOKEN")` reads it. Best for local dev.
- **systemd `EnvironmentFile=`** — for long-running daemons. Pair with `LoadCredential=` to use the systemd credential store on newer systems.
- **`/etc/<app>.conf` + `fopen`** — for sysadmin-managed deployments. `chmod 600` and own it as the service user.
- **Container env / Kubernetes Secret** — for production. Mount as env var, read via `getenv`.

Don't compile the token into the binary (`-DFRESHJOTS_TOKEN=...`). Don't read it from a file world-readable by your service account. `getenv` returning `NULL` is the standard "not set" signal — always check.

7. Going further

- **`libcurl` easy interface:** [curl.se/libcurl/c/libcurl-easy.html](https://curl.se/libcurl/c/libcurl-easy.html)
- **`libcurl` multi interface:** [curl.se/libcurl/c/libcurl-multi.html](https://curl.se/libcurl/c/libcurl-multi.html)
- **`cJSON`:** [github.com/DaveGamble/cJSON](https://github.com/DaveGamble/cJSON)
- **`jansson`** (a heavier, more feature-rich alternative to cJSON): [github.com/akheron/jansson](https://github.com/akheron/jansson)
- **Other languages — same pattern, different HTTP client.** Hub: Write a note from any project.
- **The CLI** — for shell-from-C via `popen`. See Notes from your terminal.
- **Dead-man alerts** — pair the heartbeat thread with `append_deadline_hours`. See Everything you can do here.

One function call, one Authorization header, one note in your account — pasted into a daemon today, alerting you about silent processes tomorrow.

Share this post

Ready to start taking better notes? Sign up free