Skip to content
Fresh Jots
· 16 min read
Write to Fresh Jots from C++ — `cpr`, `cpp-httplib`, or a small SDK

Write to Fresh Jots from C++ — `cpr`, `cpp-httplib`, or a small SDK

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

- **`cpr` (C++ Requests)** — modern C++ wrapper over libcurl. The recommended path when you have vcpkg/Conan.
- **`cpp-httplib`** — single-header library; drop into builds without package managers.
- **`libcurl` directly** — when you can't add a new dep and `libcurl` is already in your link line.

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.

No `freshjots` port on vcpkg or Conan yet — the snippets below are MIT-licensed; paste them, adapt them, ship them. C++ has no standard HTTP client (yet), so every approach picks a different transport — the wrapper class shape stays the same.

1. `cpr` — the recommended path (C++17+)

```cpp
// freshjots.hpp — single header, C++17+
#pragma once
#include <cpr/cpr.h>
#include <nlohmann/json.hpp>
#include <cstdlib>
#include <optional>
#include <stdexcept>
#include <string>
#include <utility>

namespace freshjots {

inline constexpr auto kBaseUrl = "https://freshjots.com/api/v1";

namespace detail {

// RFC 3986 unreserved-set percent-encoder. None of the transports
// auto-encode path segments, so a filename like "deploy log" would
// otherwise break the URL.
inline std::string url_encode(const std::string& s) {
    static constexpr char hex[] = "0123456789ABCDEF";
    std::string out;
    out.reserve(s.size() * 3);
    for (unsigned char c : s) {
        if ((c >= '0' && c <= '9') ||
            (c >= 'A' && c <= 'Z') ||
            (c >= 'a' && c <= 'z') ||
            c == '-' || c == '_' || c == '.' || c == '~') {
            out += static_cast<char>(c);
        } else {
            out += '%';
            out += hex[c >> 4];
            out += hex[c & 0x0F];
        }
    }
    return out;
}

}  // namespace detail

class ApiError : public std::runtime_error {
public:
    int         status;
    std::string code;

    ApiError(int s, std::string c, const std::string& msg)
        : std::runtime_error(msg),
          status(s),
          code(std::move(c)) {}
};

class Client {
public:
    explicit Client(std::optional<std::string> token = std::nullopt) {
        if (token) {
            token_ = std::move(*token);
        } else if (const char* env = std::getenv("FRESHJOTS_TOKEN"); env && *env) {
            token_ = env;
        } else {
            throw std::runtime_error("FRESHJOTS_TOKEN not set");
        }
    }

    void append(const std::string& filename, const std::string& text) const {
        const nlohmann::json body = {{"text", text}};
        const auto url =
            std::string(kBaseUrl) + "/notes/by-filename/" +
            detail::url_encode(filename) + "/append";

        auto res = cpr::Post(
            cpr::Url{url},
            cpr::Bearer{token_},
            cpr::Header{{"Content-Type", "application/json"}},
            cpr::Body{body.dump()});

        if (res.status_code >= 400) {
            throw parse_error(res);
        }
    }

private:
    static ApiError parse_error(const cpr::Response& res) {
        const int status = static_cast<int>(res.status_code);
        try {
            const auto j = nlohmann::json::parse(res.text);
            if (j.is_object() && j.contains("error") && j["error"].is_object()) {
                const auto& err = j["error"];
                return ApiError(
                    status,
                    err.value("code",    std::string{"unknown"}),
                    err.value("message", std::string{"request failed"}));
            }
        } catch (...) {
            // fall through to body-as-message
        }
        return ApiError(status, "unknown", res.text);
    }

    std::string token_;
};

}  // namespace freshjots
```

Two-line use:
```cpp
#include "freshjots.hpp"

freshjots::Client fj;
fj.append("deploy-log", "deploy ok — sha=abc123");
```

CMake (vcpkg or Conan toolchain):
```cmake
find_package(cpr CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
target_link_libraries(my_app PRIVATE cpr::cpr nlohmann_json::nlohmann_json)
```

vcpkg:
```sh
vcpkg install cpr nlohmann-json
```

Two design notes:
- **Default `value<std::string>` over `_json_pointer` UDL** — passing `std::string{"unknown"}` as the default forces `T = std::string` via template deduction, so the returned value is owned. The terser-looking `_json_pointer` literal needs a `using namespace nlohmann::literals;` somewhere and returns `const char*` by default, which can dangle.
- **`j.is_object()` before `j.contains(...)`** — `contains` on a non-object throws `json::type_error`. The outer catch handles it, but the explicit guard avoids the throw on every malformed-error-page response.

2. `cpp-httplib` — single-header, no package manager

When you can't pull from vcpkg or Conan, grab `cpp-httplib`'s single header and `nlohmann/json`'s single header, drop them in `third_party/`, and you're done:
```cpp
// freshjots_lite.hpp — header-only, C++17+
// CPPHTTPLIB_OPENSSL_SUPPORT must be defined at build time (see flags below),
// NOT in this header — define-in-header is fragile across translation units.
#pragma once
#include <httplib.h>
#include <nlohmann/json.hpp>
#include <cstdlib>
#include <stdexcept>
#include <string>

class FreshJots {
public:
    explicit FreshJots(std::string token = "")
        : token_(token.empty() ? require_env("FRESHJOTS_TOKEN") : std::move(token)),
          client_("https://freshjots.com") {}

    void append(const std::string& filename, const std::string& text) {
        const nlohmann::json body = {{"text", text}};
        const auto path = "/api/v1/notes/by-filename/" + url_encode(filename) + "/append";
        auto res = client_.Post(
            path,
            {{"Authorization", "Bearer " + token_}},
            body.dump(),
            "application/json");
        if (!res || res->status >= 400) {
            throw std::runtime_error(
                "Fresh Jots: status=" + std::to_string(res ? res->status : -1));
        }
    }

private:
    static std::string require_env(const char* name) {
        const char* v = std::getenv(name);
        if (!v || !*v) throw std::runtime_error(std::string(name) + " not set");
        return v;
    }

    // RFC 3986 unreserved-set percent-encoder; cpp-httplib doesn't auto-encode path segments.
    static std::string url_encode(const std::string& s) {
        static constexpr char hex[] = "0123456789ABCDEF";
        std::string out;
        out.reserve(s.size() * 3);
        for (unsigned char c : s) {
            if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
                c == '-' || c == '_' || c == '.' || c == '~') {
                out += static_cast<char>(c);
            } else {
                out += '%';
                out += hex[c >> 4];
                out += hex[c & 0x0F];
            }
        }
        return out;
    }

    std::string     token_;
    httplib::Client client_;
};
```

Build flags (HTTPS needs OpenSSL):
```sh
g++ -std=c++17 -DCPPHTTPLIB_OPENSSL_SUPPORT main.cpp -lssl -lcrypto
```

The `-DCPPHTTPLIB_OPENSSL_SUPPORT` *must* go in the build, not in the header — `cpp-httplib`'s ABI changes based on that flag, and a header-level define means TUs that include the header *before* it gets seen will silently compile a non-OpenSSL build. On Windows you can swap the OpenSSL flags for the bundled WinHTTP path; check the library README.

Reach for this when the build environment is hostile to package managers (legacy codebases, vendor toolchains, embedded SDKs).

3. `libcurl` — when you can't add anything new

When the build already links libcurl and you don't want a wrapper, this is the verbose-but-zero-new-deps path:
```cpp
#include <curl/curl.h>
#include <nlohmann/json.hpp>
#include <cstdlib>
#include <stdexcept>
#include <string>

class FreshJots {
public:
    explicit FreshJots(std::string token = "") {
        if (!token.empty()) {
            token_ = std::move(token);
        } else if (const char* env = std::getenv("FRESHJOTS_TOKEN"); env && *env) {
            token_ = env;
        } else {
            throw std::runtime_error("FRESHJOTS_TOKEN not set");
        }
        curl_global_init(CURL_GLOBAL_DEFAULT);
    }
    ~FreshJots() { curl_global_cleanup(); }

    void append(const std::string& filename, const std::string& text) {
        const nlohmann::json body_json = {{"text", text}};
        const std::string body = body_json.dump();
        const std::string url  =
            "https://freshjots.com/api/v1/notes/by-filename/" + url_encode(filename) + "/append";
        const std::string auth = "Authorization: Bearer " + token_;

        CURL* curl = curl_easy_init();
        if (!curl) throw std::runtime_error("curl_easy_init failed");

        curl_slist* headers = nullptr;
        headers = curl_slist_append(headers, auth.c_str());
        headers = curl_slist_append(headers, "Content-Type: application/json");

        curl_easy_setopt(curl, CURLOPT_URL,         url.c_str());
        curl_easy_setopt(curl, CURLOPT_HTTPHEADER,  headers);
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS,  body.c_str());

        long status       = 0;
        CURLcode res_code = curl_easy_perform(curl);
        curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
        curl_slist_free_all(headers);
        curl_easy_cleanup(curl);

        if (res_code != CURLE_OK) {
            throw std::runtime_error(std::string("curl: ") + curl_easy_strerror(res_code));
        }
        if (status >= 400) {
            throw std::runtime_error("Fresh Jots: HTTP " + std::to_string(status));
        }
    }

private:
    // RFC 3986 unreserved-set percent-encoder.
    static std::string url_encode(const std::string& s) {
        static constexpr char hex[] = "0123456789ABCDEF";
        std::string out;
        out.reserve(s.size() * 3);
        for (unsigned char c : s) {
            if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
                c == '-' || c == '_' || c == '.' || c == '~') {
                out += static_cast<char>(c);
            } else {
                out += '%';
                out += hex[c >> 4];
                out += hex[c & 0x0F];
            }
        }
        return out;
    }

    std::string token_;
};
```

A few things worth knowing about this snippet:

- **`curl_global_init` is reference-counted**, so the constructor/destructor pattern is safe for multiple `FreshJots` instances. But libcurl's docs caution that the first `curl_global_init` isn't thread-safe — for multi-threaded apps that also use libcurl elsewhere, hoist the init/cleanup pair to `main`.
- **The snippet leaks `headers` and `curl` if an exception escapes between `_init` and `_cleanup`.** All current code paths complete cleanup before any throw, but a `std::bad_alloc` from a `std::string` operation could escape. For production code, wrap the handles in `std::unique_ptr` with custom deleters (`std::unique_ptr<CURL, decltype(&curl_easy_cleanup)>{curl, &curl_easy_cleanup}`) to get RAII cleanup automatically.

Reach for this version when you're already linking `-lcurl` and adding `cpr` would mean introducing a new C++ dep into a mostly-C codebase.

4. Branching on errors

The `cpr` and `cpp-httplib` paths throw `freshjots::ApiError` (or `std::runtime_error` for `cpp-httplib`'s simpler version) with a parsed `code`. Switch on it:
```cpp
try {
    fj.append("deploy-log", entry);
} catch (const freshjots::ApiError& e) {
    if (e.code == "rate_limited") {
        std::this_thread::sleep_for(std::chrono::minutes(1));
        fj.append("deploy-log", entry);
    } else if (e.code == "cap_exceeded" || e.code == "storage_cap_exceeded") {
        ops_alert("Fresh Jots " + e.code + "; dropping log entry.");
    } else if (e.code == "unauthenticated" || e.code == "forbidden") {
        throw std::runtime_error("token bad — rotate at /settings/api_tokens");
    } else {
        throw;
    }
}
```

C++23 `std::expected` gives you a throw-free alternative when exceptions aren't allowed (RTOS, kernel-adjacent, game engines compiled with `-fno-exceptions`):
```cpp
[[nodiscard]] std::expected<void, ApiError> append_safe(
    const std::string& filename, const std::string& text) noexcept;
```

GCC 13+, Clang 17+, MSVC 19.36+ ship it. For older compilers, return `std::variant<std::monostate, ApiError>` or a small `Result` struct.

5. Patterns that work

A. Background heartbeat thread

```cpp
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <thread>

class Heartbeat {
public:
    Heartbeat(freshjots::Client& fj, std::chrono::seconds interval)
        : fj_(fj), interval_(interval), thread_([this] { run(); }) {}

    ~Heartbeat() {
        {
            std::lock_guard lock(mtx_);
            stop_ = true;
        }
        cv_.notify_all();
        if (thread_.joinable()) thread_.join();
    }

private:
    void run() {
        std::unique_lock lock(mtx_);
        while (!stop_) {
            lock.unlock();
            try {
                fj_.append("worker-heartbeat", "alive");
            } catch (const std::exception&) {
                // swallow — never let a heartbeat failure kill the loop
            }
            lock.lock();
            cv_.wait_for(lock, interval_, [this] { return stop_; });
        }
    }

    freshjots::Client&      fj_;
    std::chrono::seconds    interval_;
    bool                    stop_{false};
    std::mutex              mtx_;
    std::condition_variable cv_;
    std::thread             thread_;
};

// Use:
freshjots::Client fj;
Heartbeat hb(fj, std::chrono::minutes(15));  // dtor handles clean shutdown
```

The `condition_variable` is the load-bearing piece: a plain `std::this_thread::sleep_for` would block destruction for up to the full interval. The lock dance — unlock for the network call, re-lock before `wait_for` — keeps the mutex contention minimal while still serializing access to `stop_`. Pair this with `append_deadline_hours: 1` on the `worker-heartbeat` note: Fresh Jots emails you within an hour if the heartbeat goes silent. See [Everything you can do here](/blog/everything-you-can-do-here).

If you're on C++20, `std::jthread` + `std::stop_token` replaces the manual stop flag and the explicit join:

```cpp
std::jthread t([&fj](std::stop_token st) {
    while (!st.stop_requested()) {
        try { fj.append("worker-heartbeat", "alive"); } catch (...) {}
        std::this_thread::sleep_for(std::chrono::minutes(15));  // or a stop-aware sleep
    }
});
// jthread auto-requests stop and joins on destruction
```

B. Async append with `std::async`

```cpp
auto fut = std::async(std::launch::async, [&fj] {
    fj.append("background-job", "started");
});
// ... do other work ...
fut.get();  // joins, propagates exception if any
```

For a one-off fire-and-forget log, drop the `.get()` — but then exceptions become silent (and dropping the future blocks on its destructor if it came from `std::async`, defeating the point). Prefer a thread-pool / channel pattern over `std::async` for high-frequency calls.

C. Qt — `QNetworkAccessManager`

```cpp
class FreshJots : public QObject {
public:
    explicit FreshJots(QObject* parent = nullptr)
        : QObject(parent), token_(qEnvironmentVariable("FRESHJOTS_TOKEN")) {}

    void append(const QString& filename, const QString& text) {
        const QString encoded = QString::fromUtf8(QUrl::toPercentEncoding(filename));
        const QUrl url("https://freshjots.com/api/v1/notes/by-filename/" + encoded + "/append");
        QNetworkRequest req(url);
        req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
        req.setRawHeader("Authorization", ("Bearer " + token_).toUtf8());

        QJsonObject body{{"text", text}};
        nam_.post(req, QJsonDocument(body).toJson());
    }

private:
    QNetworkAccessManager nam_;
    QString               token_;
};
```

Qt's `QNetworkAccessManager` is event-loop-driven; the call returns immediately and emits `finished(QNetworkReply*)` on completion. Idiomatic for any Qt app — connect the signal to a slot that handles the response. `QUrl::toPercentEncoding` is the Qt idiom for path-segment encoding.

D. Unreal Engine — `FHttpModule`

C++ inside Unreal doesn't use the standard library for HTTP; it uses `FHttpModule`. The wrapper-class pattern still applies: write a small `UFreshJots` class exposing `Append(FString filename, FString text)` that builds an `IHttpRequest`, sets the bearer header, and posts. Same surface, different transport.

E. Embedded — `-fno-exceptions`, `-fno-rtti`

For builds that disable exceptions (RTOS, AUTOSAR, MISRA), return a result code instead of throwing:

```cpp
enum class AppendStatus { Ok, TokenMissing, NetworkFailed, ApiError };

[[nodiscard]] AppendStatus append(const char* filename, const char* text) noexcept;
```

No JSON parsing (skip nlohmann), no exceptions, no dynamic allocation in the hot path. The transport in embedded contexts is usually a vendor-provided HTTPS stack (lwIP + mbedTLS, ESP-IDF, etc.) — not libcurl.

6. Loading the token

- **Shell profile** (`~/.bashrc`, `~/.zshrc`, Windows env vars) — see Get & set your Fresh Jots API token. Best for local dev.
- **`.env` + a dotenv loader** (e.g. [dotenv-cpp](https://github.com/laserpants/dotenv-cpp)) — project-scoped tokens. Add `.env` to `.gitignore`.
- **CMake-time injection** via `target_compile_definitions` — **don't** bake personal tokens into the binary this way; it's fine for CI fixtures, but the binary becomes a token-carrier.
- **Container / orchestrator secret** — Kubernetes `Secret`, Docker secrets, systemd `EnvironmentFile=` with `LoadCredential=`. Always for production.

`std::getenv` returns `nullptr` for unset vars on every platform — always check before dereferencing. Windows additionally has `_dupenv_s` if you want the secure-CRT variant.

7. Going further

- **`cpr` documentation:** [docs.libcpr.org](https://docs.libcpr.org/)
- **`cpp-httplib` on GitHub:** [github.com/yhirose/cpp-httplib](https://github.com/yhirose/cpp-httplib)
- **`libcurl` easy interface:** [curl.se/libcurl/c/libcurl-easy.html](https://curl.se/libcurl/c/libcurl-easy.html)
- **`nlohmann/json`:** [json.nlohmann.me](https://json.nlohmann.me/)
- **Other languages — same pattern, different HTTP client.** Hub: Write a note from any project.
- **The CLI** — for shell-from-C++ via `popen` or `boost::process`. See Notes from your terminal.
- **Dead-man alerts** — pair the heartbeat pattern with `append_deadline_hours`. See Everything you can do here.

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

Share this post

Ready to start taking better notes? Sign up free