HEART Embedded Systems logoHEART

HEART Hub#

Reading tracks1 Basic2 Intermediate
Basic: "In one minute", Sections 2, 3 and 5 to 8 (not 7.1), if you report to a monitor site. Intermediate adds 7.1, 9 and 11. Sections 4 and 10 are for people running a site. Which track am I?

Watch the health of any device from a web page, for free, with open code

Author: Milind K. Patil, Syncaissa Systems Inc. Status: Working draft 1, November 2026. A profile of Part 4 (Connections), with privacy rules from Part 5, in the HEART Standards Family. Version: 1.0 (draft) Public site: https://heartcortex.ai · Code: heart-monitor/ (Apache-2.0) Track: Basic: "In one minute", Sections 2, 3 and 5 to 8 (not 7.1), if you report to a monitor site. Intermediate adds 7.1, 9 and 11. Sections 4 and 10 are for people running a site.


In one minute#

A product with a HEART already knows whether it is healthy. HEART Hub lets its owner see that from anywhere:

device ──(reports, outbound only)──► monitor site ──► https://heartcortex.ai/device/3f9c0a71b2d4e865
  1. The device makes a random device ID (16 hex characters) and a claim code the first time it starts, and shows the claim code to its owner: on a label, a screen, or the console.
  2. The owner registers the device on the monitor site with the ID and the claim code. The claim code proves they hold the device.
  3. From then on the device reports its Pulse and alerts. The owner sees them on the device's page, gets an email when the device goes silent, and can share the page if they want.

A small sensor may send a plain JSON light report instead of envelopes (Section 5.4), over HTTPS or through an MQTT bridge (Section 5.6), and may say how long it will sleep (Section 7).

Three rules make it safe:

  1. Nothing is shown until the owner registers it, and a registered device's page is private until the owner shares it.
  2. Only public data is reported: status and fault codes, never signal values, names or anything private.
  3. Anyone can run their own monitor site. heartcortex.ai runs the same open code as everyone else.

1. Words#

WordMeaning
Monitor siteA website that receives reports and shows device pages. heartcortex.ai is one. Anyone may run another.
Device ID16 lower-case hex characters (64 random bits), such as 3f9c0a71b2d4e865. Made on the device at first start.
Claim code20 characters in four groups of five (100 random bits), such as K7Q2M-9XMBA-3RTVW-HP6DZ. Made on the device, and shown only to the person holding it.
Device keyA secret the device makes and keeps (256 random bits). It signs nothing: it proves that a report comes from the same device that said hello.
OwnerThe person who registered the device. They have an account: a random ID that never changes, opened by any of its 1 to 10 email addresses

2. Making the ID, claim code and key#

A device MUST make all three from a cryptographic random source, the first time it uses a monitor site, and keep them in storage that survives restarts. They MUST NOT be derived from a serial number, MAC address or anything else public, so that nobody can work them out.

The claim code uses the characters 0123456789ABCDEFGHJKMNPQRSTVWXYZ (Crockford Base32: no I, L, O or U, so nobody misreads them). Sites ignore case, spaces and hyphens when checking it.

The device shows the claim code only to a person in front of it: printed on a label inside the packaging, on its screen, or on its console. It MUST NOT send the claim code to anyone. It sends only a hash.

3. Saying hello#

Before its first report, the device introduces itself:

POST /v1/devices/3f9c0a71b2d4e865/hello
Content-Type: application/json

{"claimHash": "<SHA-256 hex of 'heart-claim:' + claim code, upper case, no hyphens>",
 "keyHash":   "<SHA-256 hex of the device key>",
 "template":  "heart/embeddedstandard/python/v1.2611"}
AnswerMeaning
201 {"known": true, "siteKey"}Welcome: the site now knows this ID
200 {"known": true, "siteKey"}Already known, with the same hashes. Saying hello again is harmless.
409 conflictThis ID is already taken with different hashes. The device makes a new ID, claim code and key, and tries again.
429 rate-limitedMore than 60 new IDs from one IP address in an hour. Try again in an hour.

siteKey is the site's Ed25519 public key (base64url, 32 bytes), which signs approval decisions (Section 7.1). A device that may take approvals from the site pins it at its first successful hello over HTTPS, and from then on accepts decisions signed only by that key. A later hello that names a different key is ignored for approvals. A site that offers no approvals sends no siteKey.

Hello carries no data about the device apart from its template ID. An ID that is not registered within 30 days is forgotten.

4. Registering (the owner)#

  1. The owner signs in to the monitor site (Section 4.1).
  2. They enter the device ID and claim code at /register (a QR code can fill both in: https://heartcortex.ai/register?id=3f9c0a71b2d4e865).
  3. The site checks the claim code against the hash from hello. After five wrong codes for one device, it waits an hour.
  4. The device now belongs to the owner's account, and nobody else can register it until they let it go.

4.1 Signing in#

There are no passwords. Proving you can read an inbox is the only way in.

  1. The person enters an email address. The site emails a 6-digit code. The same form signs in and makes a new account.
  2. They type the code. The site checks it, finds the account the address belongs to (or makes one), and starts a session.

Accounts, not addresses. Every account has a random ID of 12 hex characters. Devices and sessions belong to that ID, never to an address. An account holds 1 to 10 verified addresses, and any of them can sign in. One is the primary: emails from the site go there. An address belongs to one account at a time. To change address: add the new one (a code goes to it), make it primary, remove the old one. Nothing else changes.

One person, one account. For a new account, J.Smith+x@googlemail.com counts as jsmith@gmail.com: Gmail ignores dots, and +tags reach the same inbox. If that form already has an account, the site says so with a masked hint (j…@gmail.com). Disposable-mail addresses cannot make accounts.

All the limits (per address and per network; on top of these the whole site accepts at most 60000 code requests and 120000 code tries in any 10 minutes, as a flood guard):

LimitValue
A code worksOnce, for 10 minutes
Wrong tries per code15, then it is void
Between two codes to one address30 seconds
Codes per address5 an hour
Requests per address in 24 hours30, then the address is paused for 24 hours. One email tells its owner; later requests are refused silently.
Wrong codes per address15 a day (UTC), and 50 in 30 days. That allows about 600 guesses a year out of a million codes.
From one IP address, and from one network (/24 or /48)10 code requests in 10 minutes and 40 a day; 20 tries in 10 minutes and 80 a day; 5 added addresses an hour and 10 a day (also per account)
New accounts from one IP address3 a day
Human checkCloudflare Turnstile on every code request from a browser

Each wrong-code counter takes its place before the code is compared, and a right code is used up with a condition. So guesses sent all at once cannot get past the limits, and a code cannot be used twice. A right code gives its place back and clears the counters for that address and network.

Sessions last 30 days, in a cookie that scripts cannot read (HttpOnly; Secure; SameSite=Lax). The account page lists every browser that is signed in, and can sign out one or all the others. A sign-in from a new browser or country is emailed to the primary address.

5. Reporting (the device)#

POST /v1/devices/3f9c0a71b2d4e865/report
Authorization: Bearer <device key>
Content-Type: application/json

{"pulse":  <the device's public Pulse envelope>,
 "alerts": [<public Arrhythmia envelopes, newest first, at most 16>]}

This is the full report. A device may send the light report of Section 5.4 instead. The site tells them apart by the pulse key: a report without one is light.

AnswerWhat the device does
200 {"claimed": true, "nextSec": 30}Stored. Report again after nextSec seconds, or at once when something changes. nextSec is the report's intervalSec, its sleepSec (Section 7), or 2 while the faster channel is open (Section 5.5).
202 {"claimed": false, "nextSec": 300}Not registered yet. Nothing was stored. Try again in five minutes.
401Wrong key. Stop, and tell the owner.
404The site has forgotten this ID. Say hello again.
413, 400Too large, or not a valid report. Fix the report, never retry it unchanged.
429Too often. Wait nextSec seconds.

Rules for reports:

  1. Every envelope MUST be a valid public message of its type (the Valve, Part 5). The site refuses the whole report with 400 unless all of these hold, and checks nothing more:
    • each envelope is an object with specversion "1.0", type heart.pulse or heart.arrhythmia, a string id (1 to 200 characters), source of the form /heart/{systemId} or /heart/{systemId}/{instance}, time in RFC 3339, datacontenttype "application/json", heartversion 1.{minor}, heartprivacy "public", and an object data;
    • Pulse status is healthy, degraded, failing or unknown; sequence is a whole number 0 or more; intervalSec is 1 to 86400;
    • each alert has a UUID id, a fault code (Part 3 form, such as system.main-function-lost), a severity and status from the standard lists, an RFC 3339 detectedAt, and resolvedAt in RFC 3339 if present;
    • at most 16 alerts and 4 approval requests (Section 7.1), each request complete;
    • sleepSec, if present, is a whole number from 1 to 604800 (Section 7).

A device SHOULD still send messages that pass the official schemas; a site MAY check them in full.

  1. Only these fields are kept: Pulse status, sequence and intervalSec; for each alert its id, code, severity, status, detectedAt and resolvedAt; and the report's sleepSec. The site drops everything else, even if public.
  2. A report is at most 16 KB (16384 bytes). Reports less than 10 seconds after the last stored one get 429 with nextSec. While the faster channel is open (Section 5.5), the gap is 2 seconds.
  3. Reports go out over HTTPS, directly or through an MQTT bridge (Section 5.6). A device never accepts commands from a monitor site: reporting is one-way, with two narrow exceptions. One is an owner's request for a setting the device itself offered (Section 7.2). The other is a Command Center (HEART Command Center 1.0), which may be run on the same site: it may send signed signals that only lower risk, such as cutting a device's brain (API Standard 7.4 rule 8). Nothing a monitor site or Command Center sends can make a device act, approve anything or raise anything.

5.3 Figures and settings the device chooses to share#

A device MAY add two lists to its report. Both are off unless its maker or owner turns them on on the device, and both carry only numbers and short words, never names, logs or personal data:

"figures":  [{"name": "host.containers-running", "title": "Containers running", "value": 3}],
"settings": [{"name": "web.cpus", "title": "CPUs for web", "value": 1, "min": 0.5, "max": 4, "step": 0.5, "unit": "CPUs"}]
ListAt mostEach itemShown
figures64name (lower-case, digits, . and -), title (60 characters), a number value, optional unit (12 characters)On the device page, to whoever may see it
settings8As a figure, plus min, max and step: the range the device will accept from its ownerTo the owner only, with a form to change each one (Section 7.2)

The site keeps only the latest lists, and drops them with the device. The figures are data the owner chose to publish: the Valve rule of Section 8 holds for everything else. A figure is shown to everyone the page is shared with, so a device shares only what its owner would show.

5.4 The light report#

A small device need not build envelopes. It may send a light report: plain JSON, with no pulse key. The site builds the rest.

{"status":   "degraded",
 "alerts":   [{"code": "power.battery-low", "severity": "warning"}],
 "figures":  {"temperature": 21.5, "battery.volts": 3.71},
 "sleepSec": 900}
FieldNeededContent
statusYeshealthy, degraded, failing or unknown
alertsNoEvery problem the device has now: at most 16, and at most one per fault code. Each has a fault code (Part 3 form) and a severity (info, warning, error or critical). It may also have an id (a UUID), a status (open or resolved) and a detectedAt (RFC 3339). Left out, it means no problems.
figuresNoAn object of at most 64 names and numbers, such as {"temperature": 21.5}. Or the list of Section 5.3.
settings, resultsNoAs in Sections 5.3 and 7.2
sleepSecNoHow long the device will now be silent, in seconds (Section 7)

Rules for light reports:

  1. The site refuses the whole report with 400 if it has any other field, or if any value breaks the table above. A name follows Section 5.3. Every value is a finite number. The other rules of Section 5 still apply: the device key, 16 KB, and the gap between reports.
  2. The site keeps exactly what it keeps for a full report (Section 5, rule 2), made like this:
    • sequence counts up by one with every light report the site stores for the device, from 0;
    • intervalSec is 30, and the site answers with nextSec 30 (or 2, or sleepSec, as in Section 5);
    • a figure sent as a name and a number gets that name as its title, and no unit. To give titles and units, send the list of Section 5.3.
  3. The alert list is the whole truth. The site matches each alert to an open one by its fault code:
    • An alert with the code of an open alert is the same alert. It keeps its id and detectedAt, and takes the new severity. If it brings a different id, the open one is resolved and a new one opens.
    • A new alert gets the id it brings. Without one, its id is the UUID made from the SHA-256 of heart-light-alert:{deviceId}:{code}:{detectedAt} (the first 128 bits, with the version and variant bits of a version 8 UUID). Its detectedAt is the one it brings, or else the time the report arrived. Its status is open.
    • An open alert whose code is missing from the list, or that comes with status resolved, is resolved. Its resolvedAt is the time the report arrived.
    • A resolved alert that matches no open alert is stored only if it brings an id. Otherwise it is ignored.
  4. Approval requests (Section 7.1) need the full report.

5.5 The faster channel#

A device reports every 30 seconds or so. That is too slow when its owner has just changed a setting. So the site opens a faster channel for that device while its owner is busy with it:

  1. When. For 5 minutes after the owner asks for a code to change a setting (Section 7.2) or to approve a change (Section 7.1). For 2 minutes after the owner confirms a change, approves or denies one. And while a confirmed change waits for the device's answer, which is at most 10 minutes (Section 7.2). A request that only waits for its owner does not open it.
  2. What. While it is open, the site answers that device's reports with nextSec: 2, and accepts reports 2 seconds apart instead of 10.
  3. Bounded. The site answers at most 300 reports of one device in an hour this way: 10 minutes at 2 seconds. After that the usual times apply.
  4. The device decides. A device SHOULD follow nextSec. A device that sleeps may stay awake for a while to do so, or keep its own times: then the change waits for its next report.

So a confirmed change reaches a listening device within about 2 seconds, and its result is on the owner's page within about 4, plus the time on the network. Nothing else changes: the change is still signed by the site and checked by the device.

5.6 Over MQTT#

A monitor site is a web service, not an MQTT broker. A device that already speaks MQTT may send its hello and reports through a broker. A bridge relays them to the site over HTTPS, and relays the answers back:

device ──MQTT──► broker ──► bridge ──HTTPS──► monitor site
       ◄─ reply ─ broker ◄─ bridge ◄─ answer ─┘
TopicPublished byContent
heart/v1/hub/{deviceId}/helloThe deviceThe hello body of Section 3
heart/v1/hub/{deviceId}/reportThe device{"key": <device key>, "report": <a light or full report>}
heart/v1/hub/{deviceId}/replyThe bridge{"to": "hello" or "report", "status": <HTTP status>, "body": <the site's answer>}
  1. Every message uses QoS 1 and is never retained. MQTT 3.1.1 or 5 will do. Payloads are UTF-8 JSON, at most 16 KB for the report itself.
  2. The broker's access rules MUST let only that device publish on its hello and report topics and read its reply topic, and only the bridge read hello and report and publish reply. The device key travels in each report, so the broker and the bridge can see it, as an HTTPS proxy could. Connections to the broker MUST use TLS (port 8883) unless they never leave the device's own network.
  3. The bridge posts each message to /v1/devices/{deviceId}/hello or /report, with the report's key as Authorization: Bearer, and publishes the site's answer on the reply topic, status and body unchanged. If the site cannot be reached, the answer is status 503 with body null. The bridge adds nothing to a report, keeps nothing, and drops a message with a malformed topic, payload or key, or one that is too large.
  4. QoS 1 may deliver a message twice. A repeated report is refused by the gap between reports, or stored again: both are harmless.
  5. Owner requests and decisions travel back only in that answer, signed by the site as in Sections 7.1 and 7.2. The device checks them exactly as over HTTPS. A bridge cannot make one, and cannot change one without the device noticing.

This is separate from the mqtt binding of API Standard 9.5, which carries a HEART's own envelopes on a broker. A bridge in one Python file, with no dependencies, is iot-examples/heart_mqtt_bridge.py.

6. The device page#

https://heartcortex.ai/device/3f9c0a71b2d4e865 shows:

  • Health: Healthy, Degraded, Failing, or Not heard from (the Flatline)
  • Last heard from, and how often it reports. A sleeping device shows Asleep until the time it gave (Section 7).
  • Open and past alerts, each with its fault code explained in plain words (Part 3)
  • The template the device started from
SharingWho can see the page
Private (the default)Only the owner. Everyone else gets "not found", so the page does not even say the device exists.
LinkThe owner, and anyone with a secret link. Making a new link cancels the old one.
PublicAnyone who knows the device ID

The owner can also forget a device: its page, its alerts, its approval requests, its name and its owner are deleted. The site keeps only what hello gave it (the ID, the hashes of the claim code and key, and the template), so the device may be registered again with its claim code. Those are forgotten too if nobody registers it within 30 days.

6.1 AI models and Ask AI (optional)#

A site MAY let owners connect AI models with their own API keys (/account/ai): Claude or another provider, or the owner's own OpenAI-compatible server.

  • Keys are protected. They are encrypted at rest, shown again only by their last four characters, never sent to a device, and used only for that owner's own devices: for their brains through a Command Center, and for Ask AI.
  • Ask AI answers the owner's questions. On a device's page the owner may ask in plain words, such as "Does Docker need more CPU?". The model reads the device's data through read-only tools:
    • its status;
    • the figures it shared, with up to two days of their history;
    • the settings it offers, with their ranges;
    • its alerts;
    • the changes asked so far.
  • It only recommends. It changes nothing: a change is still the owner's request of Section 7.2, which the device checks again.
  • The system prompt is open. It names the commands and data the model may use; it is shown to the owner, who may change it.

7. Flatline and emails#

If no report arrives for three reporting intervals (at least 90 seconds), the page shows Not heard from, the Flatline of Part 3 (system.flatline). The site emails the owner once per silence, and again when the device is back. Emails contain the device's name and page link, never anything else.

A planned silence is not a Flatline. A device that is about to sleep says so with sleepSec in its report: a whole number of seconds, from 1 to 604800 (7 days). Then:

  1. The device is asleep until sleepSec after that report. Its page shows "Asleep until" that time, with its last health.
  2. It becomes Flatline only after a grace as well: its usual Flatline time, or a tenth of sleepSec, whichever is longer. A device that sleeps for an hour is "Not heard from" 66 minutes after its last report.
  3. No "gone quiet" email is sent before then.
  4. Each report replaces the last one's sleepSec. A report without it means the device is awake.

7.1 Approvals#

A device's HEART sometimes has a change waiting for its owner's yes: for example, a fix a brain proposed (Core Standard, Section 7). If the owner allows it, the device can ask on the monitor site.

  1. Off by default. The owner turns approvals on for each device, on its page. While off, the site ignores approval requests and stores nothing.
  2. The device asks. It adds at most 4 requests to a report. A request holds only what the owner needs to decide, never the Proposal, readings or names:
FieldContent
actionIdThe waiting Action's ID (a UUID)
digestThe Action's digest (64 hex characters), as in API Standard 6.3
titleWhat changes, at most 120 characters, such as "Ramp rate"
changeThe change in plain words, at most 200 characters, such as "2.0 → 1.6 °C per minute"
physicalWhether it acts in the physical world
expiresAtWhen the request lapses. The site keeps it at most 7 days.
  1. The owner is told. The site emails the owner's primary address once per request, at most 20 new requests per device per day.
  2. Approving needs presence. Signing in is not enough. The owner presses Approve, and the site emails a code bound to that Action and its digest. Entering it approves. A code for one request cannot approve another, or sign anyone in.
  3. Denying needs no code, because saying no is always safe. Doing nothing changes nothing.
  4. The device gets the decision in the answer to its next report: {"approvals": "on", "decisions": [{"actionId", "decision": "approve" | "deny", "digest", "at", "signature"}]}, for decisions of the last 24 hours. at is the time the site sent this answer, and signature is the site's Ed25519 signature (base64url) over the RFC 8785 JSON of {"actionId", "at", "decision", "digest"}, made with the key it gave at hello (Section 3). A site without a signing key MUST NOT offer approvals.
  5. The device checks before it acts. This is presence method remote-site of API Standard 6.3. The device MUST act on a decision only if all of these hold, and otherwise ignore it:
    • the signature verifies with the site key the device pinned;
    • digest equals the digest of the Action that is waiting, and actionId its ID;
    • at is at most 300 seconds old by the device's clock (a device that does not know the time cannot accept decisions);
    • the Action is not physical, unless the owner turned on remoteSitePhysical on the device itself.

It ignores decisions it has already handled, and its own gate, guards and limits still apply. remote-site is weaker than a passkey (webauthn): it proves the owner could read their inbox, not that a person stood at the device, and it trusts the site. That is why it is off by default, and why physical changes need a second opt-in at the device.

  1. In the templates. The Python template offers ask_owner(title, change, apply); the ESP32 starter offers a waiting action to its owner, alongside the device's button, with CONFIG_HEART_MONITOR_APPROVALS. Both are off by default, both verify every decision as in rule 7, and both offer physical changes only with a further setting (MONITOR_APPROVALS_PHYSICAL, CONFIG_HEART_MONITOR_APPROVALS_PHYSICAL). The ESP32 records an approval from the site as presenceMethod: remote-site, so its history shows it was approved from afar, and offers only actions on public or internal signals.

7.2 Owner requests: changing a setting from the site#

The owner may change a setting that the device offered (Section 5.3), from the device's page. The site never invents a change and never sends one on its own: it relays one choice of the owner, signed.

  1. Both sides opt in. The device offers the setting and its range. The owner turns on "Let me change settings from this site" on the device's page. The site needs SITE_SIGNING_KEY.
  2. The owner confirms with a fresh emailed code, bound to the device, the setting and the value. A value outside min to max, or off the step, is refused before any code is sent.
  3. The device gets it in the reply to its next report, at most 10 minutes after it was confirmed, and usually within seconds, because of the faster channel (Section 5.5). It comes as "requests": [{"requestId", "device", "setting", "value", "at", "signature"}]. value is a string. signature is Ed25519 with the site key over the RFC 8785 JSON of the other five fields.
  4. The device checks everything again, and applies the change only if all of these hold:
    • it is signed with the site key it pinned at hello;
    • it names this device;
    • at is at most 300 seconds old;
    • the request was not seen before;
    • the setting is one the device offers;
    • the value is inside its own range.

It applies the change as a person's Action with presence remote-site (API Standard 6.3), through its own gate and guards. A physical setting needs remoteSitePhysical on the device. The device may still refuse.

  1. It says what happened in its next report: "results": [{"requestId", "status": "applied" | "refused", "detail"}]. The page shows the result next to the request.

At most 30 requests a day per device, 4 sent in one reply. A request that gets no answer expires after 10 minutes.

8. Privacy rules#

RuleWhy
A device reports nothing to any site unless its maker or owner turned reporting onWatching is the owner's choice
Nothing is stored until the owner registers the deviceAn unregistered device leaves no trace beyond its hello
Only public fields are kept, and only those listed in Section 5Health never needs the private data
Figures are published data: a device shares only numbers its owner would show to everyone the page is shared withA figure such as a room temperature can say when people are home
Pages are private by defaultEven "healthy at 3 a.m." can reveal when people are home
The site keeps the latest Pulse and 90 days of alerts, then deletes themKeep what helps; nothing more
Owners can forget a device at any timeTheir data, their choice
Approval requests carry a title and a change in plain words, chosen by the device, and only if the owner turned approvals onThe owner sees what they decide on; nothing else leaves the device
The site's signing key is kept as a secret of the site, never in its databaseA stolen database cannot forge approvals
Sign-in codes and session tokens are stored only as hashes, keyed by a server secretA stolen database cannot be used to sign in

9. Running your own monitor site#

The code in heart-monitor/ is a single Cloudflare Worker file, with its data in two Amazon DynamoDB tables (heartaispec-user and heartaispec-monitor on heartcortex.ai). Anyone can run it under their own domain, change its look, or write a different site that follows this profile. A site that offers approvals also needs its own Ed25519 signing key, kept as a secret (SITE_SIGNING_KEY). Devices are pointed at a site by one setting, monitor_url. Templates default to https://heartcortex.ai, with reporting off until the product maker turns it on.

10. All requests#

RequestWhoWhat
POST /v1/devices/{id}/helloDeviceIntroduce itself (Section 3)
POST /v1/devices/{id}/reportDevice, with its keyReport (Section 5)
GET /login, POST /loginAnyoneAsk for a sign-in code by email
POST /login/verifyAnyone with the codeSign in
POST /logoutOwnerSign out
GET /accountOwnerEmail addresses and signed-in browsers
POST /account/email/add, .../verify, .../primary, .../removeOwnerManage addresses
POST /account/sessions/signoutOwnerSign out one browser, or all the others
GET /register, POST /registerOwnerRegister a device
GET /devicesOwnerMy devices
GET /device/{id}Owner, or as sharedThe device page
POST /device/{id}/shareOwnerprivate, link or public
POST /device/{id}/forgetOwnerDelete everything about the device
POST /device/{id}/approvalsOwnerTurn approval requests on or off
POST /device/{id}/approve, .../approve/verifyOwner, with an emailed codeApprove one waiting change
POST /device/{id}/denyOwnerDeny one waiting change
POST /device/{id}/requestsOwnerTurn changes from this site on or off (Section 7.2)
POST /device/{id}/askOwnerAsk the owner's own AI model about the device (Section 6.1)
GET /account/ai, POST /account/ai/add, .../test, .../removeOwnerConnect AI models with the owner's own API keys (Section 6.1)
POST /device/{id}/request, .../request/verifyOwner, with an emailed codeChange one setting the device offered
GET /v1/devices/{id}Owner, or as sharedThe page's data as JSON, for apps and other sites

The same sign-in is available as JSON, for programs and pages that use script: POST /api/auth/start {email, turnstile}, POST /api/auth/verify {email, code}, POST /api/auth/signout, GET /api/me, GET /api/me/sessions, POST /api/me/sessions/signout {id | all_others}, and POST /api/me/emails/start | verify | primary | remove {email, code?}. They accept only JSON, and never from another site.

Forms are plain HTML, and every form is checked to come from the same site. Pages run no scripts, except Cloudflare's human check on the sign-in pages. The menu (a "hamburger" button at the top right) needs no script either.

11. Threats#

ThreatDefence
Someone registers a device that is not theirsThey need the claim code, which only the person holding the device has. 100 random bits, and five tries an hour.
Someone guesses device IDs to find pagesPages are private unless shared. An unshared page and a missing one look the same.
Someone says hello first with a device's IDIDs are random and made on the device, so nobody knows one before its first hello. A second hello with different hashes is refused.
Someone sends false reports for a deviceReports need the device key. The site keeps only its hash.
A device leaks private data in a reportThe site refuses non-public envelopes, and keeps only the listed fields
Someone guesses a sign-in code15 wrong codes a day and 50 in 30 days for each address, counted before comparing, so parallel guesses do not help. Per-network limits and a human check slow down bulk attempts.
Someone uses the site to email strangersA human check, 5 codes an hour per address, a 24-hour pause after 30 requests, and per-network limits
A stolen session cookieScripts cannot read it. The owner can see every signed-in browser and sign it out, and is emailed when a new browser or country signs in.
Someone learns which addresses have accountsAdding an address sends the code first and only then says it belongs to another account. Signing in does answer differently for a new address (a known gap, kept small by the human check and the limits).
A monitor site is hackedIt holds no device keys, no claim codes and no private data. It can show false health. It can never start a change on a device. If the attacker also has the site's signing key, and its owner turned approvals on, they could approve a change the device itself asked about, matching its exact digest, before it expires; physical changes only where the owner also allowed that at the device. The device's own gate, guards and limits still apply.
Someone replays or changes a decision on the wayEach decision is signed by the pinned site key, covers the digest, and is refused after 300 seconds.
Someone approves a change with a stolen sessionApproving needs a fresh code from the owner's inbox, bound to that change.
Text from a device is used to attack a pageEverything shown is escaped, and pages allow no scripts

12. Open items#

ItemWhy it matters
Passkey sign-inEmail codes are only as safe as the inbox. Passkeys should be offered next.
"Explain this alert"A button that asks a model through the site's Reasoning Gateway, within a budget
Changing the site keyA site that must replace SITE_SIGNING_KEY today needs every device to pin again. A statement signed by the old key, as in API Standard 10.1, would let devices follow.
Signed reportsDevices with an identity key (API Standard 10.1) could sign reports, so a site could prove where they came from
More templatesReporting is in the Python and ESP32 templates first
Sharing with named peopleFamilies and teams need to share without making a page public