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.
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:
How the pepper works and how it's delivered lives on the partners page. This reference is how you use it.
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:
value_hash, so your hashes join everyone else's on the network.pip install adoornpm install @adoor/sdkLookup — 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.
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()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
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.
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
)Pre-hashed inputs
Already hashing elsewhere — the offline Edge Hasher, a data pipeline, or your own implementation of the algorithm? Send digests straight through.
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.
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 -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" }
}'{
"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 -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"
}'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.
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")Errors
Errors are RFC 7807 application/problem+json. The SDK raises a typed hierarchy off AdoorError:
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.
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")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.