UnovaUnova Nodes

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.

Plain-HTTP warning. Requests to your node — including the 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.

POST/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 }
}
Heads up: the returned token already contains the 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 assets
create_event — submit new events
super_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:

  1. Build the payload object (the idData for assets, or idData + data for events).
  2. Serialize using the deterministic format below (sorted keys, no whitespace).
  3. Hash via web3.eth.accounts.hashMessage(serialized) (EIP-191 personal_sign hash).
  4. Sign with the facility's private key — web3.eth.accounts.sign(serialized, privateKey) handles step 3+4 together.
  5. The resulting hash also serves as the :assetId or :eventId in 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=false

Assets

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.

POST/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.

GET/assets/:uniqueID/eventsAuth: Bearer

PRIMARY 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.

POST/asset2/v2/assetSearchAuth: Bearer

Filter / 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..."
}
POST/asset2/v2/user/uniqueIdsAuth: Bearer

List all uniqueIDs the calling account has ever created. Useful for picker UIs / autocomplete.

Request body

{
  "list": ["0xpartnerA...", "0xpartnerB..."]   // optional: include partners' uniqueIDs too
}
POST/asset2/v2/user/assetTypesAuth: Bearer

List the distinct asset.type values the calling account has used. For populating type-filter dropdowns.

POST/asset2/v2/user/eventTypesAuth: Bearer

List the distinct event.type values seen across the calling account's assets. For populating event-filter dropdowns.

GET/asset2/v2/list/:publicKeyAuth: Bearer

List assets created by a specific publicKey (typically a facility wallet), paginated.

POST/asset2/v2/list/all/:publicKeyAuth: Bearer

Same 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.

GET/asset2/info/:assetIdAuth: Bearer

Get a single asset by its on-chain hash.

Response

{
  "data": {
    "assetId": "0xabc123...",
    "createdBy": "0xowner...",
    "metadata": { "name": "Container LOT-42", "type": "shipment" },
    "createdAt": 1746360800
  }
}
GET/asset2/exists/:assetIdAuth: Bearer

Cheap 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.

POST/asset2/queryAuth: Bearer

Generic 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..." }
  ]
}
DELETE/asset2/v2/remove/:uniqueIdAuth: Bearer

Remove 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.

POST/asset3/v3/list/all/:publicKeyAuth: Bearer

v3 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).

POST/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 */ }
  }
}
GET/event2/listAuth: Bearer

List recently published events across all assets the calling account can see.

GET/event2/info/:eventIdAuth: Bearer

Get a single event by its ID.

POST/event2/queryAuth: Bearer

Query 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..." }
  ]
}
POST/event2/searchAuth: Bearer

Free-text search across event payloads. Indexed by Hermes.

GET/event2/lookup/typesAuth: Bearer

List all event types your node has ever seen — useful for building filter UIs.

GET/event2/v2/list/:publicKeyAuth: Bearer

List events created by a specific publicKey, paginated.

POST/event2/v2/list/all/:publicKeyAuth: Bearer

List events from a publicKey including its child accounts, paginated.

POST/event2/v2/countAuth: Bearer

Count 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:

POST/asset2/v2/traceAuth: Bearer

Returns a numeric traceability score (% inbound vs outbound events) for a given asset. Useful for dashboards. Query params: ?inbound=true and/or ?outbound=true.

POST/asset2/v2/trace/allAuth: Bearer

Same shape as /v2/trace but aggregated across all addresses on the node — total network-level score.

GET/account/:address/traceAuth: Bearer

Returns 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:

POSThttp://<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"] }
    ]
  }]
}
POSThttp://<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.

Fresh node? Bootstrap the graph first. On a newly deployed node the Neo4j graph is empty until a one-time migration copies existing assets/events into it — until then /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.
Nodes deployed before July 2026 use the older layout: call 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.

POST/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, unitOfMeasure

Response

{
  "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.

POSThttps://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.

GET/bundle2/listAuth: Bearer

List recently published bundles.

GET/bundle2/info/:bundleIdAuth: Bearer

Get a bundle's full content — every asset and event inside it, plus the on-chain proof.

POST/bundle2/queryAuth: Bearer

Query 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.

GET/organization2/info/:organizationIdAuth: Bearer (super_account)

Get an organization's profile, owner address, and metadata.

POST/organization2/update/:organizationIdAuth: Bearer (organization_owner)

Update organization metadata (name, contact, logo, etc.).

GET/organizationAuth: Bearer (super_account)

List all organizations on this node.

GET/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.

GET/account2/listAuth: Bearer (manage_accounts)

List accounts on this node, paginated.

GET/account2/info/:publicKeyAuth: Bearer

Get account details by wallet address — permissions, organization, metadata.

POST/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"]
}
POST/account2/modify/:addressAuth: Bearer (manage_accounts)

Modify an existing account's permissions or metadata.

GET/account/:address/traceAuth: Bearer

Returns 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.

POST/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"
}
GET/organization/requestAuth: Bearer (super_account)

List pending organization requests.

GET/organization/request/refusedAuth: Bearer (super_account)

List refused organization requests.

GET/organization/request/:address/approveAuth: Bearer (super_account)

Approve a pending organization request and provision the account.

GET/organization/request/:address/refuseAuth: Bearer (super_account)

Refuse a pending organization request.

Node info

GET/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.

GET/analytics/:organizationId/:collection/countAuth: Bearer

Total count of assets or events for a given organization. :collection is 'asset' or 'event'.

GET/analytics/:organizationId/:collection/count/:start/:end/totalAuth: Bearer

Count over a time range (UNIX seconds for :start and :end).

GET/analytics/:organizationId/:collection/count/:start/:end/aggregate/:groupAuth: Bearer

Time-bucketed counts. :group is 'hour' | 'day' | 'week' | 'month'.

GET/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.

GET/metricsAuth: None (public)

Prometheus exposition format — scrape with your monitoring stack.

GET/metrics/uonAuth: Bearer (super_account)

Current UON balance of the node operator wallet.

GET/metrics/bundleAuth: Bearer (super_account)

Bundle publishing stats — count, last-published timestamp, failures.

GET/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.

GET/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.

GET/admin/getconfigAuth: Bearer (super_account)

Download a backup of all organizations + accounts on this node.

POST/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.identifiers

Alternate 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.info

Display-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.location

Current 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.identifiers

External IDs reported with the event (scan code, RFID, etc.)

{
  "type": "unova.event.identifiers",
  "identifiers": {
    "scanCode": ["8412345678901"],
    "rfid": ["E20A1234..."]
  }
}
unova.event.location

Where 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

  1. Create the asset once at origin: POST /asset2/create/:assetId
  2. Immediately tag it with display info + identifiers via an event with unova.asset.info and unova.asset.identifiers data items
  3. At each handoff (customs, warehouse, courier), submit an event with the asset's same uniqueID (your lot/container ID) and data items like unova.event.location + unova.event.scan
  4. Use groupNumbers to share each event with the right partner group (e.g., group 1 = your own facilities, group 2 = customs broker, group 3 = end customer)
  5. 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_INTO edges 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:

  1. Submit a recall event against the affected asset: { "type": "com.acme.recall", "reason": "...", "severity": "critical" }
  2. Set groupNumbers to include EVERY partner group (recall events are typically broadcast)
  3. Partners querying /graphdb/chain on 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 Success
400 Validation error — check the message field
401 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 permissions
404 Resource not found
400 Validation / schema mismatch — read the message field
500 Server error — note the node wraps many request errors (e.g. malformed queries) as 500; read the message before blaming the node

Pagination

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.