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.

Authentication header

Authorization: UNOVA_TOKEN <your-bearer>

Tokens are per-facility. Generate one in /data/company → Facilities → API token.

Response envelope

All responses are JSON, wrapped in this shape:

{
  "meta": {
    "code": 200,
    "message": "Success"
  },
  "data": [ /* result(s) */ ],
  "resultCount": 42
}

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 metadata listing endpoints (/bundle/list, /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 signed payload for a bearer token. Done by the launchpad on your behalf.

Request body

{
  "publicKey": "0x...",
  "validUntil": 1746460800,
  "signature": "0x..."
}

Response

{
  "data": [{
    "apiToken": "UNOVA_TOKEN eyJhbGciOi..."
  }]
}

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
const res = await fetch(`${HERMES_URL}/asset2/create/${assetId}`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": `UNOVA_TOKEN ${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 expires hourly.

curl -X POST "$HERMES_URL/auth/getApiToken" \
  -H "Content-Type: application/json" \
  -d '{"privateKey":"'"$FACILITY_PRIVKEY"'"}'

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: UNOVA_TOKEN $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: UNOVA_TOKEN $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: UNOVA_TOKEN $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: UNOVA_TOKEN $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": { "content.idData.createdBy": "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.

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

GET/asset/:assetId/eventsAuth: Bearer

Flat list of every event ever logged against a specific asset (by hash) — its full lifecycle history.

POST/asset2/queryAuth: Bearer

Generic query — mongo-style filter + sort + pagination. More flexible than /assetSearch, no business-logic conveniences.

Request body

{
  "query": {
    "createdBy": "0xowner..."
  },
  "sort": { "createdAt": -1 },
  "page": 0,
  "perPage": 20
}
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)"
  },
  "groupNumbers": ["1"]                          // partner groups to share with
}

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 with arbitrary mongo-style filters. Use this to surface events of a specific type, by an asset, in a date range, etc.

Request body

{
  "query": {
    "content.data.type": "unova.event.scan",
    "content.idData.assetId": "0xabc123..."
  },
  "sort": { "content.idData.timestamp": -1 },
  "perPage": 20
}
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.

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

Three layers of trace, increasing in richness:

GET/asset/:assetId/eventsAuth: Bearer

Flat list of every event ever logged against a specific asset. The simplest 'show me what happened' view.

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

Per-account traceability score across everything that account has touched.

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.

Bulk + file uploads

For high-throughput ingestion (warehouse scanners, IoT batches, ERP sync) and for attaching binary files (images, PDFs, certificates) to assets/events, use the unified /api endpoint. It accepts an array of asset+event payloads in a single multipart request and ties uploaded files to the entities by index.

POST/apiAuth: Bearer (create_asset + create_event)

Multipart bulk-create. Send a JSON array of asset/event payloads in the requestbody field; attach up to 10 files via files[]. Each entity in the array gets validated, signed, and either an assetId/eventId is computed or you can provide your own.

Request body

# multipart/form-data
requestbody: '[
  {
    "type": "asset",
    "content": {
      "idData": { "createdBy": "0x...", "timestamp": 1746460800, "sequenceNumber": 0 },
      "signature": "0x..."
    }
  },
  {
    "type": "event",
    "content": {
      "idData": {
        "assetId": "0x...",
        "createdBy": "0x...",
        "timestamp": 1746460801,
        "accessLevel": 0,
        "dataHash": "0x..."
      },
      "data": [
        { "type": "unova.event.scan", "value": "received", "fileIndex": 0 }
      ],
      "signature": "0x..."
    }
  }
]'
files[]: <binary>      # uploaded file referenced by fileIndex above

Response

{
  "data": [
    { "assetId": "0x..." },
    { "eventId": "0x..." }
  ]
}

Max 10 files per request. The token must hold both create_asset and create_event permissions even if your batch only contains one type.

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

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

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

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

GET/organizationAuth: Bearer

List all organizations on this node.

GET/organization/:organizationId/accountsAuth: Bearer

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

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 (manage_accounts)

Register a new account on this node. Used to add a new facility-style sub-identity.

Request body

{
  "permissions": ["create_asset", "create_event"],
  "accessLevel": 1,
  "organization": 1
}
POST/account2/modify/:addressAuth: Bearer (manage_accounts)

Modify an existing account's permissions or metadata.

GET/account/:address/traceAuth: Bearer

Get the full activity trace for an account — every asset created, every event submitted.

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.

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

Approve a pending organization request and provision the account.

POST/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...",
    "version": "2.x.x",
    "headContract": "0x000...0F10",
    "network": "test"
  }
}

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

Org-agnostic total count for the entire node.

Metrics

Prometheus-compatible metrics for monitoring node health. The root /metric endpoint is open for scraping; sub-paths require super_account auth.

GET/metricAuth: None (public)

Prometheus exposition format — scrape with your monitoring stack.

GET/metric/uonAuth: Bearer (super_account)

Current UON balance of the node operator wallet.

GET/metric/bundleAuth: Bearer (super_account)

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

GET/metric/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 Missing or invalid bearer token
403 Token lacks the required permission (e.g. create_event)
404 Resource not found
422 Schema mismatch — payload didn't match the expected structure
500 Server error — check your node's Hermes logs

Pagination

All list and query endpoints accept page (0-indexed) and perPage (max 50 by default, server-configurable up to 500 via the PAGINATION_MAX env var).

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.

If you find an undocumented endpoint that's useful, ping us at tech@unova.io and we'll add it here.