API reference
SONAR
Step 1 of reachability — which member holds a key. Endpoints, budgets, and the answer shape that is deliberately uninformative.
Answers which member holds this key and nothing else. What the destination accepts is RESOLVER, answered by the holder.
Requires a member credential. Called from your server — see Authentication.
POST /sonar/lookup
{
"identifier_type": "phone",
"identifier": "+221771234567",
"nonce": "9f2c41a8b7e3"
}| Field | Required | Notes |
|---|---|---|
identifier_type | yes | phone, email, bank_account, lei, reg, tax, address |
identifier | one of | Raw value — the operator normalises and hashes it |
identifier_hash | one of | A hash computed under the same pepper version |
nonce | no | Bound into the signed answer, making it non-replayable |
Response
{
"kind": "sonar_answer",
"requester": "your-member",
"nonce": "9f2c41a8b7e3",
"found": true,
"member": "member-b",
"endpoint": "https://api.member-b.com",
"protocol_version": "0.1.0",
"signed_at": "2026-09-09T10:00:00Z",
"kid": "net_example_2026_01",
"signature": "…"
}Keep the envelope. Months later, in a dispute, it is the difference between a recollection and a document.
The negative
{ "kind": "sonar_answer", "found": false, "member": null, "endpoint": null, "…": "…" }This is returned identically for a key that does not exist and for one that exists but is not visible to you — same status, same shape, same timing. Do not build logic that tries to tell them apart; the indistinguishability is deliberate, and a route that leaked the difference would answer "is this person a customer of somebody" to anyone patient enough to ask.
Budgets
Lookups consume a reciprocity budget, computed from what your member contributes to the index — not a requests-per-second ceiling.
{
"error": {
"code": "path.finder.budget_exhausted",
"message": "Reciprocity budget exhausted for this identifier type",
"detail": { "allowance": 50000, "window": "calendar_month" }
}
}Retrying more slowly does not help: the allowance is monthly. Look up fewer things, or talk to the network.
email carries its own budget. The threat there is replay of a breach list, not slow enumeration,
so the numbers are shaped differently.
Logging
Every call is recorded against your member, with the key, never with the identifier in the clear. Retention is published in the discovery document.
POST /sonar/lookup/batch
Contact synchronisation. Capped explicitly — typically 500 entries.
{
"items": [
{ "identifier_type": "phone", "identifier": "+221771234567" },
{ "identifier_type": "phone", "identifier": "+221781234567" }
]
}{
"count": 2,
"results": [
{ "identifier_type": "phone", "found": true, "member": "member-b", "endpoint": "https://…" },
{ "identifier_type": "phone", "found": false, "member": null, "endpoint": null }
]
}The cap does not fix the practice. Sending an entire address book on install exposes every contact a user has, most of whom will never be paid — and burns the budget you will want later. Look a number up when someone actually wants to pay it: same functionality, orders of magnitude less exposure, no cryptography required.
POST /sonar/confirm
Served by the member, not the network. Answers is this key yours, for this requester?
{
"identifier_type": "phone",
"identifier_hash": "8f2b38dafde5e23f…",
"requester": "member-a",
"nonce": "9f2c41a8b7e3"
}{
"kind": "holder_confirmation",
"identifier_type": "phone",
"identifier_hash": "8f2b38da…",
"member": "member-b",
"requester": "member-a",
"nonce": "9f2c41a8b7e3",
"held": true,
"kid": "op_member_b_2026_01",
"signature": "…"
}Three properties, in order of importance:
The answer stops resting on the network's word. It carries the holder's own signature.
The holder gets a per-request veto, under its own policy — the customer's discoverability level, the requester's reputation, its own caps.
The index may be stale without the answer being wrong. A customer who left last week produces a
held: false rather than a misdirected payment.
The signature covers the key, member, requester, timestamp and nonce — never the capability. Routing that through the network would hand it exactly what the two-step split withholds. Without the requester and nonce, the confirmation would be replayable by anyone who saw it once.