Reference
The lookup data a risk decision needs but cannot derive: which email domains hand out throwaway inboxes, which addresses belong to a datacentre or a Tor exit, which card scheme an issuer prefix belongs to, which browsers the fleet sees everywhere, and how current the designation lists the screening engine holds actually are.
Also for this capability: API · CLI · MCP · SDKs
The lookup data a risk decision needs but cannot derive: which email domains hand out throwaway inboxes, which addresses belong to a datacentre or a Tor exit, which card scheme an issuer prefix belongs to, which browsers the fleet sees everywhere, and how current the designation lists the screening engine holds actually are.
| Base URL | https://api.hanzo.ai |
| Operations | 5 |
| Auth | Authorization: Bearer $HANZO_API_KEY |
Specification
HIP-1262 · Reference — Lookup Sets — Draft · read the specification →
/v1/reference is the lookup data a risk decision needs but cannot derive:
which email domains hand out throwaway inboxes, which addresses belong to a
datacentre or a Tor exit, which card scheme an issuer prefix belongs to, which
browsers the fleet sees everywhere, and how current the designation lists the
screening engine holds actually are (apps/reference/reference.go:15-19). It is
implemented in hanzoai/cloud at apps/reference. This HIP is where HIP-1046
§8's subtree now lives; HIP-1046 keeps the cross-plane invariants.
Motivation
Every one of these facts could be hard-coded where it is consulted, and each copy would go stale on its own schedule with nobody noticing — a designation list that answers "not listed" because it was never loaded is indistinguishable from a clean world. Making the lookup a capability puts freshness on the wire and makes staleness a reported condition rather than a silent one.
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.
Two stores, one precedence
The Hanzo-maintained BASELINE lives in the shared warehouse —
hanzo.reference_source and hanzo.reference_entry, tables with NO tenant
column at all, so a cross-tenant write is unrepresentable rather than merely
forbidden (apps/reference/store.go:47-49). A tenant's OWN allow and deny
entries live in that organization's own SQLite file, opened per organization
through cloud.OrgStore under the name reference
(apps/reference/override.go, apps/reference/reference.go:34-40).
Resolution MUST be override first, then baseline; first hit wins.
Nothing derived from one organization's rows may enter the baseline, ever. The
only fleet-derived set is an aggregate published above a k-anonymity floor no
single organization can reach alone (apps/reference/set.go:297-299).
The address
The capability answers under /v1/reference: the set list at the root, one set
read, written and cleared at /{set} — a SET, which is why the name is singular
— resolve, and refresh (apps/reference/reference.go:507-525). Every
operation is typed. refresh writes the shared baseline every organization
reads and is therefore SuperAdmin only (apps/reference/reference.go:1328-1329);
it MUST stay one operation under this prefix, never an address family of its own. Today's router still serves
this surface under /v1/risk/reference; that pair is carried by
hanzoai/cloud openapi/misfiled.txt and closes by fold.
A version, a refusal, and freshness on the wire
Every set is a VERSION — the content digest of what was taken, so the same
data is the same version whoever fetched it, and a decision records one string
an auditor resolves back to a publisher, a licence and a date. A set that has
never loaded MUST REFUSE rather than answer "not listed"
(apps/reference/resolve.go:157), and a set whose source needs a licence we do
not hold is declared as a seam that refuses (apps/reference/set.go:93-95).
Freshness rides every answer: the version, when its oldest contributing
publisher was current — the oldest, because a set is exactly as fresh as its
weakest source (apps/reference/resolve.go:71-73) — how old that is, and
whether it is past bound. A stale set still answers, and says that it did.
Tenant, meter, events, observability, stage
Reads and override writes resolve the organization from the validated
principal (apps/reference/reference.go:549) and a request without one is
refused; the baseline is readable by every validated org and writable by none
of them. The capability is free, in those words
(plugin/reference/main.go:21, Price: cloud.Free). It publishes no events on
the bus. Beyond the request span it registers nothing; staleness and refusal —
the two ways this plane can be quietly wrong — are reported on the wire rather
than in a private metric. Its stage is whatever its manifest row declares —
HIP-0139 §8 keeps that in one place, and the beta this line used to assert had
already drifted from it.
Upstreams
It forks no code; the refusal discipline generalises what luxfi/aml
pkg/reference states (apps/reference/reference.go:25-28). What it MIRRORS
is published data, and the catalog declares each source's licence as a fact
beside its origin (apps/reference/set.go):
disposable-email-domains— CC0-1.0 (set.go:221-223);crawler-user-agents— MIT (set.go:255-257);- the Tor bulk exit list — CC BY 3.0 US (
set.go:242); - the IANA special-purpose address and AS-number registries — the registries of
record (
set.go:243-244,:269-271); - cloud and CDN operators' self-published IP feeds — machine-readable by intent,
no licence stated and none claimed (
set.go:234-241); - issuer prefixes — computed here from ISO/IEC 7812 structural facts; no issuer
database is licensed or held (
set.go:283-285); - sanctions designations — receipt only; they stay with the engine that screens
(
set.go:310-313).
A new source MUST enter through this catalog with its licence basis declared, or as a seam that refuses.
Rationale
The alternative was to file these operations under the model-serving plane, and
an earlier cut did exactly that (manifest/apps.go:231-238): lookup data in a
live product with different customers, where its growth reads as that product's
growth. The other alternative — folding into risk — fails on the store rule:
this capability owns two stores of its own, and the baseline's no-tenant-column
shape is a property risk's per-tenant planes cannot host.
Security Considerations
The baseline is consulted by every organization's decisions, so poisoning it is
the high-value attack: refresh is held to the platform's own identity, a
version is a content digest so a tampered take is a different version by
construction, and nothing an organization sends can reach the baseline tables —
their shape has nowhere for it to go. The override plane is the tenant-side
exposure: a per-org file reached only through the validated principal, so one
organization's allow-list cannot bleed into another's decisions. The remaining
risk is absence — an unloaded, unlicensed or stale set silently passing for a
clean answer — closed by refusing the first two and reporting the third.
Four surfaces
| Surface | Reaches this capability as | Coverage |
|---|---|---|
| REST | reference at its own prefix | 5 operations |
| CLI | hanzo reference … | 5 of 5 |
| SDK | ReferenceApi in every published client | 5 methods |
| MCP | tool reference on https://api.hanzo.ai/v1/mcp | 5 operations |
Quickstart
export HANZO_API_KEY=sk-... # console.hanzo.ai → API keysThen the first call — a read that needs nothing but the key. GET /v1/reference, operation riskReferenceSets:
hanzo reference listimport { Configuration, ReferenceApi } from 'hanzoai';
const api = new ReferenceApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.riskReferenceSets();from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import ReferenceApi
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = ReferenceApi(client).risk_reference_sets()cfg := hanzoai.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := hanzoai.NewAPIClient(cfg)
resp, _, err := client.ReferenceAPI.RiskReferenceSets(context.Background()).Execute()
if err != nil {
return err
}use hanzo_client::apis::{configuration::Configuration, reference_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = reference_api::risk_reference_sets(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.ReferenceApi;
ApiClient client = new ApiClient();
client.setBearerToken(System.getenv("HANZO_API_KEY"));
var result = new ReferenceApi(client).riskReferenceSets();curl https://api.hanzo.ai/v1/reference \
-H "Authorization: Bearer $HANZO_API_KEY"Tool reference, op riskReferenceSets — POST the JSON-RPC envelope to https://api.hanzo.ai/v1/mcp.
curl -X POST https://api.hanzo.ai/v1/mcp \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "reference",
"arguments": {
"op": "riskReferenceSets",
"input": {}
}
}
}'Answers 200 with object — ok.
Endpoints
| Endpoint | What it does |
|---|---|
GET /v1/reference/{set} | Describes one set and lists your org's overrides in it. |
PUT /v1/reference/{set} | Writes your organisation's own allow and deny entries over a set. |
DELETE /v1/reference/{set} | Removes one of your organisation's overrides. |
POST /v1/reference/resolve | Looks keys up against the reference plane. |
GET /v1/reference | Lists every set this plane publishes, with its version and how fresh it is. |