GitHub

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)
  • METHOD uppercase: POST
  • PATH without the query string: /api/path/v1/sonar/lookup
  • TIMESTAMP exactly as sent in Path-Timestamp
  • SHA256_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.

StatusDirectory calls
candidateRefused — path.auth.member_not_active
memberAllowed
suspendedRefused, reversibly
excludedRefused, 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.

On this page