Skip to content

First device walkthrough

This is the one page that takes you the whole way: from an empty tenant to a device whose telemetry is stored, streamed live, and proven on the blockchain — using nothing but curl and the HTTP ingest path. Follow it top to bottom and you will have a working, anchored device by the end.

Every command is real and copy-paste runnable. Replace the placeholders (<...>) and the example IDs with the values you get back as you go.

flowchart LR
  A["1. Sign in<br/>(JWT)"] --> B["2. Profile"]
  B --> C["3. Provisioning key"]
  C --> D["4. Claim device<br/>(device_id + api_key)"]
  D --> E["5. Send telemetry"]
  E --> F["6. See it live"]
  F --> G["7. Anchored + verified"]
  • A CORE-M tenant and a tenant-admin login (email + password).
  • curl and jq (optional, for pretty output).
  • The API base URL. We use https://api.kronoxdata.com; telemetry ingest is on port 8080.
  1. Sign in and get a tenant-admin token.

    Dashboard login exchanges your credentials for a JWT (an Ed25519-signed access token). You’ll use it as a bearer token for every admin call in this walkthrough — creating profiles, keys, and reading anchor status.

    Terminal window
    curl -X POST https://api.kronoxdata.com/api/v1/auth/login \
    -H "Content-Type: application/json" \
    -d '{
    "email": "you@example.com",
    "password": "<your-password>"
    }'
    {
    "access_token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...",
    "token_type": "Bearer",
    "expires_in": 3600
    }

    Save it for reuse:

    Terminal window
    export COREM_JWT="eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9..."
  2. Create a device profile.

    A profile holds the defaults a device inherits — tags, anchoring policy, telemetry schema, and OTA policy. You can skip this and use your tenant defaults, but creating one now means the device comes up configured. Keep the schema permissive for your first device.

    Terminal window
    curl -X POST https://api.kronoxdata.com/api/v1/profiles \
    -H "Authorization: Bearer $COREM_JWT" \
    -H "Content-Type: application/json" \
    -d '{
    "name": "Temperature Sensor v2",
    "type": "temp-sensor",
    "default_tags": { "region": "eu" },
    "anchoring_policy": { "mode": "merkle_batch" }
    }'
    { "profile_id": "prof_temp_v2", "name": "Temperature Sensor v2" }
  3. Create a provisioning key (template).

    A provisioning template issues a one-time provision key a device presents to claim its own identity in the field. It references the profile from step 2 and can constrain which hardware_ids may claim it and when the key expires.

    Terminal window
    curl -X POST https://api.kronoxdata.com/api/v1/provisioning/templates \
    -H "Authorization: Bearer $COREM_JWT" \
    -H "Content-Type: application/json" \
    -d '{
    "name": "factory-batch",
    "profile_id": "prof_temp_v2",
    "hardware_id_pattern": "HW-*",
    "one_time": true,
    "expires_in_seconds": 86400
    }'
    {
    "template_id": "tmpl_5d1a",
    "provision_key": "pk_live_Td9vQ2sR... (shown once)"
    }

    The provision_key is shown once — this is the secret you flash onto, or hand to, the device. Save it:

    Terminal window
    export PROVISION_KEY="pk_live_Td9vQ2sR..."
  4. From the “device”, claim it.

    Now act as the device. The device presents its provision key, a human-readable name, and its stable hardware_id. CORE-M derives the tenant from the key (never from a client field), creates the device record, and issues its long-lived API key — returned exactly once.

    Terminal window
    curl -X POST https://api.kronoxdata.com/api/v1/provision \
    -H "Content-Type: application/json" \
    -d "{
    \"provision_key\": \"$PROVISION_KEY\",
    \"device_name\": \"Lobby sensor\",
    \"hardware_id\": \"HW-001\"
    }"
    {
    "device_id": "dev_8f3a",
    "api_key": "sk_live_9Qk3pR2wXn7vJ4mB..."
    }

    That api_key (prefix sk_live_) is the device’s credential. Save both:

    Terminal window
    export DEVICE_ID="dev_8f3a"
    export DEVICE_KEY="sk_live_9Qk3pR2wXn7vJ4mB..."
  5. Send your first telemetry point.

    Authenticate with the device key and POST a TelemetryPoint to the ingest endpoint on port 8080. The timestamp is RFC 3339; everything measured goes in numeric_values, states in string_values.

    Terminal window
    curl -X POST https://api.kronoxdata.com:8080/api/v1/telemetry \
    -H "Authorization: Bearer $DEVICE_KEY" \
    -H "Content-Type: application/json" \
    -d "{
    \"points\": [{
    \"device_id\": \"$DEVICE_ID\",
    \"timestamp\": \"2026-05-29T14:30:00Z\",
    \"numeric_values\": { \"temperature\": 22.5, \"humidity\": 65 },
    \"string_values\": { \"status\": \"ok\" }
    }]
    }"
    { "accepted": 1, "rejected": 0 }

    accepted: 1 means the point passed auth, was matched to a known device, and passed schema validation. Behind that ack the device’s last_seen updated, it flipped offline → online, and the point was enriched, dual-written to the hot and cold stores, fanned out live, and queued for anchoring. See Sending telemetry for the full shape and the other encodings.

  6. See it live.

    The point is already on the dashboard. Open your tenant dashboard and the device tile for Lobby sensor shows temperature: 22.5 updating in real time — these tiles read the hot store and the live fan-out.

    To consume the same stream programmatically, subscribe over WebSocket. The dashboard-bff pushes every validated point for a device you’re watching:

    Terminal window
    # Subscribe to live telemetry for one device (WebSocket)
    wscat -c "wss://api.kronoxdata.com/ws?token=$COREM_JWT" \
    -x '{"subscribe":"telemetry.live","device_id":"dev_8f3a"}'

    Each accepted point arrives on telemetry.live.{tenant}.{device} within milliseconds of ingestion.

  7. Confirm it gets anchored — and verify the proof.

    This is the part that makes CORE-M CORE-M. Your point’s hash joins a Merkle batch, the batch root is committed to the BSV blockchain in one transaction, and you can verify this exact point against that on-chain commitment.

    Your queued point joins a Merkle batch, which flushes after 1000 points or 30 seconds. You can watch batches form on the anchors feed:

    Terminal window
    curl "https://api.kronoxdata.com/api/v1/anchors" \
    -H "Authorization: Bearer $COREM_JWT"

    Then ask the verifier for a portable proof of this exact point. The verifier recomputes the canonical hash from the fields you submit and looks it up — so the values must match what you sent exactly. While the point is still pending it returns NOT_FOUND; once the batch is anchored and confirmed it returns the Merkle path plus an SPV/BEEF proof of block inclusion:

    Terminal window
    curl -X POST "https://api.kronoxdata.com/api/v1/verify/raw" \
    -H "Authorization: Bearer $COREM_JWT" \
    -H "Content-Type: application/json" \
    -d "{
    \"device_id\": \"$DEVICE_ID\",
    \"timestamp\": \"2026-05-29T14:30:00Z\",
    \"numeric_values\": { \"temperature\": 22.5, \"humidity\": 65 },
    \"string_values\": { \"status\": \"ok\" }
    }"
    {
    "verified": true,
    "txid": "4f3b9a...c1d0",
    "merkle_root": "a1b2c3...",
    "merkle_path": [{ "hash": "", "is_right": false }, { "hash": "", "is_right": true }],
    "block_height": 871234,
    "beef_hex": "0100beef..."
    }

    verified: true means the verifier recomputed your point’s canonical hash (SHA256(device_id || timestamp_seconds_be || jcs_payload)), walked the Merkle path to the root, and confirmed that root sits in the named blockchain transaction. Your first reading is now provable to anyone, forever. See Anchoring and Merkle proofs for the full machinery.

Pick a richer protocol

Operate the device

Prove the data