API Reference
Programmatic interface for any Unova Type-2 node. Generic data primitives — works equally well for supply chain, IoT telemetry, certificates, real estate, carbon credits, or any domain where you need a verifiable on-chain audit trail.
Overview
Every Type-2 node runs the same Hermes API. The launchpad shows you the URL on the node detail page (typically http://<your-droplet-ip>). You authenticate per-facility with a bearer token, then read and write assets and events against your node — which batches them into bundles published on-chain.
Core data model
Asset
A trackable entity with a unique on-chain identity.
Examples: A shipment container · A medical device · A real-estate parcel · An IoT sensor · A carbon credit certificate · A vehicle's VIN
Event
Anything that happened to or with an asset, signed and timestamped.
Examples: Container scanned at customs · Sensor reading · Title transferred · Carbon credit retired · Vehicle service record
Bundle
A batch of assets and events published on-chain. Provides cryptographic proof of integrity for the data inside.
Examples: Auto-generated by your node based on bundle settings (interval + min items)
Facility
A sub-identity of your company — typically a physical site or system that submits data.
Examples: A warehouse · An IoT gateway · A specific factory line · A regional office
Base URL
http://<your-type-2-droplet-ip>
Find the URL on your node's detail page at /nodes/mine. SSL/custom domain support is on the roadmap; today the API is exposed on plain HTTP port 80 of the droplet.
privateKey body of /auth/getApiTokenand every bearer token — are unencrypted in transit. The launchpad itself never sends your keys over this channel (it uses the node's SSH tunnel), but YOUR integrations do. Until node TLS ships: call the API from a trusted network path, treat leaked bearers as rotatable (mint fresh ones), and never reuse a facility key anywhere else. For public traffic put your own TLS proxy (nginx/Caddy) in front of port 80.Two services, two ports
A Type-2 node runs two HTTP services:
Hermes (REST API) — port 80: http://<node-ip>. Every endpoint group on this page (auth, assets, events, bulk, bundles, organizations, accounts, node info, analytics, metrics, admin) lives here.
Atlas worker — port 9876: http://<node-ip>:9876. Serves the Neo4j-backed /graphdb genealogy endpoints (and mirrors of some Hermes routes). Only the endpoints whose documented path starts with http://<node-ip>:9876 use this port.
Authentication header
Authorization: UNOVA_TOKEN <jwt>
Tokens are per-facility. Generate one in /data/company → Facilities → API token. The token you get back (from the launchpad or from /auth/getApiToken) is the entire header value — it already starts with UNOVA_TOKEN , so pass it as-is without prepending anything.
Response envelope
All responses are JSON, wrapped in this shape:
{
"data": [ /* result(s) — an array for lists, an object for single lookups */ ],
"meta": {
"code": 200,
"count": 42, // number of items in data (list endpoints)
"message": "..." // only present sometimes, e.g. "No results found"
},
"pagination": { /* present on paged list endpoints */ }
}On most endpoints the result count lives at meta.count and you iterate data directly. Three exceptions: GET /assets/:uniqueID/events returns {results, resultCount} without the envelope, POST /asset2/v2/assetSearch nests its paged result (items + cursors) inside data, and GET /bundle/:bundleId returns raw bundle JSON.
Read access
All asset, event, and bundle-listing reads require a valid bearer token. Anonymous requests get 401 Unauthorized.
The accessLevel filter is enforced: events with content.idData.accessLevelhigher than the caller's account accessLevel are hidden. super_account bearers bypass this and see everything.
Every Type-2 node deployed via the launchpad enforces this by default — no configuration needed.
Privacy at the bundle level: data with accessLevel > 0is automatically encrypted with your organization's key before being bundled. Other Atlas nodes that shelter the bundle see ciphertext they can't decrypt — only your organization's accounts can read it back.
The one exception is GET /bundle/:bundleId — deliberately public so other Atlas nodes can pull bundles for sheltering and challenge resolution. Bundle listing endpoints (GET /bundle/, POST /bundle/query) require auth.
Authentication
Each facility you create at /data/company gets its own auto-generated wallet (privkey encrypted at rest in the launchpad). To get an API token, you exchange that privkey at your node's /auth/getApiToken endpoint.
The launchpad does this for you — when you click API token on a facility, it loads the privkey, calls Hermes, and returns the bearer. You never need to handle the privkey manually.
/auth/getApiTokenAuth: None (public)Exchange a facility's operator key for a bearer token. Done by the launchpad on your behalf.
Request body
{
"privateKey": "0x..."
}Response
{
"data": {
"apiToken": "UNOVA_TOKEN eyJhbGciOi..."
},
"meta": { "code": 200 }
}UNOVA_TOKEN prefix — do not prepend it again. Read data.apiToken (an object, not an array) and pass it as-is as the Authorization header value.Token permissions
Tokens carry permissions inherited from the wallet they were issued to. Common ones:
create_asset — submit new assetscreate_event — submit new eventssuper_account — admin endpoints (push bundle, backup/restore)A facility wallet typically has create_asset + create_event. The node operator wallet typically has super_account. Permissions are configured during the organization-onboarding flow on Hermes.
Signing requests
Every createendpoint (asset or event) requires a signed payload so the network can verify the writer's identity and detect tampering. The signing scheme is the same for both:
- Build the payload object (the
idDatafor assets, oridData+datafor events). - Serialize using the deterministic format below (sorted keys, no whitespace).
- Hash via
web3.eth.accounts.hashMessage(serialized)(EIP-191 personal_sign hash). - Sign with the facility's private key —
web3.eth.accounts.sign(serialized, privateKey)handles step 3+4 together. - The resulting hash also serves as the
:assetIdor:eventIdin the URL — Hermes checks they match.
Deterministic serialization
All object keys are sorted alphabetically; arrays preserve order; strings are JSON-quoted; numbers and booleans stringify to their default form. No whitespace.
// Pseudocode for serializeForHashing
function serialize(x) {
if (isObject(x)) {
const keys = Object.keys(x).sort();
return "{" + keys.map(k => `"${k}":${serialize(x[k])}`).join(",") + "}";
}
if (Array.isArray(x)) {
return "[" + x.map(serialize).join(",") + "]";
}
if (typeof x === "string") return `"${x}"`;
return String(x);
}Node.js example — sign + create an asset
import Web3 from "web3";
const web3 = new Web3();
// Your facility's private key (decrypted from the launchpad's API-token modal,
// or stored in your warehouse / IoT system)
const FACILITY_PRIVKEY = "0x...";
const FACILITY_ADDRESS = "0x..."; // derived from the privkey
// 1. Build the idData
const idData = {
createdBy: FACILITY_ADDRESS,
timestamp: Math.floor(Date.now() / 1000),
sequenceNumber: 0,
};
// 2-4. Serialize + sign in one call
const serialize = (x) => { /* see above */ };
const serialized = serialize(idData);
const { signature } = web3.eth.accounts.sign(serialized, FACILITY_PRIVKEY);
// 5. Compute the assetId — it's the hash of the {idData, signature} content
const content = { idData, signature };
const contentSerialized = serialize(content);
const assetId = web3.eth.accounts.hashMessage(contentSerialized);
// 6. POST to the node
// BEARER = data.apiToken from /auth/getApiToken — it already starts
// with "UNOVA_TOKEN ", so pass it as-is.
const res = await fetch(`${HERMES_URL}/asset2/create/${assetId}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": BEARER,
},
body: JSON.stringify({ content }),
});For events, the content additionally includes a data array, and idData includes assetId + dataHash (the hash of the data array). See the Events section.
Code examples
Five end-to-end examples covering the flows almost every integration needs. Replace placeholders (HERMES_URL, FACILITY_PRIVKEY, FACILITY_ADDRESS) with values from your facility's API token modal in /data/company.
1. Get a bearer token
Exchange your facility's private key for a Hermes bearer. Cache the token client-side; it is valid ~5 days. Expiry surfaces as HTTP 403 (not 401) — refresh and retry once on 403. The returned token already contains the UNOVA_TOKEN prefix — do not prepend it again.
curl -X POST "$HERMES_URL/auth/getApiToken" \
-H "Content-Type: application/json" \
-d '{"privateKey":"'"$FACILITY_PRIVKEY"'"}'
# → { "data": { "apiToken": "UNOVA_TOKEN eyJ..." }, "meta": { ... } }
# The token already includes the "UNOVA_TOKEN " prefix. Use it as-is:
BEARER=$(curl -s -X POST "$HERMES_URL/auth/getApiToken" \
-H "Content-Type: application/json" \
-d '{"privateKey":"'"$FACILITY_PRIVKEY"'"}' | jq -r '.data.apiToken')
# Later: -H "Authorization: $BEARER"2. Create an asset
Sign + POST the smallest valid asset payload. The assetId in the URL is the content hash; Hermes rejects mismatches.
# Use the Node.js or Python example to compute the signature + assetId,
# then POST the resulting JSON:
curl -X POST "$HERMES_URL/asset2/create/$ASSET_ID" \
-H "Content-Type: application/json" \
-H "Authorization: $BEARER" \
-d '{
"content": {
"idData": {
"createdBy": "0xfacility...",
"timestamp": 1746460800,
"sequenceNumber": 0
},
"signature": "0x..."
}
}'3. Create an event
Same signing flow as assets, but idData also includes assetId, uniqueID (your physical-asset identifier), and dataHash.
# As with assets, compute eventId + signature in code, then POST:
curl -X POST "$HERMES_URL/event2/create/$EVENT_ID" \
-H "Content-Type: application/json" \
-H "Authorization: $BEARER" \
-d '{
"content": {
"idData": {
"assetId": "0xabc...",
"uniqueID": "LOT-42",
"createdBy": "0xfacility...",
"timestamp": 1746460800,
"accessLevel": 0,
"dataHash": "0x..."
},
"data": [
{ "type": "unova.event.scan", "value": "delivered" }
],
"signature": "0x..."
},
"groupNumbers": ["1"]
}'4. Look up by uniqueID
The lookup users actually want — find an asset's full event history by YOUR identifier (lot, container, SKU).
curl "$HERMES_URL/assets/LOT-42/events" \ -H "Authorization: $BEARER"
5. Walk the genealogy graph
Trace inputs (parents) and outputs (children) of an asset via the Atlas /graphdb worker. Note port 9876 — different from the main Hermes port.
# Forward (children + descendants):
curl -X POST "http://$NODE_IP:9876/graphdb/chain?forward=true" \
-H "Content-Type: application/json" \
-H "Authorization: $BEARER" \
-d '{"uniqueID":"LOT-42"}'
# Backward (parents + roots): same call with ?forward=falseAssets
An asset is any entity you want to track. It has a unique ID, an owner address, and a timeline of events. You define the schema — the API doesn't prescribe a shape for asset metadata.
/asset2/create/:assetIdAuth: Bearer (create_asset)Create a new asset. The :assetId in the URL is the content hash of the body — Hermes verifies they match and rejects mismatches. See the Signing section for how to compute it.
Request body
{
"content": {
"idData": {
"createdBy": "0xfacility-address...",
"timestamp": 1746460800,
"sequenceNumber": 0
},
"signature": "0x... (130-char hex)"
}
}Response
{
"data": {
"assetId": "0x...",
"createdBy": "0xfacility-address...",
"createdAt": 1746460800,
"content": { /* echoed */ }
}
}Look up an asset
Two identifiers exist: uniqueID is YOUR identifier (lot, container, SKU, animal tag — assigned by you when you create the asset), assetId is the on-chain content hash (returned by Hermes after creation, deterministic from signed content). Real integrations almost always look up by uniqueIDbecause that's what your ERP / scanners / labels use.
/assets/:uniqueID/eventsAuth: BearerPRIMARY USER LOOKUP — find all events for an asset using YOUR uniqueID (lot, container, SKU). This is what your ERP integration calls when a user types a lot number into a search box.
/asset2/v2/assetSearchAuth: BearerFilter / paginate assets by arbitrary mongo-style query. Use this for list views with filters (date range, supplier address, asset type, customer).
Request body
{
"query": [
{ "field": "content.idData.createdBy", "operator": "equal", "value": "0xfacility..." }
],
"paginationField": "content.idData.timestamp",
"supplier": "0xsupplier...",
"customer": "0xcustomer..."
}/asset2/v2/user/uniqueIdsAuth: BearerList all uniqueIDs the calling account has ever created. Useful for picker UIs / autocomplete.
Request body
{
"list": ["0xpartnerA...", "0xpartnerB..."] // optional: include partners' uniqueIDs too
}/asset2/v2/user/assetTypesAuth: BearerList the distinct asset.type values the calling account has used. For populating type-filter dropdowns.
/asset2/v2/user/eventTypesAuth: BearerList the distinct event.type values seen across the calling account's assets. For populating event-filter dropdowns.
/asset2/v2/list/:publicKeyAuth: BearerList assets created by a specific publicKey (typically a facility wallet), paginated.
/asset2/v2/list/all/:publicKeyAuth: BearerSame as /v2/list but includes assets from sub-accounts under that publicKey.
Direct lookups (when you already have the assetId)
After /asset2/create returns an assetId, you can cache it and address the asset directly without a uniqueID search.
/asset2/info/:assetIdAuth: BearerGet a single asset by its on-chain hash.
Response
{
"data": {
"assetId": "0xabc123...",
"createdBy": "0xowner...",
"metadata": { "name": "Container LOT-42", "type": "shipment" },
"createdAt": 1746360800
}
}/asset2/exists/:assetIdAuth: BearerCheap existence check — returns 200 if the asset is known to this node, 404 otherwise.
Note: there is no /asset/:assetId/eventsroute. To get an asset's event history, use GET /assets/:uniqueID/events with your own uniqueID (above), or POST /asset2/v2/trace for a traceability score.
/asset2/queryAuth: BearerGeneric query. The query is an ARRAY of {field, operator, value} clauses — raw Mongo-style filter objects are rejected ('bad operators found in query'). Operators: equal, not-equal, greater-than, greater-than-equal, less-than, less-than-equal, inrange, startsWith, contains. Pass limit/next as URL query parameters.
Request body
{
"query": [
{ "field": "content.idData.createdBy", "operator": "equal", "value": "0xowner..." }
]
}/asset2/v2/remove/:uniqueIdAuth: BearerRemove an asset and ALL its events from this node's local cache by uniqueID. Does NOT remove from chain — the bundle stays anchored. For GDPR-style erasure or test cleanup.
/asset3/v3/list/all/:publicKeyAuth: Bearerv3 alternative listing endpoint — returns assets owned by publicKey including child accounts, paginated.
Events
An event is a signed, timestamped record of something that happened to or with an asset. Events are the primary write — you submit them as your business logic executes (a scan, a sensor reading, a state transition).
/event2/create/:eventIdAuth: Bearer (create_event)Submit a new event. The :eventId in the path is the content hash of the body — Hermes verifies they match. uniqueID inside idData is YOUR identifier (the lot number, container ID, SKU, animal tag — whatever links this event to a physical asset); it's what the trace graph uses to walk genealogy.
Request body
{
"content": {
"idData": {
"assetId": "0xabc123...",
"uniqueID": "LOT-42", // ← required: your physical-asset link
"timestamp": 1746360800,
"accessLevel": 0,
"createdBy": "0xfacility...",
"dataHash": "0x..." // = calculateHash(data)
},
"data": [
{
"type": "unova.event.location",
"geoJson": { "type": "Point", "coordinates": [4.40, 51.22] }
},
{
"type": "unova.event.scan",
"value": "delivered"
}
],
"signature": "0x... (130-char hex over serialized idData)"
}
// NOTE: send { "content": ... } only. Strict node schemas reject unknown
// top-level fields (a legacy "groupNumbers" field is not read by current
// nodes — omit it).
}Response
{
"data": {
"eventId": "0x...",
"content": { /* echoed */ }
}
}/event2/listAuth: BearerList recently published events across all assets the calling account can see.
/event2/info/:eventIdAuth: BearerGet a single event by its ID.
/event2/queryAuth: BearerQuery events. The query is an ARRAY of {field, operator, value} clauses (same format as /asset2/query) — raw Mongo-style filter objects are rejected, and there is no sort key. Use this to surface events of a specific type, by an asset, in a date range, etc.
Request body
{
"query": [
{ "field": "content.data.type", "operator": "equal", "value": "unova.event.scan" },
{ "field": "content.idData.assetId", "operator": "equal", "value": "0xabc123..." }
]
}/event2/searchAuth: BearerFree-text search across event payloads. Indexed by Hermes.
/event2/lookup/typesAuth: BearerList all event types your node has ever seen — useful for building filter UIs.
/event2/v2/list/:publicKeyAuth: BearerList events created by a specific publicKey, paginated.
/event2/v2/list/all/:publicKeyAuth: BearerList events from a publicKey including its child accounts, paginated.
/event2/v2/countAuth: BearerCount events matching a query — for dashboards / pagination totals.
Trace endpoints
For the simple "show me what happened" view, use GET /assets/:uniqueID/events (by your own uniqueID — see Assets). The endpoints below return traceability scores:
/asset2/v2/traceAuth: BearerReturns a numeric traceability score (% inbound vs outbound events) for a given asset. Useful for dashboards. Query params: ?inbound=true and/or ?outbound=true.
/asset2/v2/trace/allAuth: BearerSame shape as /v2/trace but aggregated across all addresses on the node — total network-level score.
/account/:address/traceAuth: BearerReturns the account record for an address (includes a trace score field when present).
Genealogy graph (Atlas /graphdb)
For the full lineage walk — backward (inputs) + forward (outputs) + events — the Atlas worker exposes Neo4j-backed endpoints on a separate port:
http://<node-ip>:9876/graphdb/chainAuth: Bearer (any facility token)Walks TRANSFORMS_INTO + HAS_EVENT relationships in either direction. Returns backwardReferenceAssets[], forwardReferenceAssets[], events[]. Pass ?forward=true|false to choose direction. Body: { uniqueID }. The launchpad's /data/trace UI uses this.
Request body
{
"uniqueID": "LOT-42" // your physical-asset identifier
}Response
{
"data": [{
"success": true,
"backwardReferenceAssets": [
{ "uniqueID": "LOT-37", "data": "...", "labels": ["ASSET"] }
],
"forwardReferenceAssets": [
{ "uniqueID": "LOT-42-A", "data": "...", "labels": ["ASSET"] }
],
"events": [
{ "uniqueID": "Event_42_scan", "data": "...", "labels": ["EVENT"] }
]
}]
}http://<node-ip>:9876/graphdb/routesAuth: Bearer (any facility token)Returns just the TRAVERSAL ROUTES (paths) without full node data. Cheaper than /chain for 'how is X related to Y' lookups.
/graphdb/chain returns empty results. Kick it off with GET http://localhost:19876/graphdb/insert-all from the node itself (SSH in first), then poll GET http://localhost:19876/graphdb/check-status until it reports done. These admin endpoints are deliberately NOT reachable from outside — the public :9876 only serves the token-gated /graphdb/chain and /graphdb/routes.localhost:9876 (or :81 on the oldest installs) instead of :19876, and note their admin routes are still publicly reachable until the node is re-deployed.Bulk + file uploads
For simple ingestion from ERP / scanner / IoT systems — and for attaching binary files (images, PDFs, certificates) — use the high-level /unova/api endpoint. Unlike /asset2/create / /event2/create, it takes a flat JSON payload (no client-side hashing or signing — you pass the facility privateKey and Hermes signs server-side) in the multipart requestbody field, plus up to 10 attached files.
/unova/apiAuth: Bearer (create_asset + create_event)Multipart high-level create. One request creates one asset (typeOfRequest: "asset" — an asset plus its info event) or one event on an existing asset (typeOfRequest: "event"). Send the JSON object as a string in the requestbody form field; attach files via files[].
Request body
# multipart/form-data
requestbody: '{
"typeOfRequest": "asset",
"privateKey": "0xfacility-privkey...",
"name": "Container LOT-42",
"uniqueID": "LOT-42",
"timestamp": "2026-07-06 12:00:00",
"assetType": "shipment",
"packagingID": "PKG-1",
"eventType": "Commission"
}'
files[]: <binary> # optional, up to 10 attachments
# For typeOfRequest "event" drop assetType/packagingID and pick an
# event-type eventType; uniqueID must reference an existing asset.
# Optional fields: companyIdentifier ("Warehouse", "Store", ...),
# groupNumbers ("1,2" — comma string, groups 1-5),
# assetsReferencedOrAdded ("LOT-37,LOT-38" — parent uniqueIDs),
# expirationDate / ETA ("yyyy-mm-dd hh:mm:ss"), quantity, unitOfMeasureResponse
{
"status": "Success",
"message": "Asset created successfully",
"response": { /* created event(s) */ }
}
# Validation errors also come back with HTTP 200:
# { "status": "Failure", "message": "uniqueID should not be null or blank." }Required for both types: typeOfRequest ("asset" | "event"), privateKey, name, eventType, uniqueID, timestamp. Assets additionally require assetType + packagingID, and a new (unused) uniqueID.
Valid eventType values — asset: Commission, Transformation, Aggregation; event: Inbound Transport, Outbound Transport, Observation, Other, Decommission, Disaggregation, Modification, Add Document.
Max 10 files per request. The token must hold both create_asset and create_event permissions regardless of type.
Webhook ingestion (no-code)
The easiest way to get events onto your node — no signing, no bearer tokens, no SDK. POST plain JSON to a launchpad webhook URL and we do the rest: look up the facility behind the token, sign the event with the facility key, and forward it to your Type-2 node's Hermes. Built for ERP exports, IoT gateways, and Zapier-style integrations.
Get the token in /data/company → Facilities → Webhook token (a 64-char hex string). The token in the URL is the auth gate — treat the full URL as a secret.
https://nodes.unova.io/api/webhook/{token}Auth: Webhook token (in the URL)Creates one event on the asset identified by assetId + uniqueID. Identifiers come from query params, with body fields as fallback. Everything else in the JSON body becomes the event's data payload.
Query params
assetIdstringRequired (query param or body field). The asset's on-chain hash — 0x-prefixed 64-char hex.uniqueIDstringRequired (query param or body field). Your physical-asset identifier (lot, container, SKU).typestringOptional event type. Falls back to a "type" body field, then the token's default event type.groupsstringOptional comma list (e.g. 1,5) overriding the token's default groupNumbers.Request body
POST /api/webhook/<token>?assetId=0xabc...&uniqueID=LOT-42&type=delivery
Content-Type: application/json
{
"temperature": 4.2,
"carrier": "DHL"
}
# assetId / uniqueID / type may also be sent as body fields instead of
# query params. They're stripped from the body; the rest becomes the
# event data: [{ "type": "delivery", "temperature": 4.2, "carrier": "DHL" }]
# An empty body is fine too — you'll get a bare event of the given type.Response
{
"success": true,
"eventId": "0x...",
"uniqueID": "LOT-42",
"assetId": "0xabc...",
"type": "delivery"
}
# Errors: { "success": false, "error": "..." } with HTTP 400 (bad input),
# 401 (bad token), 502/503 (node unreachable / not deployed yet)The event's accessLevel and default groupNumberscome from the webhook token's settings. Note this endpoint lives on the launchpad (nodes.unova.io), not on your node — it's a hosted convenience in front of /event2/create.
Bundles
Bundles are the on-chain anchor for assets and events. Each bundle contains a batch of entities, signed by your node operator, and its hash is published to the chain. You typically don't fetch bundles directly — they're for proving data integrity to a third party.
/bundle2/listAuth: BearerList recently published bundles.
/bundle2/info/:bundleIdAuth: BearerGet a bundle's full content — every asset and event inside it, plus the on-chain proof.
/bundle2/queryAuth: BearerQuery bundles by node operator, time range, or content type.
Note: the v1 /bundle/:bundleIdendpoint is intentionally public — Atlas nodes pull bundles from each other without auth as part of the network's sheltering / challenge-resolution protocol. The data inside bundles can still be encrypted (see the accessLevel > 0 section in the user docs).
Organizations
An organization is the top-level tenant on a Hermes node. It owns accounts, assets, events, and bundles. The launchpad creates one organization per Type-2 node automatically; you typically don't need to call these endpoints unless you're running a multi-org node or doing migrations.
/organization2/info/:organizationIdAuth: Bearer (super_account)Get an organization's profile, owner address, and metadata.
/organization2/update/:organizationIdAuth: Bearer (organization_owner)Update organization metadata (name, contact, logo, etc.).
/organizationAuth: Bearer (super_account)List all organizations on this node.
/organization/:organizationId/accountsAuth: Bearer (manage_accounts)List all accounts (users) belonging to an organization.
Accounts
Accounts are individual identities (wallets) within an organization. Each account has a public address, permissions, and metadata. The facility wallets you create in the launchpad register as accounts under your organization.
/account2/listAuth: Bearer (manage_accounts)List accounts on this node, paginated.
/account2/info/:publicKeyAuth: BearerGet account details by wallet address — permissions, organization, metadata.
/account2/create/:addressAuth: Bearer (register_accounts)Register a new account on this node. Used to add a new facility-style sub-identity. The new account's organization is derived from the creating account — you don't pass it in the body.
Request body
{
"email": "ops@acme.example",
"fullName": "Warehouse Antwerp",
"accessLevel": 1,
"permissions": ["create_asset", "create_event"]
}/account2/modify/:addressAuth: Bearer (manage_accounts)Modify an existing account's permissions or metadata.
/account/:address/traceAuth: BearerReturns the account record for an address (includes a trace score field when present).
Organization requests (KYC)
For nodes that vet new organizations before granting write access, a request / approve / refuse flow is built in. The launchpad doesn't use this today (we manage tenancy in our DB), but it's available if you want pure on-node KYC.
/organization/requestAuth: None (public)Submit a new organization application. Goes into the pending queue.
Request body
{
"address": "0x...",
"title": "Acme Logistics BV",
"email": "ops@acme.example"
}/organization/requestAuth: Bearer (super_account)List pending organization requests.
/organization/request/refusedAuth: Bearer (super_account)List refused organization requests.
/organization/request/:address/approveAuth: Bearer (super_account)Approve a pending organization request and provision the account.
/organization/request/:address/refuseAuth: Bearer (super_account)Refuse a pending organization request.
Node info
/nodeinfoAuth: None (public)Get this Hermes node's identity, version, and configuration. Use it as a health check.
Response
{
"data": {
"nodeAddress": "0x...",
"commit": "a1b2c3d",
"version": "2.x.x"
}
}Analytics
Aggregate counts of assets/events by organization and time range. Useful for building dashboards on top of your node's data without paginating through every record.
/analytics/:organizationId/:collection/countAuth: BearerTotal count of assets or events for a given organization. :collection is 'asset' or 'event'.
/analytics/:organizationId/:collection/count/:start/:end/totalAuth: BearerCount over a time range (UNIX seconds for :start and :end).
/analytics/:organizationId/:collection/count/:start/:end/aggregate/:groupAuth: BearerTime-bucketed counts. :group is 'hour' | 'day' | 'week' | 'month'.
/analytics/:collection/countAuth: Bearer (super_account)Org-agnostic total count for the entire node.
Metrics
Prometheus-compatible metrics for monitoring node health. The root /metrics endpoint is open for scraping; sub-paths require super_account auth.
/metricsAuth: None (public)Prometheus exposition format — scrape with your monitoring stack.
/metrics/uonAuth: Bearer (super_account)Current UON balance of the node operator wallet.
/metrics/bundleAuth: Bearer (super_account)Bundle publishing stats — count, last-published timestamp, failures.
/metrics/balanceAuth: Bearer (super_account)Atlas worker balance + recent gas usage.
Admin
Admin endpoints are gated by the super_account role — typically the node operator key. Used for backups and forcing immediate bundle publishing.
/admin/pushbundleAuth: Bearer (super_account)Force the worker to publish a bundle right now, ignoring the schedule. Useful for testing and for end-of-day flushes.
/admin/getconfigAuth: Bearer (super_account)Download a backup of all organizations + accounts on this node.
/admin/restoreconfigAuth: Bearer (super_account)Restore from a backup payload. Disaster-recovery use only.
Standard schemas
The Unova network ships with a small catalog of well-known data types that every node validates and that the trace UI knows how to render. You can mix these with your own custom type values — the standard ones just get richer rendering and validation.
Asset metadata items
Submitted as the data array of a regular event against an asset (right after the asset is created).
unova.asset.identifiersAlternate IDs for an asset (SKU, GTIN, serial, internal ref)
{
"type": "unova.asset.identifiers",
"identifiers": {
"sku": ["WHEEL-12-BLK"],
"gtin": ["1234567890123"],
"internalRef": ["LOT-2024-W47-A"]
}
}unova.asset.infoDisplay-friendly metadata: name, description, images
{
"type": "unova.asset.info",
"name": "Container LOT-42",
"description": "Pharmaceutical batch, cold chain",
"images": ["https://example.com/img1.jpg"]
}unova.asset.locationCurrent location of the asset
{
"type": "unova.asset.location",
"geoJson": { "type": "Point", "coordinates": [4.40, 51.22] },
"name": "Warehouse Antwerp",
"country": "BE",
"city": "Antwerp"
}Event data items
unova.event.identifiersExternal IDs reported with the event (scan code, RFID, etc.)
{
"type": "unova.event.identifiers",
"identifiers": {
"scanCode": ["8412345678901"],
"rfid": ["E20A1234..."]
}
}unova.event.locationWhere the event happened — GeoJSON Point + optional human-readable name
{
"type": "unova.event.location",
"geoJson": { "type": "Point", "coordinates": [4.40, 51.22] },
"name": "Customs checkpoint Antwerp",
"country": "BE",
"city": "Antwerp"
}Custom types: any typestring that doesn't start with unova. is passed through as-is (no validation). Use your own namespace, e.g. com.acme.shipment.scan, to avoid collision with future standard types.
Supply chain patterns
Common modeling patterns for supply-chain customers — these are recipes built on the generic asset/event primitives, not separate APIs.
Tracking a shipment end-to-end
- Create the asset once at origin:
POST /asset2/create/:assetId - Immediately tag it with display info + identifiers via an event with
unova.asset.infoandunova.asset.identifiersdata items - At each handoff (customs, warehouse, courier), submit an event with the asset's same
uniqueID(your lot/container ID) and data items likeunova.event.location+unova.event.scan - Use
groupNumbersto share each event with the right partner group (e.g., group 1 = your own facilities, group 2 = customs broker, group 3 = end customer) - At delivery, look up
POST /graphdb/chainby the shipment's uniqueID to render the full timeline with location history + every scan
Batch genealogy (TRANSFORMS_INTO)
For manufacturing or processing, where one batch becomes another:
- Create child assets with a custom data item that references parent uniqueIDs, e.g.
{ "type": "com.acme.transform", "fromAssets": ["LOT-37", "LOT-38"] } - Atlas's graph migration creates
TRANSFORMS_INTOedges automatically based on these references - Trace genealogy walks both directions — backward to find inputs, forward to find downstream uses
Recall workflow
Flag a problem batch + propagate to anyone who received downstream assets:
- Submit a recall event against the affected asset:
{ "type": "com.acme.recall", "reason": "...", "severity": "critical" } - Set
groupNumbersto include EVERY partner group (recall events are typically broadcast) - Partners querying
/graphdb/chainon any descendant asset will see the recall event in the genealogy
Privacy patterns
- Public (transparency):
accessLevel: 0,groupNumbers: ["1"](your own group). Anyone who shelters the bundle reads it in plaintext. - Private to your org:
accessLevel: > 0,groupNumbers: ["1"]. Hermes auto-encrypts with your org key; partners who pull the bundle see ciphertext. - Shared with specific partners:
accessLevel: > 0,groupNumbers: ["1", "5"]where 5 is a partner group. Only that group's nodes get bundle-routed and only their accounts decrypt.
Errors & limits
HTTP status codes
200 Success400 Validation error — check the message field401 Signature problem — a create signed with a different key than the token's account (Atlas trace: expired token)403 Missing or expired token, or token lacks the required permission (e.g. create_event) — refresh the token and retry once before assuming permissions404 Resource not found400 Validation / schema mismatch — read the message field500 Server error — note the node wraps many request errors (e.g. malformed queries) as 500; read the message before blaming the nodePagination
List and query endpoints use cursor pagination: pass limit (default 10, max 50 — raise the ceiling with the PAGINATION_MAXenv var on your node). Each response's top-level pagination object returns { hasNext, next, hasPrevious, previous } — pass the next / previous cursor strings back as request parameters to walk the result set. (Cursors are in pagination, not meta; meta only carries code/count/message.) There is no page/perPage numbering.
Rate limits
None by default — your node, your rules. Add a reverse proxy (nginx, Cloudflare) in front if you're serving public traffic.
Asset / event size limits
Hermes accepts payloads up to ~16 MB (Mongo BSON limit). Bundles are capped by chain block size — typically a few hundred entities per bundle.
Building with AI or code? The @unovalabs/client SDK (npm) handles signing, tokens, casing, and pagination for you, and @unovalabs/mcp plugs your node straight into Claude / Cursor as tools. Machine-readable versions of these docs live at /llms.txt and /llms-full.txt — point your AI assistant at those.
If you find an undocumented endpoint that's useful, ping us at tech@unova.io and we'll add it here.