Watchlists, developer keys, webhooks, saved queries, graph cases, knowledge
submissions and node RPC execution belong to an owner. The owner is the
holder of an API key issued by this backend. There is no user_id anywhere:
a body or query field with that name is ignored, and a resource that belongs
to another owner answers 404.
POST /api/v1/intelligence/developer/owners
{ "name": "Ingestion service" }Answers 201 with the first key. secret_key is shown once and is never
stored in clear. The owner id is generated by the server. Creation is
bounded per caller address (10 per hour).
{ "key_id": "...", "owner_id": "...", "name": "Ingestion service",
"scopes": ["read", "watchlists", "webhooks", "queries", "cases", "knowledge", "node:rpc", "keys:manage"],
"secret_key": "uip_live_...", "storage": "durable" }Authorization: Bearer uip_live_...X-Api-Key is accepted too. A missing or unknown key answers 401; a key
without the needed scope answers 403.
| Scope | Grants |
|---|---|
read |
Reads that are owner scoped |
watchlists |
/api/v1/intelligence/watchlists/* |
webhooks |
/api/v1/intelligence/developer/webhooks* |
queries |
/api/v1/intelligence/query/saved |
cases |
/api/v1/intelligence/graph/cases* |
knowledge |
POST /api/v1/intelligence/knowledge/labels, challenges |
node:rpc |
POST /api/v1/node/rpc (still bounded by the read-only allowlist and per-method budget) |
keys:manage |
Minting and revoking further keys for the same owner, with a subset of the caller's scopes |
* cannot be minted through the API. An operator key with every scope can
be provided out of band through UNIVERSE_INTELLIGENCE_LEGACY_MASTER_KEY;
nothing is seeded from source.
Keys are stored as an HMAC-SHA256 of the secret under a pepper that is
either UNIVERSE_INTELLIGENCE_KEY_PEPPER or, when unset, generated once and
kept in intelligence_settings. Webhook signing secrets are AES-256-GCM
encrypted with UNIVERSE_INTELLIGENCE_SECRET_KEY (32 bytes hex) or a key
derived from the pepper. All owner rows carry owner_id and network;
the network is the backend's own.
Without a database (DATABASE.ENABLED=false) the same contract runs in
memory and every owner response reports storage: "memory"; nothing
survives a restart.
POST /api/v1/intelligence/developer/webhooks
{ "target_url": "https://hooks.example.org/universe", "events": ["watchlist.notification"] }Targets must be https, must not carry credentials, and must resolve only
to public addresses; every resolved address is checked again at delivery
time and the connection is pinned to it. A delivery is one HTTPS POST
with headers X-Universe-Event-Id, X-Universe-Timestamp and
X-Universe-Signature: v1=<hex> where the signature is
HMAC-SHA256(secret, timestamp.body). Only a 2xx is a success; redirects,
timeouts, connection errors and other statuses are failures that are
retried from a leased outbox with capped backoff, up to eight attempts.
GET /api/v1/intelligence/developer/webhooks/:id/attempts lists what
actually happened.
Entities are stored as SHA-256 of the raw identifier. Pass "blinded": true
to submit a hash you computed yourself. Rules: confirmation,
value_transfer (optional threshold_value in sats), rbf_replacement,
feerate_cross (threshold_value in sat/vB), reorg_displaced. The
matcher runs on every block the backend processes and on every replacement
the RBF cache records; a notification is unique per rule and event, a
re-observed block produces nothing new, and a reorg marks displaced
confirmations. GET /api/v1/intelligence/watchlists/notifications lists the
owner's notifications; POST .../notifications/:id/ack acknowledges one.