For a while, this site said Thermal Canary Pro doesn’t phone home after activation. We said it in the FAQ, in the privacy policy, in a blog post, and in the pricing copy. It was wrong. We caught it, fixed all four places the same day, and this post is the actual answer: the one request the app makes, what’s inside it, why it exists, and what happens when it fails.

No hedging, no “technically” qualifier. If you’re the kind of person who points Wireshark at software before trusting it, this is for you.

The one request

After you activate a license, Thermal Canary Pro makes exactly one kind of outbound call related to licensing: a POST to /api/v1/refresh, once at startup and at most once every 24 hours while the app keeps running.

POST /api/v1/refresh HTTP/1.1
Host: api.thermalcanary.com
Authorization: Bearer <64-hex device_token>
Content-Type: application/json
Accept: application/json

The body is empty. There is no license key in that request, no /etc/machine-id, no hostname, no hardware model, no sensor reading, no session data, no app version string. The only thing that identifies you is an opaque bearer token, minted once at activation and never rotated by this endpoint. Point mitmproxy or tcpdump -i any port 443 at the app for a day and this is genuinely the entire licensing-related footprint you’ll see.

Why it exists at all

A perpetual, offline-verifiable license (Ed25519-signed, checked locally against a public key baked into the binary) sounds like it should need zero network contact ever. It almost does. But a signed token that’s never re-checked against anything is also a token that can never be revoked, no refund enforcement, no way to stop a leaked key from running forever on a machine you no longer control.

/refresh is the one thing that makes revocation possible without turning the whole license model into an online-only scheme. On success it does three things: confirms the activation is still valid, mints a freshly-signed offline token (same device identity, new signature, so the local 14-day clock below gets reset), and logs that this device checked in. That’s it. No usage stats, no feature-flag payload, nothing else rides along.

Two different failures, two different outcomes

The server can say no to /refresh in two distinct ways, and the app treats them very differently on purpose.

DEVICE_DEACTIVATED, an immediate, no-grace-period lockout. This fires when your license key gets activated on a second machine, which force-rebinds the seat and marks your old device’s row deactivated_at. You knowingly moved the license, so there’s no reason to soften the landing, the app locks within the next /refresh cycle, up to about 24 hours.

UNAUTHORIZED, everything else: the server is unreachable, your device_token got orphaned by an ops mistake, DNS is having a bad day. This is the case that actually needs patience, because “the license server didn’t answer” and “my key got stolen” produce the exact same symptom from the client’s point of view, and punishing the first one as hard as the second one is how you lock out a paying customer for no reason.

Folding those into a single generic 401, which is what an earlier version of this endpoint did, meant a knowingly-superseded machine could ride the same grace period as an honest outage. That’s a real key-sharing hole, and it’s why the split exists now.

What happens when the server just doesn’t answer

UNAUTHORIZED (and any network-level failure, no route, TLS handshake timeout, DNS failure) starts a 14-day countdown from your last successful check-in, not from install day. Inside that window, the app keeps working exactly as if the server had said yes.

That number moved twice. It started at 30 days, got tightened to 7 after the key-sharing gap above was closed by the DEVICE_DEACTIVATED split, then got widened back out to 14 once it became clear the 7-day number was solving a problem that the DEVICE_DEACTIVATED path already solves on its own, while actively punishing the wrong group: this product’s own target user is explicitly the kind of person who goes offline for a week at a time, travelling, or running an air-gapped machine on principle. A week-long grace window is exactly long enough to make that person’s honest situation indistinguishable from a problem.

The last 4 days of that window aren’t silent either. Starting at day 10, the Settings panel shows an amber banner with a day count and a one-click “Verify now” retry. It goes red at day 13. This isn’t configurable, and that’s deliberate, the same reasoning we use for every threshold in this app: the fix for “the default is wrong” is a better default, not a settings knob nobody will find until it’s too late.

Why the client can’t quietly start sending more

None of this is a promise you have to take on faith. The client holds no private signing key, only the public half. Every offline validity check, which is the overwhelming majority of what “is this license valid” means day to day, runs entirely locally against that embedded public key, no network call involved. Your machine identifier is never written to disk anywhere; it’s read live from /etc/machine-id only at the moment a signature gets verified.

That architecture is also why the request above can’t quietly grow a new field later without you noticing, if it changes shape, it changes because a new signed binary shipped, and that binary’s diff is sitting in a public GitHub repo the day it’s released. Adding one more field to that request body isn’t a config toggle on a server somewhere, it’s a client release.

The actual line between “license enforcement” and “telemetry”

Every “no telemetry” badge on a pricing page elides these two into one claim, and most of the time they genuinely are the same thing, because most software has no reason to talk to its vendor at all once it’s paid for. This product does have a reason, revocation and one-seat enforcement, and pretending otherwise was the actual mistake, not the request itself.

So: no hardware data, no sensor readings, no session history, no crash dumps, no usage stats, no app version, ever leave your machine, over /refresh or anywhere else. One opaque token, once a day at most, so a refund or a stolen key can actually be enforced. That’s the complete list. If a future version of this app ever needs to add anything to that wire payload, this page and the privacy policy both get updated in the same commit as the code, not after.