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": "+12025550123",
"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",
"identifier_type": "phone",
"identifier_hash": "8f2b38dafde5e23f…",
"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. Do not build logic that tries to tell them apart.
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.
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
Many keys, one call. Capped explicitly — typically 500 entries.
The budget is charged and the journal written per key. Batching saves round trips, not allowance.
{
"items": [
{ "identifier_type": "phone", "identifier": "+12025550123" },
{ "identifier_type": "phone", "identifier": "+12025550199" }
]
}{
"count": 2,
"signed": false,
"results": [
{ "identifier_type": "phone", "identifier_hash": "8f2b38da…", "found": true, "member": "member-b", "endpoint": "https://…" },
{ "identifier_type": "phone", "identifier_hash": "c14e07b2…", "found": false, "member": null, "endpoint": null }
]
}The hash comes back so you can map results to what you sent without keeping the identifiers around.
signed: true
Returns one signed envelope per key, identical in shape to a single lookup — so
/sonar/confirm takes them unchanged, and each holder receives only the
entry that concerns it.
{ "items": [ … ], "signed": true }Leave signed off for a reachability list. Use signed: true when each answer may have to be
shown to the holder that will receive the money. Confirm accepts those envelopes as they are.
Look a key up when a payment is actually being prepared.
POST /sonar/confirm
Served by the member, not the network. Answers is this key yours?
Takes the signed SONAR answer, and only that. No credential, no loose fields.
{
"sonar_answer": {
"kind": "sonar_answer",
"identifier_type": "phone",
"identifier_hash": "8f2b38dafde5e23f…",
"requester": "member-a",
"nonce": "9f2c41a8b7e3",
"found": true,
"member": "member-b",
"endpoint": "https://api.member-b.com",
"signed_at": "2026-09-09T10:12:04Z",
"kid": "net_openconnect_2026_01",
"signature": "…"
}
}The envelope must verify, be less than five minutes old, and name this member as the holder.
Why the envelope
Confirmation is a cross-network check. The caller holds a signed answer from a lookup; it may not
hold a credential here. requester is taken from that envelope.
A plain batch answer is unsigned and cannot be used here. Re-query the key, or batch with
signed: true.
{
"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 — not the capability.
The SONAR answer carries identifier_type and identifier_hash in the signed payload.