This Linux C++ example checks registration of a password-authenticated IllyVoIP SIP line using PJSUA2, then unregisters and exits. It is a small diagnostic starting point for a native application, not a complete softphone or a Portal API client. It does not place, answer or record calls.

1. Choose an appropriate test line
In IllyVoIP, open Services → SIP Lines, select the intended password-authenticated line and expand Technical endpoints. Copy its SIP username, SIP password, regional hostname, destination port and transport. These are not your Portal login credentials. Keep any existing working devices and IP-authenticated configuration intact.
Use a dedicated development line without an incoming public-number route. This example creates an unencrypted UDP signaling transport, supports hostname/IPv4 server values and deliberately does not implement TLS, audio devices or call handling. Run it only where UDP is explicitly supported by the line and permitted by your network policy. Do not downgrade a TLS-only account to run this example.
Registration changes where a line can be reached while the binding exists. Avoid using a credential shared with production phones. The program does not initiate paid calls, but registration is still a real account operation when pointed at your provider.
2. Build PJSIP in a local directory
The example was compiled against the official PJSIP 2.16 source release. On Linux, install your distribution’s C/C++ build tools and pkg-config, download and unpack that release from the official project, then run these commands inside its source directory. Choose a directory you own; no system-wide installation is needed.
./configure --prefix="$PWD/local-install" --disable-sound --disable-video
make dep
make -j2
make install
export PKG_CONFIG_PATH="$PWD/local-install/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
Keep the source/build directory until the example is compiled. The local prefix contains the headers and libraries used below. Disabling sound and video is intentional for this registration-only test; it is not a recipe for a calling application.
3. Save a private five-line configuration
Create a file readable only by your user. The following commands create an empty protected file; edit it locally rather than putting a password in a shell command or command-line argument.
umask 077
touch sip-test.conf
chmod 600 sip-test.confWrite exactly five lines, in this order, with a final newline and no extra blank line:
| Setting | Value or action |
|---|---|
| Line 1 | Registrar hostname, without sip:, port, path or transport parameter. |
| Line 2 | The account’s UDP destination port as a decimal number. |
| Line 3 | SIP identity/username. |
| Line 4 | SIP authentication username, even when it equals line 3. |
| Line 5 | SIP password. |
Copy actual account values; do not use the Portal password. This intentionally narrow example accepts simple ASCII usernames and hostnames, and rejects URI parameters, whitespace in identity fields, IPv6 literals and extra configuration lines. If your supplied identity requires a broader grammar, extend and test URI construction rather than removing validation.
4. Save and compile the example
Save this complete source as register.cpp. It handles callbacks on the main thread, bounds registration and unregistration waits, and destroys the account before the endpoint. Library message logging is disabled so routine output does not include credentials or SIP authorization data.
#include <pjsua2.hpp>
#include <chrono>
#include <fstream>
#include <iostream>
#include <regex>
#include <string>
using namespace pj;
struct Registration : Account {
bool registered = false, rejected = false, stopped = false;
void onRegState(OnRegStateParam &) override {
auto i = getInfo();
registered = i.regIsActive;
if (i.regStatus >= 300) rejected = true;
if (i.regStatus == 200 && !i.regIsActive) stopped = true;
std::cout << "Registration status " << i.regStatus
<< ", active=" << i.regIsActive << '\n';
}
};
int main(int argc, char **argv) {
if (argc != 2) { std::cerr << "Usage: register PRIVATE_CONFIG_FILE\n"; return 2; }
std::ifstream in(argv[1]);
std::string host, port, identity, auth, password, extra;
if (!in || !std::getline(in,host) || !std::getline(in,port) ||
!std::getline(in,identity) || !std::getline(in,auth) ||
!std::getline(in,password) || std::getline(in,extra)) {
std::cerr << "Expected exactly five configuration lines\n"; return 2;
}
const std::regex hostname(R"(^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$)"),
user(R"(^[A-Za-z0-9_.+-]+$)"), digits(R"(^[0-9]{1,5}$)");
if (host.size()>253 || !std::regex_match(host,hostname) ||
!std::regex_match(port,digits) || std::stoi(port)<1 || std::stoi(port)>65535 ||
identity.size()>128 || auth.size()>128 || !std::regex_match(identity,user) ||
!std::regex_match(auth,user) || password.empty() || password.size()>512 ||
password.find_first_of("\r\n\0",0,3)!=std::string::npos) {
std::cerr << "Invalid configuration; values were not printed\n"; return 2;
}
Endpoint ep;
bool created=false;
int result=1;
try {
ep.libCreate(); created=true;
EpConfig config;
config.uaConfig.threadCnt=0;
config.uaConfig.mainThreadOnly=true;
config.logConfig.level=0;
config.logConfig.consoleLevel=0;
config.logConfig.msgLogging=0;
ep.libInit(config);
TransportConfig transport;
transport.port=0;
ep.transportCreate(PJSIP_TRANSPORT_UDP,transport);
ep.libStart();
{
Registration account;
AccountConfig ac;
const auto server=host+":"+port;
ac.idUri="sip:"+identity+"@"+host;
ac.regConfig.registrarUri="sip:"+server+";transport=udp";
ac.regConfig.timeoutSec=60;
ac.regConfig.retryIntervalSec=0;
ac.sipConfig.authCreds.push_back(AuthCredInfo("digest","*",auth,0,password));
account.create(ac);
auto pump = [&](auto predicate,int seconds) {
auto end=std::chrono::steady_clock::now()+std::chrono::seconds(seconds);
while (!predicate() && std::chrono::steady_clock::now()<end) ep.libHandleEvents(50);
return predicate();
};
pump([&]{return account.registered || account.rejected;},10);
if(account.registered) {
std::cout<<"Registered; requesting removal of this binding\n";
account.setRegistration(false);
const bool confirmed=pump([&]{return account.stopped;},3);
std::cout<<(confirmed ? "Unregistration acknowledged\n" : "Unregistration unconfirmed; check registrar expiry\n");
result=confirmed?0:3;
} else {
std::cout<<"Registration failed or timed out\n";
result=1;
}
account.shutdown();
}
ep.libDestroy();created=false;
} catch(const Error &) {
// Do not print library exception text, which can contain account details.
std::cerr<<"SIP library operation failed; inspect sanitized local diagnostics\n";
if(created) { try { ep.libDestroy(); } catch(...) {} }
result=1;
}
return result;
}
With PKG_CONFIG_PATH still pointing to the local installation, compile and run:
g++ -std=c++17 -O2 register.cpp -o register $(pkg-config --cflags --libs --static libpjproject)
./register sip-test.conf
The argument contains only the configuration file path. Keep the file out of version control, backups shared with others and screenshots. Protect the process and host too: a private file does not defend against an administrator or a compromised user session.
5. Interpret registration and shutdown
PJSUA2 handles a normal digest challenge using the authentication credential. A final rejected registration or a ten-second registration timeout produces failure. Successful registration is followed immediately by a request to remove this binding; the program waits up to three seconds for acknowledgment.
| Setting | Value or action |
|---|---|
| Exit 0 | Registration succeeded and unregistration was acknowledged. |
| Exit 1 | Registration failed, timed out or a SIP library operation failed. |
| Exit 2 | The file or its values did not meet the example’s input requirements. |
| Exit 3 | Registration succeeded, but removal was not acknowledged within the wait. Check the registrar’s current binding and expiry. |
After a timeout, do not assume the server never accepted a request: a response may have been lost. Likewise, local account shutdown does not prove a remote binding disappeared. Inspect the line’s current state before repeating tests, and allow the server’s granted expiry to take effect if removal cannot be confirmed. Avoid tight retry loops.
6. Diagnose the failing layer
- Compilation cannot find pjsua2.hpp: check the local installation path and pkg-config output.
- No response: check DNS, the actual destination port, transport and permitted network path. Do not open every SIP/RTP port.
- Authentication rejection: distinguish SIP identity from authentication username and confirm password authentication is enabled for the line.
- Registration works but calling is unimplemented: this program has no Call subclass, audio device routing or application call state machine. Registration success is not a two-way audio test.
A production client needs separate reviewed handling for incoming/outgoing calls, registration renewal and reconnection, shutdown during active calls, secure secret storage, TLS certificate verification where required, and the account’s supported media encryption. Do not infer those features from this diagnostic.
What was tested
The source was compiled locally against PJSIP 2.16 and exercised with a loopback SIP digest registrar: successful registration/removal, different identity and authentication usernames, a rejected password, a lost unregistration response, registration timeout and invalid input. These tests do not establish live IllyVoIP registration, TLS interoperability or device audio acceptance.
Documentation and scope
PJSIP official 2.16 source; PJSIP build instructions. Reviewed October 11, 2026. Manufacturer instructions and current IllyVoIP settings inform this guide; physical-device and live-provider acceptance must be completed on your installation.