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.
git clone --branch v6.1.0 https://github.com/0xli/Elastos.NET.Carrier.Native.SDK.gitIntegration verified on 2026-11-06: built
0xli/Elastos.NET.Carrier.Native.SDKbranchfeat/agentnet-proto-v2(commite8eb0b0) 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 andlibcarrier.dylib. On the live network the Quickstart gotreadyandconnected. Two nodes became friends, exchanged a message with aByFriendreceipt, and sent an offline message through the Express nodelens.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 tagv6.1.0(commitfd7dfc8), with no download cache, builds with the commands below on macOS arm64;c-toxcorenow comes from a mirror release whose SHA-256 (9a6276d8c4e5202b972d45819fd0b47ff9b55750c8f35a83d54f3ca4a4726381) matches the pin. The Quickstart, linked against that build, printed its address and reachedreadyandconnected. The same tag also builds for iOS (device arm64, Simulator arm64 and x86_64).v6.1.0ise8eb0b0plus 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 / branch | Commit | Status |
|---|---|---|
0xli/… tag v6.1.0 | fd7dfc8 (2026-11-08) | Use this. e8eb0b0 plus the c-toxcore mirror download. |
| 0xli/…/feat/agentnet-proto-v2 | fd7dfc8 at release time | Latest. 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-v2 | abd1a90 | Older copy of the same branch, 8 commits behind (no multi-node Express, old LibreSSL/curl, no avatar on friend requests). |
| decentnetworks/…/master and 0xli/…/master | fb830b0 / 19aac23 | Pre-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 installDependency 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:
| Path | Contents |
|---|---|
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.conf | The 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 -lcarriersessionInitialize
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
killedThe 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:
| Field | Meaning |
|---|---|
persistent_location | Data directory (required). |
secret_key | Optional 32-byte identity secret. Used only when the directory has no data yet (import an identity). |
udp_enabled | false forces TCP only. |
log_level, log_file, log_printer | CarrierLogLevel_None … CarrierLogLevel_Verbose; log to stdout by default. |
bootstraps, bootstraps_size | DHT bootstrap nodes (ipv4, ipv6, port, public_key, all strings). |
express_nodes, express_nodes_size | Express (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: 1Profile: 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 onlineOther 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: ByFriendIncoming 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=pongGroups 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 groupFiles
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 closedFor 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:
- Both sides call
carrier_session_init(carrier). The receiving side registerscarrier_session_set_callback(carrier, NULL, on_request, ctx). - Caller:
carrier_session_new(carrier, peer_userid), thencarrier_session_add_stream(...). Wait forCarrierStreamState_initialized, then callcarrier_session_request(session, NULL, on_complete, ctx). - Callee, in or after
on_request(from, bundle, sdp, len):carrier_session_new, then the sameadd_stream. Wait forinitialized, callcarrier_session_reply_request(session, NULL, 0, NULL), wait fortransport_ready, then callcarrier_session_start(session, sdp, len). - Caller, in
on_complete(…, status == 0, sdp, len): wait fortransport_ready, then callcarrier_session_start(session, sdp, len). - Both streams reach
CarrierStreamState_connected. Then usecarrier_stream_writeand thestream_datacallback.
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 streamSend 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 awayThreading and lifecycle
carrier_newcreates the node and loads or createscarrier.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 untilcarrier_killis called, then savescarrier.dataand returns 0.interval0 means 1000 ms. Run it on a dedicated thread, as in the Quickstart.CarrierCallbackscallbacks, message receipts and invite responses are called from insidecarrier_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_runthread. Protect shared state. - Calls such as
carrier_send_friend_messageandcarrier_invite_friendfail withERROR_NOT_BEING_READYuntil the node is ready. Wait for thereadycallback orcarrier_is_ready(). - Shutdown: close file transfers and sessions, call
carrier_filetransfer_cleanup/carrier_session_cleanup, thencarrier_kill(carrier)and join the run thread. Aftercarrier_killthe 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 offlinePlatforms
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.
| Target | How | Status |
|---|---|---|
| macOS arm64 / x86_64 | host build (above) | arm64 verified 2026-11-06 |
| Linux (Ubuntu/Debian) | host build, same commands | per 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 device | per README; not run |
| Windows x86/x64 | CMake + Visual Studio, per README | per 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
| Function | Signature |
|---|---|
| version | const char *carrier_get_version(void) |
| validate | bool 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) |
| lifecycle | Carrier *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) |
| identity | char *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 *) |
| signatures | int 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]) |
| profile | int 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 info | int 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 *) |
| friends | int 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 *) |
| messages | int carrier_send_friend_message(Carrier *, const char *to, const void *message, size_t len, uint32_t *msgid, CarrierFriendMessageReceiptCallback *cb, void *context) |
| invites | int 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) |
| groups | int 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) |
| errors | int 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
- Release:
v6.1.0, commitfd7dfc8ec1929303cf9b335815847fc0e4caa0af(2026-11-08). - Repository: 0xli/Elastos.NET.Carrier.Native.SDK (public, GPLv3), branch
feat/agentnet-proto-v2for the latest. The API on this page was read at commite8eb0b0f18a1b55d7ede08de9482bc51d080584e(2026-11-01);v6.1.0changes only thec-toxcoredownload URL. - Protocol notes on the branch:
docs/AGENTNET_PROTOCOL.md,docs/BUILD-DEPS.md,docs/REBUILD-IOS-ANDROID.md,CHANGELOG.md, and the shell command listCOMMANDS.md. - Mirror: decentnetworks/Elastos.NET.Carrier.Native.SDK (default branch
master; itsfeat/agentnet-proto-v2is behind, see the table above).