All docs / Mobile + native Carrier SDKs

Carrier Native SDK (C/C++)

The C library the mobile SDKs wrap: identity, friends, messages with receipts, Express offline delivery, invites, groups, file transfer and sessions. Source release v6.1.0, built with CMake.

Version
6.1.0 · GitHub source release
Platform
macOS · Linux · iOS · Android
git clone --branch v6.1.0 https://github.com/0xli/Elastos.NET.Carrier.Native.SDK.git

Integration verified on 2026-11-06: built 0xli/Elastos.NET.Carrier.Native.SDK branch feat/agentnet-proto-v2 (commit e8eb0b0) from a fresh clone on macOS 26.6 arm64 (Apple clang 21.0.0, CMake 4.1.1), then compiled and linked the C programs on this page against the installed headers and libcarrier.dylib. On the live network the Quickstart got ready and connected. Two nodes became friends, exchanged a message with a ByFriend receipt, and sent an offline message through the Express node lens.beagle.chat. They also completed an invite, a group, and a file transfer. They also opened a session with a reliable stream, which connected over LAN because both nodes ran on one machine. Port forwarding, channels, and builds for Linux, Android and Windows were not run.

Release v6.1.0, 2026-11-08: a fresh clone of tag v6.1.0 (commit fd7dfc8), with no download cache, builds with the commands below on macOS arm64; c-toxcore now comes from a mirror release whose SHA-256 (9a6276d8c4e5202b972d45819fd0b47ff9b55750c8f35a83d54f3ca4a4726381) matches the pin. The Quickstart, linked against that build, printed its address and reached ready and connected. The same tag also builds for iOS (device arm64, Simulator arm64 and x86_64). v6.1.0 is e8eb0b0 plus that one download fix, so the two-node results above apply to it.

The native Carrier SDK is the C library under every non-JavaScript Decent Network client. It gives a process a self-generated Carrier identity (address, user ID) on the Carrier DHT. On top of that it provides friends and presence, text and binary messages with delivery receipts, and offline delivery through Express nodes. It also covers invites, groups, file transfer, and peer-to-peer sessions with streams and TCP port forwarding. The Beagle Chat iOS and Android apps ship it from the feat/agentnet-proto-v2 branch. The iOS app gets it through the Swift Carrier SDK and the Android app through the Android Carrier SDK. The API is plain C (carrier.h, carrier_session.h, carrier_filetransfer.h) and can be called from C++ as is.

When to use it

Use it from C or C++ on macOS or Linux, for example in a daemon, a sidecar, or an embedded device, or as the base of a new language binding. On iOS use the Swift Carrier SDK, and on Android use the Android Carrier SDK; both wrap this library. In Node.js or a browser use @decentnetwork/peer, a separate JavaScript implementation of the same wire protocol.

Which version to use. Use release v6.1.0 of 0xli/Elastos.NET.Carrier.Native.SDK, or the feat/agentnet-proto-v2 branch it was cut from for the latest. The release is a source release: no prebuilt libraries are attached, so you build it as shown below. The older tags (release-v5.x, elastos-carrier-native-5.1, …) are upstream Elastos releases from 2018–2019 and predate this network's protocol work. feat/agentnet-proto-v2 is the branch the shipping Beagle apps are built from, and its docs/AGENTNET_PROTOCOL.md documents byte compatibility with @decentnetwork/peer. The other branches are behind it:

Repository / branchCommitStatus
0xli/… tag v6.1.0fd7dfc8 (2026-11-08)Use this. e8eb0b0 plus the c-toxcore mirror download.
0xli/…/feat/agentnet-proto-v2fd7dfc8 at release timeLatest. Proto v2 API (identity signatures, client info, profile extension), 16 MB bulk messages, receipt-timeout fallback to Express, multi-node Express, avatar on friend requests, LibreSSL 4.3.4 / curl 8.23.0.
decentnetworks/…/feat/agentnet-proto-v2abd1a90Older copy of the same branch, 8 commits behind (no multi-node Express, old LibreSSL/curl, no avatar on friend requests).
decentnetworks/…/master and 0xli/…/masterfb830b0 / 19aac23Pre-AgentNet base with this network's bootstrap list. It advertises protocol version 0 (legacy): peers limit it to 5 MB messages, and it has none of the proto v2 API.

Add it to your project

Build release v6.1.0 from source; no prebuilt libraries are published. On macOS you need Xcode command-line tools, CMake, and brew install autoconf automake libtool shtool pkg-config gettext. On Debian/Ubuntu, the README lists build-essential autoconf automake autopoint libtool flex bison libncurses5-dev cmake. The commands below were run from a fresh clone on Apple silicon on 2026-11-08; the build takes a few minutes with -j8 and downloads its dependencies on the first configure.

git clone --branch v6.1.0 https://github.com/0xli/Elastos.NET.Carrier.Native.SDK.git
cd Elastos.NET.Carrier.Native.SDK

cmake -S . -B build/macos-arm64 -DCMAKE_BUILD_TYPE=Release \
      -DENABLE_SHARED=ON -DENABLE_STATIC=ON -DENABLE_APPS=ON -DENABLE_TESTS=OFF \
      -DCMAKE_INSTALL_PREFIX=$PWD/../carrier-dist
make -C build/macos-arm64 -j8
make -C build/macos-arm64 install

Dependency tarballs are cached in build/.tarballs/. A failed download leaves a 0-byte file there, and the next configure treats it as cached. Delete any empty file there before retrying.

make install produces:

PathContents
include/carrier.h, carrier_session.h, carrier_filetransfer.h and their *_deprecated.h companions
lib/libcarrier, libcarriersession, libcarrierfiletrans, libcrystal (.dylib/.so and .a), plus the static dependencies (libtoxcore.a, libsodium.a, libssl.a, libcrypto.a, libtls.a, libcurl.a, libpj*.a, libsrtp.a, libflatccrt.a, libcjson.a, libz.a)
bin/elashell (interactive shell), elafile, elapfd, elaspeedtest, elaerr (error-code lookup)
etc/carrier/carrier.confThe network's bootstrap and Express node list

Link against the shared libraries:

P=$PWD/../carrier-dist
cc -Wall -o hello hello.c -I$P/include -L$P/lib -lcarrier -Wl,-rpath,$P/lib
# sessions:      add -lcarriersession
# file transfer: add -lcarrierfiletrans -lcarriersession

Initialize

Fill a CarrierOptions with a data directory and the network's bootstrap nodes. Optionally add Express nodes for offline delivery. Register the callbacks you need, call carrier_new, then run carrier_run on its own thread. Use the bootstrap and Express nodes from config/carrier.conf on the branch. The program below uses three of them.

#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <carrier.h>

static void on_connection(Carrier *c, CarrierConnectionStatus status, void *ctx)
{
    printf("connection: %s\n",
           status == CarrierConnectionStatus_Connected ? "connected" : "disconnected");
}

static void on_ready(Carrier *c, void *ctx)
{
    printf("ready\n");
}

static void on_friend_request(Carrier *c, const char *userid,
                              const CarrierUserInfo *info, const char *hello, void *ctx)
{
    printf("friend request from %s: %s\n", userid, hello);
    carrier_accept_friend(c, userid);
}

static void on_friend_message(Carrier *c, const char *from, const void *msg, size_t len,
                              int64_t timestamp, bool offline, void *ctx)
{
    printf("%s from %s: %.*s\n", offline ? "offline msg" : "msg", from, (int)len,
           (const char *)msg);
}

static void *run_loop(void *arg)
{
    carrier_run((Carrier *)arg, 10);   /* blocks until carrier_kill() */
    return NULL;
}

int main(int argc, char *argv[])
{
    BootstrapNode bootstraps[] = {
        { "13.58.208.50",  NULL, "33445", "89vny8MrKdDKs7Uta9RdVmspPjnRMdwMmaiEW27pZ7gh" },
        { "18.216.102.47", NULL, "33445", "G5z8MqiNDFTadFUPfMdYsYtkUDbX5mNCMVHMZtsCnFeb" },
        { "52.57.248.163", NULL, "33445", "CfJLve8FNQPQJ9xYQ8oEVkPxeAPCN7iSdhnYFkWmWgLn" },
    };
    ExpressNode express[] = {
        { "lens.beagle.chat", NULL, "443", "ECbs4GxwGzxGerNkmqDJFibEmevu8jAXqAZtikccvD95" },
    };
    CarrierOptions opts;
    CarrierCallbacks cbs;
    char addr[CARRIER_MAX_ADDRESS_LEN + 1];
    char userid[CARRIER_MAX_ID_LEN + 1];
    char nodeid[CARRIER_MAX_ID_LEN + 1];
    char err[128];
    pthread_t tid;

    memset(&opts, 0, sizeof(opts));
    opts.persistent_location = argc > 1 ? argv[1] : "./carrier-data";
    opts.udp_enabled = true;
    opts.log_level = CarrierLogLevel_Warning;
    opts.bootstraps = bootstraps;
    opts.bootstraps_size = sizeof(bootstraps) / sizeof(bootstraps[0]);
    opts.express_nodes = express;
    opts.express_nodes_size = sizeof(express) / sizeof(express[0]);

    memset(&cbs, 0, sizeof(cbs));
    cbs.connection_status = on_connection;
    cbs.ready = on_ready;
    cbs.friend_request = on_friend_request;
    cbs.friend_message = on_friend_message;

    Carrier *c = carrier_new(&opts, &cbs, NULL);
    if (!c) {
        printf("carrier_new failed: %s\n",
               carrier_get_strerror(carrier_get_error(), err, sizeof(err)));
        return 1;
    }

    printf("version: %s\n", carrier_get_version());
    printf("address: %s\n", carrier_get_address(c, addr, sizeof(addr)));
    printf("userid:  %s\n", carrier_get_userid(c, userid, sizeof(userid)));
    printf("nodeid:  %s\n", carrier_get_nodeid(c, nodeid, sizeof(nodeid)));

    pthread_create(&tid, NULL, run_loop, c);
    sleep(argc > 2 ? atoi(argv[2]) : 15);
    carrier_kill(c);
    pthread_join(tid, NULL);
    printf("killed\n");
    return 0;
}

Verified output (./hello ./data 20, branch feat/agentnet-proto-v2 @ e8eb0b0, 2026-11-06):

version: 5219aac2
address: MK5iHYpfjr2vujMmMVCZiPTHkPGZemkbhzCaQ3brWwVfEQtEM1fX
userid:  AEzccEk8AkYLtMx3QJK76DwqMSfqVFM3qUN1hpDNwmgG
nodeid:  AEzccEk8AkYLtMx3QJK76DwqMSfqVFM3qUN1hpDNwmgG
ready
connection: connected
killed

The first run creates carrier.data in persistent_location. That file holds the identity, unencrypted, plus the friend list. Later runs reuse it. carrier_get_version() returns major, minor and the first six hex digits of the build checkout's local master, with no separators (5 2 19aac2 above). It does not identify the branch you built.

CarrierOptions fields:

FieldMeaning
persistent_locationData directory (required).
secret_keyOptional 32-byte identity secret. Used only when the directory has no data yet (import an identity).
udp_enabledfalse forces TCP only.
log_level, log_file, log_printerCarrierLogLevel_None … CarrierLogLevel_Verbose; log to stdout by default.
bootstraps, bootstraps_sizeDHT bootstrap nodes (ipv4, ipv6, port, public_key, all strings).
express_nodes, express_nodes_sizeExpress (offline message) nodes, same shape. Without them, offline delivery is off.

Identity

Each node's identity is a key pair kept in carrier.data. The user ID is the base58 public key. The address (52 characters) is the user ID plus a nospam value and checksum. Others need the address to send a friend request; after that they reach you by user ID. The node ID equals the user ID on this build. Changing the nospam value with carrier_set_self_nospam changes the address, so old copies of it stop working for new requests.

The proto v2 API adds XEdDSA signatures that the JavaScript peer can verify (curve25519-js), plus secret-key export and import. The excerpt below is from a program that was run:

const char *msg = "decent-auth\nhttps://example.org\nnonce-123";
uint8_t sig[CARRIER_IDENTITY_SIGNATURE_BYTES], secret[CARRIER_IDENTITY_SECRET_BYTES];

carrier_identity_sign(c, (const uint8_t *)msg, strlen(msg), sig);
/* 1 = valid, 0 = invalid, -1 = bad arguments; needs only the userid */
int ok = carrier_identity_verify(userid, (const uint8_t *)msg, strlen(msg), sig);

carrier_export_secret_key(c, secret);   /* keep it in a keychain, never a plain file */
/* Import: a fresh persistent_location plus opts.secret_key = secret gives the same userid. */
address valid: 1, userid valid: 1
userid from address matches: 1
verify good: 1
verify tampered: 0
same identity from secret_key: 1

Profile: carrier_set_self_info / carrier_get_self_info take a CarrierUserInfo (name, description, gender, phone, email, region). Beagle clients put the CryptoPunk avatar ID in gender. carrier_set_self_profile_ext adds avatar_url, url, ens and extra. carrier_set_client_info advertises platform, os_version, app_version and proto_version. Set proto_version to 2 only if your app answers the DNPACK1 text acknowledgements that @decentnetwork/peer sends. Read a friend's values with carrier_get_friend_client_info and carrier_get_friend_profile_ext.

Friends

carrier_add_friend(c, address, hello) sends a request. The other side gets friend_request and calls carrier_accept_friend(c, userid). On the requester, friend_added fired right after the request was sent. friend_connection reports when a friend comes online or goes offline. This excerpt is from a two-node program that was run, with both nodes in one process:

/* B */
if (!carrier_is_friend(b, a_id) && carrier_add_friend(b, a_addr, "hi from B") < 0)
    printf("[B] add_friend failed: 0x%x\n", carrier_get_error());

/* A: CarrierCallbacks.friend_request */
static void a_on_friend_request(Carrier *c, const char *userid,
                                const CarrierUserInfo *info, const char *hello, void *ctx)
{
    printf("[A] friend request from %s, hello=\"%s\"\n", userid, hello);
    carrier_accept_friend(c, userid);
}
[A] ready
[B] ready
[B] friend added: 9EbkTvShD1HnzXXFFB8QFukrhTAiMANuFU7R5V38X9W9
[A] friend request from FXP4Vu7zdMBjbPbkjtyryh7rMLtGFAbf5bPciY7xNpy2, hello="hi from B"
[B] friend 9EbkTvShD1HnzXXFFB8QFukrhTAiMANuFU7R5V38X9W9 is online

Other calls: carrier_remove_friend, carrier_get_friends (iterator), carrier_get_friend_info, carrier_set_friend_label, carrier_is_friend. Callbacks: friend_list, friend_info, friend_presence, friend_added, friend_removed. A friend request carries the requester's gender (punk ID) in info->gender. Since 2026-11-01 it also carries an avatar URL; read it inside the callback with carrier_get_friend_request_profile_ext. The SDK remembers only the last 32 requesters, and only in memory.

Messages

carrier_send_friend_message sends up to CARRIER_MAX_APP_BULKMSG_LEN (16 MB). Messages over 1024 bytes are fragmented and reassembled for you. The library refuses messages over 5 MB to a friend that advertises protocol version 0. The receipt callback reports one of three states: CarrierReceipt_ByFriend (delivered online), CarrierReceipt_Offline (stored on an Express node), or CarrierReceipt_Error.

static void on_receipt(uint32_t msgid, CarrierReceiptState state, void *ctx)
{
    printf("[B] receipt for msg %u: %s\n", msgid,
           state == CarrierReceipt_ByFriend ? "ByFriend" :
           state == CarrierReceipt_Offline ? "Offline" : "Error");
}

uint32_t msgid;
if (carrier_send_friend_message(b, a_id, "hello A", 7, &msgid, on_receipt, NULL) < 0)
    printf("[B] send failed: 0x%x\n", carrier_get_error());
[B] sent msg 1793954296
[A] message from FXP4Vu7zdMBjbPbkjtyryh7rMLtGFAbf5bPciY7xNpy2 (offline=0): hello A
[B] receipt for msg 1793954296: ByFriend

Incoming messages arrive on friend_message(carrier, from, msg, len, timestamp, offline, context). The header documents timestamp as seconds since the Epoch. The source passes microseconds, and the run above saw 1793954355618750, so treat it as microseconds. If a message gets no receipt within CARRIER_RECEIPT_TIMEOUT_SECONDS (30 s), the library re-sends it through Express.

Invites are request/response exchanges with a friend (up to CARRIER_MAX_INVITE_DATA_LEN, 8192 bytes, plus an optional bundle). The excerpt below was run:

/* A: CarrierCallbacks.friend_invite */
static void a_on_invite(Carrier *c, const char *from, const char *bundle,
                        const void *data, size_t len, void *ctx)
{
    carrier_reply_friend_invite(c, from, bundle, 0, NULL, "pong", 4);   /* status 0 = accept */
}

/* B */
carrier_invite_friend(b, a_id, NULL, "ping", 4, b_on_invite_reply, NULL);
[A] invite from FXP4Vu7zdMBjbPbkjtyryh7rMLtGFAbf5bPciY7xNpy2: ping
[B] invite reply from 9EbkTvShD1HnzXXFFB8QFukrhTAiMANuFU7R5V38X9W9: status=0 data=pong

Groups are decentralized; peers must be online. carrier_new_group creates one and carrier_group_invite invites a friend. The friend gets group_invite with a cookie and calls carrier_group_join. Group events go to CarrierCallbacks.group_callbacks. The excerpt below was run:

/* A */
char groupid[128];
carrier_new_group(a, groupid, sizeof(groupid));
carrier_group_invite(a, groupid, b_id);

/* B: CarrierCallbacks.group_invite */
static void b_on_group_invite(Carrier *c, const char *from, const void *cookie,
                              size_t len, void *ctx)
{
    char gid[128];
    carrier_group_join(c, from, cookie, len, gid, sizeof(gid));
}

/* B: group_callbacks.group_connected */
static void b_on_group_connected(Carrier *c, const char *groupid, void *ctx)
{
    carrier_group_send_message(c, groupid, "hi group", 8);
}
[A] created group EXLdYroudtB6xpZBdhVrjE9deSHTzFCDiKH2hEczdKNF
[B] group invite from 9EbkTvShD1HnzXXFFB8QFukrhTAiMANuFU7R5V38X9W9 (35-byte cookie)
[B] joined group EXLdYroudtB6xpZBdhVrjE9deSHTzFCDiKH2hEczdKNF
[B] group connected EXLdYroudtB6xpZBdhVrjE9deSHTzFCDiKH2hEczdKNF
[A] group EXLdYroudtB6xpZBdhVrjE9deSHTzFCDiKH2hEczdKNF message from FXP4Vu7zdMBjbPbkjtyryh7rMLtGFAbf5bPciY7xNpy2: hi group

Files

carrier_filetransfer.h (link -lcarrierfiletrans -lcarriersession) is a pull-driven, resumable transfer over a session stream. Each side calls carrier_filetransfer_init once; the receiver passes a connect callback. The sender opens a transfer with the file info and connects. The receiver accepts, gets file, and pulls from an offset. The sender gets pull and calls carrier_filetransfer_send. The excerpt below was run:

/* Receiver (A): carrier_filetransfer_init(a, a_on_connect, NULL) */
static void a_on_connect(Carrier *c, const char *from, const CarrierFileTransferInfo *fi, void *ctx)
{
    static CarrierFileTransferCallbacks cbs;
    cbs.state_changed = a_state; cbs.file = a_file; cbs.data = a_data;
    CarrierFileTransfer *ft = carrier_filetransfer_new(c, from, NULL, &cbs, NULL);
    carrier_filetransfer_accept_connect(ft);
}
static void a_file(CarrierFileTransfer *ft, const char *fileid, const char *filename,
                   uint64_t size, void *ctx)
{
    carrier_filetransfer_pull(ft, fileid, 0);
}

/* Sender (B): carrier_filetransfer_init(b, NULL, NULL) */
CarrierFileTransferInfo fi;
memset(&fi, 0, sizeof(fi));
strcpy(fi.filename, "hello.txt");
carrier_filetransfer_fileid(fi.fileid, sizeof(fi.fileid));
fi.size = sizeof(payload) - 1;
bcbs.state_changed = b_state; bcbs.pull = b_pull;   /* b_pull calls carrier_filetransfer_send */
CarrierFileTransfer *ft = carrier_filetransfer_new(b, a_id, &fi, &bcbs, NULL);
carrier_filetransfer_connect(ft);
[B] state connecting
[A] filetransfer connect from FXP4Vu7zdMBjbPbkjtyryh7rMLtGFAbf5bPciY7xNpy2
[A] state connecting
[A] state connected
[B] state connected
[A] offered hello.txt (40 bytes), pulling
[B] pull at 0, sent 40
[A] data 40 bytes: file body sent over carrier_filetransfer
[B] state closed
[A] state closed

For large files, send from a separate thread in chunks of at most CARRIER_MAX_USER_DATA_LEN (2048 bytes). That is what the library's internal easyfile.c helper does; it is not exported. This file transfer is native to native only. Beagle apps and @decentnetwork/peer send online files as inline bulk messages (up to 16 MB, through carrier_send_friend_message), wrapped in an application-level JSON envelope. The JavaScript peer's messenger file transfer (packet IDs 80–82) is not implemented in this SDK. See docs/AGENTNET_PROTOCOL.md §2 on the branch.

Sessions and calls

carrier_session.h (link -lcarriersession) sets up ICE (pjnath) peer-to-peer sessions with a friend: direct, server-reflexive, or relayed. A session carries one or more streams. Stream options: CARRIER_STREAM_PLAIN, CARRIER_STREAM_RELIABLE, CARRIER_STREAM_MULTIPLEXING, and CARRIER_STREAM_PORT_FORWARDING. Multiplexing streams carry channels (carrier_stream_open_channel, carrier_stream_write_channel). Port-forwarding streams expose a local TCP service (carrier_session_add_service) that the peer opens with carrier_stream_open_port_forwarding. This library has no audio/video call API. Beagle calls use the WebRTC SDKs, which signal over Carrier messages.

The handshake:

  1. Both sides call carrier_session_init(carrier). The receiving side registers carrier_session_set_callback(carrier, NULL, on_request, ctx).
  2. Caller: carrier_session_new(carrier, peer_userid), then carrier_session_add_stream(...). Wait for CarrierStreamState_initialized, then call carrier_session_request(session, NULL, on_complete, ctx).
  3. Callee, in or after on_request(from, bundle, sdp, len): carrier_session_new, then the same add_stream. Wait for initialized, call carrier_session_reply_request(session, NULL, 0, NULL), wait for transport_ready, then call carrier_session_start(session, sdp, len).
  4. Caller, in on_complete(…, status == 0, sdp, len): wait for transport_ready, then call carrier_session_start(session, sdp, len).
  5. Both streams reach CarrierStreamState_connected. Then use carrier_stream_write and the stream_data callback.

The excerpt below is from a program that was run. Both nodes ran in one process, and its main thread waited for the stream states:

CarrierStreamCallbacks scb;
memset(&scb, 0, sizeof(scb));
scb.state_changed = on_state;      /* records the state per side */
scb.stream_data   = on_data;

/* B (caller) */
B.s = carrier_session_new(b, a_id);
B.stream = carrier_session_add_stream(B.s, CarrierStreamType_text,
                                      CARRIER_STREAM_RELIABLE, &scb, &B);
wait_state(&B, CarrierStreamState_initialized, 30);
carrier_session_request(B.s, NULL, b_on_complete, NULL);   /* b_on_complete saves A's SDP */

/* A (callee), after a_on_request saved `from` and B's SDP */
A.s = carrier_session_new(a, A.from);
A.stream = carrier_session_add_stream(A.s, CarrierStreamType_text,
                                      CARRIER_STREAM_RELIABLE, &scb, &A);
wait_state(&A, CarrierStreamState_initialized, 30);
carrier_session_reply_request(A.s, NULL, 0, NULL);
wait_state(&A, CarrierStreamState_transport_ready, 30);
carrier_session_start(A.s, A.remote_sdp, A.sdp_len);

/* B */
wait_state(&B, CarrierStreamState_transport_ready, 30);
carrier_session_start(B.s, B.remote_sdp, B.sdp_len);
wait_state(&B, CarrierStreamState_connected, 60);
carrier_stream_write(B.s, B.stream, "hello over a stream", 19);
[B] stream 1 -> initialized
[B] stream 1 -> transport_ready
[A] stream 1 -> initialized
[B] request complete status=0
[A] stream 1 -> transport_ready
[A] stream 1 -> connecting
[B] stream 1 -> connecting
[A] stream 1 -> connected
[B] stream 1 -> connected
[B] topology LAN
[A] stream data: hello over a stream

Send the session request only after the friend shows as online on the caller's side. Otherwise it fails with 0x86000022 (ERROR_FRIEND_OFFLINE). Close with carrier_session_close, then carrier_session_cleanup before carrier_kill.

Offline messages (Express)

With express_nodes set, sending to an offline friend stores the message on an Express node, and the receipt reports CarrierReceipt_Offline. The recipient pulls it when it next starts and gets friend_message with offline == true. Offline friend requests travel the same way. This branch posts to and pulls from every configured Express node. The excerpt below was run against lens.beagle.chat. B sent while A was not running; A then started alone:

/* B (A is not running) */
carrier_send_friend_message(b, a_id, "sent while you were away", 24, &msgid, on_receipt, NULL);
[B] receipt 1793954356: Offline
[A] message from FXP4Vu7zdMBjbPbkjtyryh7rMLtGFAbf5bPciY7xNpy2 (offline=1, ts=1793954355618750): sent while you were away

Threading and lifecycle

  • carrier_new creates the node and loads or creates carrier.data. It does not touch the network.
  • carrier_run(carrier, interval_ms) connects to the bootstraps and runs the event loop on the calling thread. It blocks until carrier_kill is called, then saves carrier.data and returns 0. interval 0 means 1000 ms. Run it on a dedicated thread, as in the Quickstart.
  • CarrierCallbacks callbacks, message receipts and invite responses are called from inside carrier_run, on its thread. Do not block in them. Calling Carrier functions from them works: the samples above accept friends, reply to invites and join groups inside callbacks.
  • Session ICE runs on a pjnath worker thread per session, so stream and file-transfer callbacks may arrive on that thread rather than the carrier_run thread. Protect shared state.
  • Calls such as carrier_send_friend_message and carrier_invite_friend fail with ERROR_NOT_BEING_READY until the node is ready. Wait for the ready callback or carrier_is_ready().
  • Shutdown: close file transfers and sessions, call carrier_filetransfer_cleanup / carrier_session_cleanup, then carrier_kill(carrier) and join the run thread. After carrier_kill the handle is invalid.

Errors. Functions return -1 or NULL on failure and set a thread-local error code. Read it with carrier_get_error() and clear it with carrier_clear_error(). carrier_get_strerror(err, buf, len) turns the code into text, and so does the elaerr tool. A code is 0x80000000 | facility << 24 | code. The facilities are FACILITY_GENERAL (1), FACILITY_SYS (2), FACILITY_ICE (5), FACILITY_DHT (6) and FACILITY_EXPRESS (7). The codes are ERROR_INVALID_ARGS (0x01) … ERROR_BAD_FLATBUFFER (0x23), ERROR_UNKNOWN (0xFF). Examples:

add_friend error 0x81000001: [General] Invalid argument(s)
$ elaerr 0x86000022
[DHT] Friend is being offline

Platforms

These targets come from the branch's CMake files. Only the macOS arm64 host build was run for this page. The Swift Carrier SDK page separately verified the iOS Simulator arm64 slice of this branch.

TargetHowStatus
macOS arm64 / x86_64host build (above)arm64 verified 2026-11-06
Linux (Ubuntu/Debian)host build, same commandsper README and CI config; not run for this page
iOS iphoneos arm64, iphonesimulator arm64 / x86_64-DCMAKE_TOOLCHAIN_FILE=cmake/iOSToolchain2.cmake -DIOS_PLATFORM=… -DIOS_ARCH=…; static only; deployment target 13.0 (IOS_DEPLOYMENT_TARGET)used for the shipping app; see docs/REBUILD-IOS-ANDROID.md
Android armeabi-v7a, arm64-v8a, x86, x86_64-DCMAKE_TOOLCHAIN_FILE=cmake/AndroidToolchain.cmake -DANDROID_ABI=… -DANDROID_NDK_HOME=… -DANDROID_API=24 (default 21; 24 needed for getifaddrs)arm64-v8a with NDK r28.2 used for the shipping app; recent NDKs need GNU-named binutils wrappers (see docs/REBUILD-IOS-ANDROID.md)
Raspberry Pi (armv7l)cmake/RPiToolchain.cmake, or -DBUILD_ON_RPI=ON on the deviceper README; not run
Windows x86/x64CMake + Visual Studio, per READMEper README; not run on this branch

CMake switches: ENABLE_SHARED, ENABLE_STATIC, ENABLE_APPS, ENABLE_TESTS, ENABLE_UNIT_TESTS, ENABLE_DOCS (Linux only), CMAKE_BUILD_TYPE, CMAKE_INSTALL_PREFIX.

API reference

All functions are declared in the installed headers. Unless noted, they return 0 on success or -1/NULL on error (see carrier_get_error).

carrier.h: node, identity, friends, messages, groups

FunctionSignature
versionconst char *carrier_get_version(void)
validatebool carrier_address_is_valid(const char *address), bool carrier_id_is_valid(const char *id), char *carrier_get_id_by_address(const char *address, char *userid, size_t len)
lifecycleCarrier *carrier_new(const CarrierOptions *options, CarrierCallbacks *callbacks, void *context), int carrier_run(Carrier *carrier, int interval), void carrier_kill(Carrier *carrier), bool carrier_is_ready(Carrier *carrier)
identitychar *carrier_get_address(Carrier *, char *address, size_t len), char *carrier_get_userid(Carrier *, char *userid, size_t len), char *carrier_get_nodeid(Carrier *, char *nodeid, size_t len), int carrier_set_self_nospam(Carrier *, uint32_t), int carrier_get_self_nospam(Carrier *, uint32_t *)
signaturesint carrier_identity_sign(Carrier *, const uint8_t *message, size_t length, uint8_t signature[64]), int carrier_identity_verify(const char *userid, const uint8_t *message, size_t length, const uint8_t signature[64]) (returns 1/0/-1), int carrier_export_secret_key(Carrier *, uint8_t secret[32]), int carrier_identity_create_auth_proof(Carrier *, const uint8_t server_public_key[32], const uint8_t *transcript, size_t transcript_len, uint8_t proof[32])
profileint carrier_set_self_info(Carrier *, const CarrierUserInfo *), int carrier_get_self_info(Carrier *, CarrierUserInfo *), int carrier_set_self_presence(Carrier *, CarrierPresenceStatus), int carrier_get_self_presence(Carrier *, CarrierPresenceStatus *), int carrier_set_self_profile_ext(Carrier *, const CarrierProfileExt *), int carrier_get_self_profile_ext(Carrier *, CarrierProfileExt *)
client infoint carrier_set_client_info(Carrier *, const CarrierClientInfo *), int carrier_get_client_info(Carrier *, CarrierClientInfo *), int carrier_get_friend_client_info(Carrier *, const char *friendid, CarrierClientInfo *)
friendsint carrier_add_friend(Carrier *, const char *address, const char *hello), int carrier_accept_friend(Carrier *, const char *userid), int carrier_remove_friend(Carrier *, const char *userid), bool carrier_is_friend(Carrier *, const char *userid), int carrier_get_friends(Carrier *, CarrierFriendsIterateCallback *, void *context), int carrier_get_friend_info(Carrier *, const char *friendid, CarrierFriendInfo *), int carrier_set_friend_label(Carrier *, const char *friendid, const char *label), int carrier_get_friend_profile_ext(Carrier *, const char *friendid, CarrierProfileExt *), int carrier_get_friend_request_profile_ext(Carrier *, const char *userid, CarrierProfileExt *)
messagesint carrier_send_friend_message(Carrier *, const char *to, const void *message, size_t len, uint32_t *msgid, CarrierFriendMessageReceiptCallback *cb, void *context)
invitesint carrier_invite_friend(Carrier *, const char *to, const char *bundle, const void *data, size_t len, CarrierFriendInviteResponseCallback *callback, void *context), int carrier_reply_friend_invite(Carrier *, const char *to, const char *bundle, int status, const char *reason, const void *data, size_t len)
groupsint carrier_new_group(Carrier *, char *groupid, size_t length), int carrier_leave_group(Carrier *, const char *groupid), int carrier_group_invite(Carrier *, const char *groupid, const char *friendid), int carrier_group_join(Carrier *, const char *friendid, const void *cookie, size_t cookie_len, char *groupId, size_t group_len), int carrier_group_send_message(Carrier *, const char *groupid, const void *message, size_t length), int carrier_group_get_title(…), int carrier_group_set_title(Carrier *, const char *groupid, const char *title), int carrier_group_get_peers(…), int carrier_group_get_peer(…), int carrier_get_groups(Carrier *, CarrierIterateGroupCallback *, void *context)
errorsint carrier_get_error(void), void carrier_clear_error(void), char *carrier_get_strerror(int errnum, char *buf, size_t len)

CarrierCallbacks members: idle, connection_status, ready, self_info, friend_list, friend_connection, friend_info, friend_presence, friend_request, friend_added, friend_removed, friend_message, friend_invite, group_invite, and group_callbacks (group_connected, group_message, group_title, peer_name, peer_list_changed).

Limits: address 52 characters, IDs up to 45, name 63, description 127, single message unit 1024 bytes, bulk message 16 MB (5 MB to legacy peers), invite data 8192, bundle 511, group title 127, userinfo packet 1007 bytes, friend request packet 1016 bytes.

carrier_session.h: sessions, streams, port forwarding

carrier_session_init, carrier_session_cleanup, carrier_session_set_callback(Carrier *, const char *bundle_prefix, CarrierSessionRequestCallback *, void *), carrier_session_new(Carrier *, const char *address), carrier_session_close, carrier_session_get_peer, carrier_session_set_userdata / _get_userdata, carrier_session_request(CarrierSession *, const char *bundle, CarrierSessionRequestCompleteCallback *, void *), carrier_session_reply_request(CarrierSession *, const char *bundle, int status, const char *reason), carrier_session_start(CarrierSession *, const char *sdp, size_t len), carrier_session_add_stream(CarrierSession *, CarrierStreamType, int options, CarrierStreamCallbacks *, void *), carrier_session_remove_stream, carrier_session_add_service(CarrierSession *, const char *service, PortForwardingProtocol, const char *host, const char *port), carrier_session_remove_service, carrier_stream_get_type, carrier_stream_get_state, carrier_stream_get_transport_info, ssize_t carrier_stream_write(CarrierSession *, int stream, const void *data, size_t len), carrier_stream_open_channel, carrier_stream_close_channel, ssize_t carrier_stream_write_channel(…), carrier_stream_pend_channel, carrier_stream_resume_channel, carrier_stream_open_port_forwarding(CarrierSession *, int stream, const char *service, PortForwardingProtocol, const char *host, const char *port), carrier_stream_close_port_forwarding.

CarrierStreamCallbacks: state_changed, stream_data, channel_open, channel_opened, channel_close, channel_data, channel_pending, channel_resume. States: initialized, transport_ready, connecting, connected, deactivated, closed, failed. Topology (carrier_stream_get_transport_info): NetworkTopology_LAN, _P2P, _RELAYED. Only TCP port forwarding is defined (PortForwardingProtocol_TCP).

carrier_filetransfer.h: file transfer

carrier_filetransfer_fileid, carrier_filetransfer_init(Carrier *, CarrierFileTransferConnectCallback *, void *), carrier_filetransfer_cleanup, carrier_filetransfer_new(Carrier *, const char *address, const CarrierFileTransferInfo *, CarrierFileTransferCallbacks *, void *), carrier_filetransfer_close, carrier_filetransfer_get_fileid, carrier_filetransfer_get_filename, carrier_filetransfer_connect, carrier_filetransfer_accept_connect, carrier_filetransfer_add, carrier_filetransfer_pull(CarrierFileTransfer *, const char *fileid, uint64_t offset), ssize_t carrier_filetransfer_send(CarrierFileTransfer *, const char *fileid, const uint8_t *data, size_t length), carrier_filetransfer_cancel, carrier_filetransfer_pend, carrier_filetransfer_resume, carrier_filetransfer_set_userdata / _get_userdata. Callbacks: state_changed, file, pull, data, pending, resume, cancel.

The *_deprecated.h headers keep the older ela_* names as aliases. Do not use them in new code.

Source