Adoor · API referenceRequest access →
Adoor developer platform

Build with Adoor.

Two calls carry the product: lookup() scores an identifier before money moves, and report() contributes confirmed fraud so the network protects everyone. Identifiers are hashed inside your own infrastructure — only one-way digests ever reach Adoor.

POSThttps://ghapi.masenu.app/v1/lookups
Base URL
https://ghapi.masenu.app
API base
https://ghapi.masenu.app/v1
Auth
Bearer sk_live_…
Format
JSON · RFC 7807 errors
Hash at the edge. Identifiers reach Adoor only as one-way keyed digests — the SDK never transmits a raw phone number, account, email or ID. You cannot leak what you never send.
01/Onboarding

Before you start

Integration begins after your pilot agreement. Your onboarding pack delivers four secrets — keep every one in your secrets manager, never in source or a browser:

API key · sk_live_…
Authenticates your org. Scoped to lookups:read and reports:write.
Test key · sk_test_…
A non-production key for integration work. Scope and protect it like a live key.
Consortium pepper
The shared secret that keys your edge hash so it joins the network's. Versioned (pepper_v).
Webhook signing secret
Verifies exposure-alert callbacks (X-Adoor-Signature).

How the pepper works and how it's delivered lives on the partners page. This reference is how you use it.

02/Integrate

Install the SDK

The official SDKs are zero-dependency — hashing and both calls are built in, and nothing new enters your supply chain. Three ways in:

    SDK · recommended
    Any backend that can add a package. ~an afternoon.
    Raw REST
    Stacks that can't add a dependency, or non-Python/JS runtimes. Hash + HTTP yourself.
    Offline Edge Hasher
    Compliance-restricted machines or no-engineer teams: hash a CSV in the browser, submit the hashed JSON.
One hash, every on-ramp. The Python SDK, the TypeScript SDK, the offline Edge Hasher and the server each carry their own copy of the hashing algorithm — held byte-identical by a parity suite that runs on every commit, pinned to shared golden vectors and checked against the server's own code. Whichever path you pick, the same real-world identifier produces the same value_hash, so your hashes join everyone else's on the network.
python · 3.10+, stdlib only
pip install adoor
typescript · node 18+, bun, deno, workers, browser
npm install @adoor/sdk
03/Decide

Lookup — before money moves

Pass raw values; the SDK normalizes and HMAC-hashes them locally with your pepper and sends only the digest. Score before a payout, transfer, or onboarding.

python
from adoor import AdoorClient

client = AdoorClient(
    api_key="sk_live_...",         # from your onboarding pack
    pepper="<consortium-pepper>",  # what makes your hashes join the network's
)

decision = client.lookup(msisdn="0244123456", momo_wallet="0244123456")

print(decision.recommended_action)  # "allow" | "review" | "step_up" | "block"
print(decision.risk_band)           # "low" | "medium" | "high" | "critical"
for reason in decision.explanations:
    print("-", reason)

if decision.should_block:
    hold_transaction()
elif decision.needs_review:         # review | step_up
    send_to_manual_review()
typescript
import { AdoorClient } from "@adoor/sdk";

const client = new AdoorClient({ apiKey: "sk_live_...", pepper: "<consortium-pepper>" });

const decision = await client.lookup({ msisdn: "0244123456" });

if (decision.recommended_action === "block") holdTransaction();
decision.explanations.forEach((r) => console.log("-", r));

Decision fields

recommended_action
allow → review → step_up → block (ascending severity) — the field to branch on
risk_band
low / medium / high / critical
risk_score
0–100
explanations
human-readable reasons — log them for analysts and auditors
hits
per-identifier adverse matches
protective_registration
present if a consumer protectively registered an identifier (Adoor Guardian)
04/Contribute

Report — when you confirm fraud

Reporting keeps your lookup access on under give-to-get. The narrative stays private to your org; only sector + country are ever attributed. A new report protects every other member within seconds.

python
from datetime import UTC, datetime
from adoor import Identifier, FraudType, Confidence, Role

client.report(
    fraud_type=FraudType.SIM_SWAP,
    confidence=Confidence.CONFIRMED,
    occurred_at=datetime.now(UTC),
    identifiers=[
        Identifier("msisdn", "0244123456", Role.PERPETRATOR, country="GH"),
        Identifier("momo_wallet", "0552333003", Role.MULE, country="GH"),
    ],
    amount_minor=120_000, currency="GHS",   # GHS 1,200.00 in minor units
    narrative="SIM swap then drained the victim wallet.",  # org-private
)
05/Pipeline

Pre-hashed inputs

Already hashing elsewhere — the offline Edge Hasher, a data pipeline, or your own implementation of the algorithm? Send digests straight through.

python
client.lookup_hashed([
    {"kind": "msisdn", "value_hash": "4a89a523de05…", "pepper_v": 1},
])

No engineers on hand? The offline Edge Hasher is a single auditable HTML file that hashes a CSV with your pepper — no network, no install.

06/Any runtime

Raw REST

For Go, Java, .NET, or any stack that can't add a dependency: normalize and HMAC-SHA256 each identifier with your pepper, then call the endpoints directly. Send an Idempotency-Key on /v1/reports and a retry replays the original result instead of filing a duplicate.

curl · lookup
curl -sS https://ghapi.masenu.app/v1/lookups \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "identifiers": [
      { "kind": "msisdn", "value_hash": "4a89a523de05…", "pepper_v": 1 }
    ],
    "context": { "channel": "payout", "amount_minor": 410000, "currency": "GHS" }
  }'
200 · response
{
  "decision_id": "dec_9f3a...",
  "risk_score": 87,
  "risk_band": "high",
  "recommended_action": "step_up",
  "hits": [
    {
      "identifier_kind": "msisdn",
      "match": "exact",
      "report_count": 2,
      "fraud_types": ["sim_swap"],
      "first_reported": "2026-06-14",
      "last_reported": "2026-08-01",
      "reporter_profile": { "sectors": ["telco", "fintech"], "countries": ["GH"] }
    }
  ],
  "explanations": [
    "Exact match: 2 confirmed report(s) (sim_swap)",
    "Linked to 2 flagged identifiers",
    "Member of detected ring (7 identifiers)"
  ],
  "ruleset_v": "rules-1.0.0",
  "latency_ms": 8
}
curl · report
curl -sS https://ghapi.masenu.app/v1/reports \
  -H "Authorization: Bearer sk_live_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "fraud_type": "sim_swap",
    "confidence": "confirmed",
    "occurred_at": "2026-08-01T12:00:00Z",
    "identifiers": [
      { "kind": "msisdn", "value_hash": "4a89a523de05…", "pepper_v": 1,
        "role": "perpetrator", "country": "GH" }
    ],
    "amount_minor": 120000, "currency": "GHS"
  }'
07/Listen

Webhooks

Register an endpoint to receive signed events — exposure.alert (an identifier you cleared is later reported), watchlist.hit, ring.detected. Each delivery carries X-Adoor-Signature: t=<ts>,v1=<sig>. Verify the HMAC over t.body with your webhook secret and reject a clock skew over 5 minutes. Envelopes never carry raw identifiers or the reporter's identity.

python · verify
import hmac, hashlib, time

def verify(secret: str, header: str, body: str) -> None:
    parts = dict(p.split("=") for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > 300:
        raise ValueError("stale timestamp")
    mac = hmac.new(secret.encode(), f'{parts["t"]}.{body}'.encode(), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(mac, parts["v1"]):
        raise ValueError("bad signature")
08/Handle

Errors

Errors are RFC 7807 application/problem+json. The SDK raises a typed hierarchy off AdoorError:

AdoorAuthError
401 / 403 — key missing, revoked, or wrong scope
AdoorRateLimitError
429 — read e.retry_after (seconds) and back off
AdoorAPIError
4xx/5xx — e.status, e.detail (problem+json)
AdoorConnectionError
network / timeout — the SDK raises rather than guessing
Fail-open vs fail-closed. On a lookup timeout the SDK raises. Decide per flow whether an unreachable network means allow (availability) or hold (safety), and write it down — your auditors will ask.
09/Operate

Rate limits & retries

Each key gets a per-minute token bucket; over the limit returns 429 with Retry-After in seconds. report() derives an idempotency key from the payload digest, so retries never duplicate. Your onboarding pack includes a sk_test_ key for integration work — during the design-partner pilot we agree a test window and data-handling plan with your team rather than pointing you at a shared sandbox.

python · back off on 429
from adoor import AdoorRateLimitError

try:
    decision = client.lookup(msisdn="0244123456")
except AdoorRateLimitError as e:
    sleep(e.retry_after or 1)   # seconds, from the Retry-After header
    decision = client.lookup(msisdn="0244123456")
10/Trust

Security

Keys are stored as keyed HMAC-SHA256 digests and shown once. Every lookup, report and admin action lands in an append-only, hash-chained audit log. Member-private data is org-scoped with row-level security as a backstop, and the console requires TOTP. The pepper stays in your infrastructure — during the design-partner pilot the Masenu team pairs with yours and never takes custody of your pepper or raw data.

One deliberate exception, stated plainly: /v1/reports accepts an optional pii object for the dispute and law-enforcement workflow. It is never required, the SDKs never populate it, and it is sealed server-side under envelope encryption — a per-record data key wrapped with your public key when you register one, so the platform cannot unilaterally decrypt it. Identifiers themselves are always hash-only.