Nexaria IoT API
Register devices, authenticate securely, and stream real-world events into your digital twins.
Nexaria IoT API
Connect physical devices to your digital assets. The Nexaria IoT module lets QR codes, NFC tags, BLE beacons, smart billboards, sensors, and smart locks stream real-world events into your digital twins, NFTs, and campaigns across the xSPECTAR and XRPL ecosystem.
This guide documents the endpoints that ship in the codebase today. It does not describe endpoints that do not exist.
Status: Integration Ready / Demo Mode
The IoT API is integration-ready, not fully live in production.
- What works today: every request is fully crypto-validated on the server. API keys are generated for real, timestamps and nonces are checked, HMAC signatures are verified, rate limits are enforced, and payloads are validated with Zod.
- What activates on connection: persistence (writing devices and events to the database) turns on once the Supabase service key and Clerk auth are connected. Until then the API runs in demo mode — it accepts and validates your request, returns a
mode: "demo"marker, and reads sample data for list endpoints, but does not store anything.
You can therefore build and test a full integration against the real security surface now, and it will keep working unchanged when persistence is switched on.
Base URL
https://www.nexariadigital.com
The domain is currently transferring to Vercel — treat the base URL as the target host, not a guaranteed-live endpoint.
Registering a device
Provision a device and receive its API key.
POST /api/iot/register
Content-Type: application/json
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | 1–160 characters. Internal device name. |
deviceType | string | no | Up to 80 chars, e.g. "QR Kiosk". |
publicLabel | string | no | Up to 120 chars, shown publicly. |
locationId | UUID | no | Links the device to a physical location / digital twin. |
linkedAssetId | UUID | no | Links the device to a digital asset. |
Response
{
"ok": true,
"apiKey": "nxk_a1b2c3d4_kJ8s...base64url-secret",
"keyPrefix": "a1b2c3d4"
}
In demo mode the response also carries "mode": "demo" and a note reminding you to store the key.
About the key
- The key format is
nxk_<prefix>_<secret>. Theprefixis 8 hex characters (safe to store and display); thesecretis 24 random bytes, base64url-encoded. - The plaintext
apiKeyis shown once. Only its SHA-256 hash is stored — Nexaria can never show it to you again. If you lose it, provision a new device. - The
keyPrefixis used for a fast database lookup; the full key is then verified against the stored hash with a constant-time compare.
This endpoint is rate-limited (20 requests/window per IP).
API authentication
Every ingestion request (sending events) is authenticated with a set of headers. No cookies or sessions are involved — devices authenticate purely with their key.
| Header | Required | Description |
|---|---|---|
x-nexaria-key | yes | The device API key (nxk_<prefix>_<secret>). |
x-nexaria-timestamp | yes | Current time as unix seconds. Used for replay protection. |
x-nexaria-nonce | yes | A value unique to this request (max 128 chars). A UUID works well. |
x-nexaria-signature | optional | HMAC-SHA256 of the raw request body, keyed with the API key, hex-encoded. |
If a signature header is present it must verify, or the request is rejected. Sending a signature is strongly recommended for tamper protection.
Security model
The IoT ingestion path layers several defenses:
- API-key authentication — keys are hashed with SHA-256 at rest and compared in constant time to prevent timing attacks.
- Timestamp validation — the request timestamp must be within ±300 seconds (5 minutes) of server time, or it is rejected as stale. This shrinks the window for replaying a captured request.
- Per-device unique nonce — each event carries a nonce that must be unique for the device. A repeated
(device, nonce)pair is a replay and is rejected by a database unique constraint, returning 409. - Optional HMAC body signature — when supplied, guarantees the body was not altered in transit and was produced by a holder of the key.
- Rate limiting — ingestion is capped at 120 requests per minute per IP; registration and device management have their own tighter limits.
- Row-Level Security (RLS) — once Supabase and Clerk are connected, every device, key, and event row is owner-scoped so you only ever read or write your own data.
- Audit logging — key usage (
last_used_at) and event source IPs are recorded. - Server-side validation — all bodies are validated with Zod before anything is trusted.
Sending events
Stream a real-world event from a device into Nexaria.
POST /api/iot/events
Content-Type: application/json
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
eventType | string | yes | One of the 16 supported event types. |
payload | object | no | Free-form JSON details for the event. Defaults to {}. |
occurredAt | string | no | ISO 8601 datetime. Defaults to server receive time. |
Response codes
| Status | Meaning |
|---|---|
202 | Accepted. The event was validated (and persisted when connected). |
400 | Malformed request — invalid JSON or a missing/oversized nonce. |
401 | Authentication failed, stale/missing timestamp, or invalid signature. |
409 | Replay detected — this device already used this nonce. |
422 | Validation failed — bad eventType or payload shape. |
429 | Rate limit exceeded (more than 120 requests/minute from your IP). |
A successful response looks like:
{ "ok": true, "accepted": true }
curl example (with HMAC signature)
API_KEY="nxk_a1b2c3d4_your-secret-here"
BODY='{"eventType":"qr_scan","payload":{"campaign":"Genesis Drop","ref":"poster-a"}}'
TS=$(date +%s)
NONCE=$(uuidgen)
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$API_KEY" | awk '{print $2}')
curl -X POST https://www.nexariadigital.com/api/iot/events \
-H "content-type: application/json" \
-H "x-nexaria-key: $API_KEY" \
-H "x-nexaria-timestamp: $TS" \
-H "x-nexaria-nonce: $NONCE" \
-H "x-nexaria-signature: $SIG" \
-d "$BODY"
The signature is computed over the exact raw body bytes you send. Sign the same string you transmit — do not re-serialize it.
Node.js example
import { createHmac, randomUUID } from "node:crypto";
const API_KEY = process.env.NEXARIA_KEY;
// Serialize once, then sign and send that exact string.
const body = JSON.stringify({
eventType: "proof_of_play",
payload: { creative: "Genesis Drop", duration: 15 },
});
const signature = createHmac("sha256", API_KEY).update(body).digest("hex");
const res = await fetch("https://www.nexariadigital.com/api/iot/events", {
method: "POST",
headers: {
"content-type": "application/json",
"x-nexaria-key": API_KEY,
"x-nexaria-timestamp": String(Math.floor(Date.now() / 1000)),
"x-nexaria-nonce": randomUUID(),
"x-nexaria-signature": signature,
},
body,
});
console.log(res.status); // 202 on success
Event payload format
Every event shares the same envelope. eventType and the security headers are the contract; payload is intentionally free-form so each device type can send what makes sense for it.
{
"eventType": "qr_scan",
"payload": { "campaign": "Avatar Launch", "ref": "poster-a" },
"occurredAt": "2026-07-03T05:20:05Z"
}
Example payloads per event type:
// proof_of_play — a smart billboard finished showing a creative
{ "eventType": "proof_of_play", "payload": { "creative": "Genesis Drop", "duration": 15 } }
// nfc_scan — a wallet tapped an NFC tag
{ "eventType": "nfc_scan", "payload": { "wallet": "0x91a…f2", "membership": "gold" } }
// bluetooth_beacon — a BLE beacon saw nearby visitors
{ "eventType": "bluetooth_beacon", "payload": { "rssi": -62, "visitors": 3 } }
// smart_lock — an NFT-gated lock opened
{ "eventType": "smart_lock", "payload": { "action": "unlock", "method": "nft_gate" } }
// occupancy — a sensor reported a headcount
{ "eventType": "occupancy", "payload": { "count": 41 } }
// vr_unlock — a scan unlocked a VR experience
{ "eventType": "vr_unlock", "payload": { "experience": "aurora-live" } }
Supported event types
The API accepts exactly these 16 event types:
| Event type | Typical source |
|---|---|
qr_scan | QR kiosks and printed codes |
nfc_scan | NFC tags and readers |
bluetooth_beacon | BLE beacons |
occupancy | Occupancy / people-counting sensors |
motion | Motion sensors |
proof_of_play | Smart billboards / digital signage |
smart_lock | NFT-gated smart locks |
environmental | Combined environmental sensors |
temperature | Temperature sensors |
humidity | Humidity sensors |
vibration | Vibration / tamper sensors |
camera_trigger | Camera / computer-vision triggers |
digital_signage | Signage playback / status |
vr_unlock | VR/AR experience unlocks |
campaign_interaction | Taps / interactions on a campaign surface |
heartbeat | Periodic device liveness ping |
Any other value returns 422.
Reading data
These endpoints list your devices and events. In demo mode they return a sample fleet and event feed; once Clerk + Supabase are connected they read your rows, scoped by RLS.
List devices
GET /api/iot/devices
{ "ok": true, "devices": [ { "id": "d3", "name": "Aurora Arena QR Portal", "deviceType": "QR Kiosk", "status": "online", "health": "degraded", "eventCount": 15890, "location": "Aurora VR Event Arena" } ] }
List recent events
GET /api/iot/events
{ "ok": true, "events": [ { "id": "e3", "deviceId": "d3", "deviceName": "Aurora Arena QR Portal", "eventType": "qr_scan", "payload": { "campaign": "Avatar Launch", "ref": "poster-a" }, "occurredAt": "2026-07-03T05:20:05Z" } ] }
Update a device
PATCH /api/iot/device
Content-Type: application/json
| Field | Type | Notes |
|---|---|---|
id | UUID | Required. The device to update. |
name | string | Optional, up to 160 chars. |
publicLabel | string | Optional, up to 120 chars. |
status | enum | Optional: provisioning, online, offline, error, decommissioned. |
linkedAssetId | UUID or null | Optional. Link/unlink a digital asset. |
linkedCampaignId | UUID or null | Optional. Link/unlink a campaign. |
{ "ok": true, "updated": "b7e1…-uuid" }
Delete a device
DELETE /api/iot/device?id=<device-uuid>
{ "ok": true, "deleted": "b7e1…-uuid" }
Missing id returns 400.
Webhooks
Integration-ready. Instead of polling GET /api/iot/events, subscribers will be able to receive events pushed to their own URL as they happen. Nexaria will POST an envelope shaped like this:
{
"id": "evt_9f2c…",
"deviceId": "d3",
"eventType": "qr_scan",
"payload": { "campaign": "Avatar Launch", "ref": "poster-a" },
"occurredAt": "2026-07-03T05:20:05Z",
"signature": "hex-hmac-sha256-of-the-body"
}
Subscribers verify the delivery by recomputing an HMAC-SHA256 of the raw body with their shared webhook secret and comparing it to the signature header — the same signing pattern used for Stripe events. Reject any delivery whose signature does not match.
This outbound delivery path is not wired to production yet; the envelope above is the planned contract so you can design your receiver ahead of time.
Digital Twin architecture
A digital twin is the bridge between a physical place and its digital counterpart. Each twin links a real location to:
- digital land (e.g. a Sandbox parcel or xSPECTAR world),
- a virtual building,
- an NFT (membership or access pass),
- a digital billboard,
- a VR/AR experience (
vrExperienceUrl/arExperienceUrl), - a campaign, and
- a creator profile.
Devices installed at the location feed events into the twin. Because every event names its deviceId and eventType, the twin accumulates a live event history and derives analytics (scan volume, occupancy, proof-of-play counts, uptime, health). A single twin can host multiple devices — for example the Prime Plaza twin combines a smart billboard and a BLE beacon into one aggregated view.
Device workflows
Different capture technologies have very different browser and hardware support. Choose based on honest reach, not ideal-case demos.
QR — the universal fallback
QR works everywhere: any phone camera or QR app can open a URL, which then calls the events API (typically through a server that holds the device key). Because it has no platform gaps, QR is the recommended baseline for public-facing capture. Use it as the default and layer NFC/BLE on top where available.
NFC — progressive enhancement
Web NFC (reading tags directly in the browser) is supported only on Chrome for Android — roughly 6% of the global browser base — and is unavailable on iOS/Safari and desktop. Native apps can read NFC more broadly, but on the open web treat NFC as a progressive enhancement: offer it when the browser supports it, and always keep a QR path as the fallback.
BLE — progressive enhancement
Web Bluetooth has no support in Safari/iOS or Firefox, so it cannot be your only capture method on the web. Use BLE beacons for proximity/visitor signals where you control the hardware or app environment, and again fall back to QR for universal reach.
Bottom line: QR is universal; NFC and BLE are enhancements. Design the QR flow first, then enrich it.
Billboard integrations
Smart billboards and digital signage report proof_of_play events — one per creative shown, with details like the creative name and display duration. These events give advertisers verifiable playback records and feed the twin's analytics (impressions, dwell, campaign performance). Pair proof_of_play with campaign_interaction (taps on the surface) and digital_signage (playback/status) for a complete signage picture. See how billboards fit the marketplace on the advertise page.
Sample code
Register a device (Node.js)
const res = await fetch("https://www.nexariadigital.com/api/iot/register", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
name: "Aurora Arena QR Portal",
deviceType: "QR Kiosk",
publicLabel: "QR-AUR-03",
}),
});
const { apiKey, keyPrefix } = await res.json();
// Store apiKey NOW — it is shown only once.
console.log(keyPrefix); // e.g. "a1b2c3d4"
Register a device (curl)
curl -X POST https://www.nexariadigital.com/api/iot/register \
-H "content-type: application/json" \
-d '{"name":"Aurora Arena QR Portal","deviceType":"QR Kiosk","publicLabel":"QR-AUR-03"}'
Send a signed event (Node.js)
import { createHmac, randomUUID } from "node:crypto";
const API_KEY = process.env.NEXARIA_KEY;
const body = JSON.stringify({ eventType: "heartbeat", payload: { fw: "3.0.2" } });
const signature = createHmac("sha256", API_KEY).update(body).digest("hex");
await fetch("https://www.nexariadigital.com/api/iot/events", {
method: "POST",
headers: {
"content-type": "application/json",
"x-nexaria-key": API_KEY,
"x-nexaria-timestamp": String(Math.floor(Date.now() / 1000)),
"x-nexaria-nonce": randomUUID(),
"x-nexaria-signature": signature,
},
body,
});
Future SDK support
Official SDKs are planned to wrap key management, header signing, and retry/replay handling so you do not build the crypto by hand. JavaScript/TypeScript ships first, with other languages to follow. The API is versioned, and these HTTP endpoints remain the stable contract underneath any SDK.
Related pages
- API & Developers overview — the broader adapter framework and REST preview.
- Security — Nexaria's platform-wide security model.
- Marketplace — the digital assets your devices bring to life.
- xSPECTAR and the XRPL ecosystem.