GitHub
TransversePATH FINDER

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"
}
FieldRequiredNotes
identifier_typeyesphone, email, bank_account, lei, reg, tax, address
identifierone ofRaw value — the operator normalises and hashes it
identifier_hashone ofA hash computed under the same pepper version
noncenoBound 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.

On this page