API reference
Authentication
Request signing — how to build the signature, why it is not a bearer token, and the constraint that follows.
Signed requests, not bearer tokens
A bearer key travels on every call, so it ends up in logs, in proxies, and in a mobile binary somebody decompiled. A signature proves possession without transmitting the secret, and binds the proof to this request — a captured header is worthless for the next one.
These calls come from your server, never from a client application. A credential shipped inside an app is public the moment someone opens the binary. Your app talks to your backend; your backend talks to the operator.
The headers
Path-Key-Id: member_2026_01
Path-Timestamp: 2026-09-09T10:00:00.000Z
Path-Signature: 3fa8c1…Building the signature
Ed25519 over:
METHOD \n PATH \n TIMESTAMP \n SHA256_HEX(BODY)METHODuppercase:POSTPATHwithout the query string:/api/path/v1/sonar/lookupTIMESTAMPexactly as sent inPath-TimestampSHA256_HEX(BODY)of the serialised body, or of the empty string when there is none
import { ed25519 } from '@noble/curves/ed25519';
import { sha256 } from '@noble/hashes/sha256';
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
const body = JSON.stringify({ identifier_type: 'phone', identifier: '+221771234567' });
const timestamp = new Date().toISOString();
const message = new TextEncoder().encode(
['POST', '/api/path/v1/sonar/lookup', timestamp, bytesToHex(sha256(new TextEncoder().encode(body)))].join('\n'),
);
const signature = bytesToHex(ed25519.sign(message, hexToBytes(privateKeyHex)));The SDK does this for you. Build it by hand only when porting to another language.
Why the body digest is in there
Without it, a captured signature authorises a different body on the same route. The digest binds the signature to the exact payload.
Freshness
Timestamps must fall within five minutes of the operator's clock. Outside that, the request is
rejected with path.auth.unauthenticated.
Clock skew is the single most common cause of authentication failures here, and it is far more likely than an attack. Check your server's time before anything else.
Credentials
A network issues one to each member: a kid and an Ed25519 key pair. The member keeps the private
key; the network publishes nothing but the public half.
Rotation is publish-then-switch. Register the new public key, start signing with the new kid,
then retire the old one once nothing in flight still uses it.
Revocation is immediate. A revoked credential returns path.auth.credential_revoked, which is
distinct from unauthenticated on purpose: one means stop and get a new credential, the other means
check your clock.
Member status
A credential can be valid while its member is not.
| Status | Directory calls |
|---|---|
candidate | Refused — path.auth.member_not_active |
member | Allowed |
suspended | Refused, reversibly |
excluded | Refused, permanently |
path.auth.member_not_active is a membership matter, not a technical one. Retrying will not fix it.
Public routes take no credential
Discovery, reading a payment request, reading a receipt, resolving a known address, verifying a receipt. Do not send credentials to them. There is no benefit, and it puts a secret on a request that did not need one.