It was supposed to be a five-minute config change. It ended at 4 in the morning, staring at a TLS handshake and finally understanding why my car (and AI assistant) had been lying to me for five hours.
Here's the whole thing, because the punchline is a failure mode I'd never seen and won't forget.
The backstory: a metered API I wanted off of
A while back I migrated my self-hosted TeslaMate from Tesla's old Owner API to the newer Fleet API. (Long story. Don't ask.) The Fleet API is metered — Tesla bills per request against a $10/month credit — and TeslaMate, by design, polls your car relentlessly. The bill made the case all by itself:
| App | Category | Requests | Cost |
|---|---|---|---|
tm-fleet (TeslaMate) | Data | 12,602 | $25.20 |
| Streaming Signals | 21,903 | $0.15 |
And that wasn't even a full month — nor was it the real cost. The cycle had run just 14 days, not even half of June, and TeslaMate had already burned past $25, almost entirely from one line item: hammering the metered vehicle-data endpoint.
Worse, that was the de-tuned number — I'd already throttled the polling intervals, trading away drive resolution just to slow the meter. Even crippled, it was pacing toward ~$55/month, and I was already at 82% of a spend cap I'd raised once — on a collision course with the ceiling, where Tesla blocks further requests and TeslaMate simply stops logging. As much as I love these charts and graphs, it's not worth that.
The Owner API — the original, unofficial, free one — was still right there. The plan was simple: revert.
I was wrong about "simple."
How TeslaMate chooses an API (it's just env vars)
There's no "API mode" switch. TeslaMate's target is driven entirely by environment variables, and the Owner API is the default when they're all absent. Fleet mode just overrides them:
TESLA_API_HOST: https://fleet-api.prd.na.vn.cloud.tesla.com
TESLA_AUTH_HOST: https://auth.tesla.com
TESLA_AUTH_PATH: /oauth2/v3
TESLA_AUTH_CLIENT_ID: <fleet app id>
TESLA_WSS_HOST: "ws://<lan-ip>:8081" # a local streaming proxy I'd stood up
Reverting = deleting all of that so the defaults come back. I even had a clean pre-conversion docker-compose backup sitting next to the live one. cp, docker compose up -d, done.
Friends, I was not done.
Wall #1: "Signed in successfully" is a lie
I restored the Owner config, signed in with Owner API tokens, and got the big green "Signed in successfully." Then the car sat at unavailable. No location, no battery, no state.
Here's the trap, and it's worth internalizing for any app: TeslaMate's sign-in only performs a token refresh. It never calls the vehicle API. A green checkmark proves your refresh token is valid and absolutely nothing else. The first real vehicle fetch fails silently in the background, and the UI cheerfully renders a stale car record from the database. The dashboard looks alive while the API is stone dead.
Lesson one: believe the logs, not the UI. And the logs said:
GET https://owner-api.teslamotors.com/api/1/products -> 403
{"error": "forbidden, see https://developer.tesla.com/docs/fleet-api"}
A 403 forbidden, on the account-level endpoint, with a message politely pointing me at the Fleet API.
Wrong diagnosis #1: "Tesla killed my account"
A 403 — not a 401 — means authenticated but not allowed. The account-wide /products call failing, with a literal "go use Fleet" body, reads exactly like Tesla had migrated my account to Fleet-only and revoked Owner access for good.
So I tested it properly. I minted a fresh, genuine Owner API token with tesla_auth and decoded the JWT: aud: owner-api.teslamotors.com, azp: ownerapi. Unambiguously an Owner-scoped credential. Pasted it in. Refresh succeeded (200)… and the vehicle call still returned 403.
Valid Owner token, account endpoint forbidden. I called time of death: the Owner API was gone for me. Pivot to cutting Fleet costs instead.
I was wrong. I just wouldn't learn that for another two hours.
Wrong diagnosis #2: the release that "fixes 403"
Poking through settings, I noticed a brand-new major release — v4.0.0, shipped the day before — and its changelog opened with:
This release resolves the issue of 403 Forbidden errors with Owner API tokens.
My exact bug, fixed yesterday. The mechanism seemed obvious: TeslaMate sends a User-Agent: TeslaMate/<version> header, Tesla had surely started blocking the old 3.x string, so a version bump would fix it.
I backed up the database, pinned the teslamate container in docker-compose to teslamate:4.0.0, swapped to the Owner config, re-signed-in, and watched:
GET https://owner-api.teslamotors.com/api/1/products -> 403
Still forbidden, on a confirmed-running v4.0.0. The user-agent theory died too. Two confident diagnoses, two strikeouts.
The real cause: your TLS version picks your token's scope
This is the part I'd never have guessed in a year, and it's genuinely clever-evil.
Digging through the closed PRs behind that changelog, I hit PR #5390 — "Bump Tesla Auth API to TLS v1.3 to ensure tokens are valid for owner API" — and a contributor's note that lit the whole thing up:
"OTP 26's TLS stack causes auth.tesla.com to issue Fleet-API-scoped access tokens on refresh, which owner-api rejects with 403. The fix results in Owner-API-scoped tokens as before."
Read that twice. auth.tesla.com decides what scope to mint based on the TLS version of the refresh call.
- Refresh over TLS < 1.3 → you get a Fleet-scoped token →
owner-api403s it. - Refresh over TLS 1.3 → you get an Owner-scoped token →
owner-apisays 200.
That's why everything looked like a dead account. My refresh kept succeeding with a 200, but it was silently handing me a Fleet token every single time, because my client wasn't negotiating TLS 1.3. The token was "valid." It was just the wrong species. My account was never decommissioned — the handshake was quietly determining the outcome.
v4.0.0 had tried to fix this by jumping to OTP 28 and hoping the new stack defaulted to TLS 1.3. For a lot of us it still negotiated down. The real fix — explicitly pinning the auth host to TLS 1.3 — landed in PR #5406, committed that same day:
# lib/teslamate/http.ex
System.get_env("TESLA_AUTH_HOST", "https://auth.tesla.com") => [
protocols: [:http1, :http2],
conn_opts: [transport_opts: [versions: [:"tlsv1.3"]]]
],
And the detail that confirmed this was a Tesla-side change, not a TeslaMate quirk: the Home Assistant Tesla integration was scrambling to apply the exact same TLS 1.3 fix in the same week. When two independent clients force TLS 1.3 on the same day, that's not coincidence — that's the auth server tightening underneath everyone.
The fix: a 90-second image swap
TeslaMate's CI publishes a Docker image for every PR, so I didn't have to build anything. I just pointed at the fix:
image: ghcr.io/teslamate-org/teslamate:pr-5406
Owner config, PR image, down && up -d, re-sign-in with the Owner token. Then the most beautiful log output of the night — which is to say, the absence of output:
POST https://auth.tesla.com/oauth2/v3/token -> 200
No 403. None. For five hours every fetch had been a 403 every 30 seconds; now they were simply gone. The refresh negotiated TLS 1.3, Tesla minted an Owner-scoped token, and the car snapped to online with live telemetry.
The footnote that buried the user-agent theory for good: the PR image self-reports 4.1.0-dev, so every request now goes out as User-Agent: TeslaMate/4.1.0-dev — a dev build string — and Tesla accepts it without complaint. It was never the user-agent. It was always the handshake.
Update, by daylight: that PR has since been merged and cut as a proper release — the TLS 1.3 fix now ships in TeslaMate v4.0.1 (the current Latest). So if you hit this today, skip the PR-image dance entirely: a normal upgrade to v4.0.1 or later carries the fix.
The receipt
The entire point, in one number:
TeslaMate Fleet spend, first 14 days of June: $25.35 (on pace for ~$55/mo)
TeslaMate Fleet spend after the revert: ~$0
All other Fleet apps (HA + SWEAT): ~$1/mo
Monthly free credit: $10
From a meter trending past a $50 month bogey — and minutes from tripping a spend cap that would've taken TeslaMate offline — to effectively zero, five hours and one existential TLS crisis later than planned.
The quiet bonus: back on the free Owner API, I could pull those polling throttles right back out. So I didn't just kill the bill — I got back the resolution and drive precision I'd sacrificed trying to survive the meter. Lower cost and better data, at the same time. That's my idea of a win:win outcome.
What I'd tell the next person
401vs403is the whole fork. 401 = bad credential, fixable with a new token. 403 = authenticated but not allowed — look upstream at scope, account state, or whatever decided your scope.- "Signed in" can be a lie. If an app's sign-in only refreshes a token, success tells you nothing about whether the resource API works. Find the real call and watch it.
- The UI will show you ghosts. TeslaMate fell back to a cached vehicle record and rendered a plausible car while every API call failed. The dashboard is not evidence.
- The fix can be hours old. The patch I needed was committed the same day, sitting in an unmerged PR with a prebuilt image. "Latest release" is not the frontier — open issues and PRs are.
- TLS can carry semantics, not just security. A server choosing what to give you based on your negotiated TLS version is a failure mode I'll never un-see. If auth behaves differently across two environments and nothing else explains it, check the handshake.
And the meta-lesson, since I did this with an AI pair-debugger riding shotgun the entire night: it twice declared the Owner API "welded shut, account decommissioned," with real confidence and real evidence behind it. Both times, the thing that actually moved the needle was a dumb human hunch — go look at the new release; go read the PR thread. The tooling is phenomenal at chasing a trail at speed. Deciding which trail is still the job.
Now I'm going to bed.
Appendix: is this you? (the 30-second check)
If TeslaMate (or any Owner API client) shows the car as unavailable and your logs look like this:
POST https://auth.tesla.com/oauth2/v3/token -> 200 # refresh "works"
GET https://owner-api.teslamotors.com/api/1/products -> 403 # ...but forbidden
{"error": "forbidden, see https://developer.tesla.com/docs/fleet-api"}
…then a valid token is being scoped wrong by an old TLS negotiation. It is almost certainly not a dead account. Run a build that pins the auth host to TLS 1.3 — TeslaMate v4.0.1 or later (released June 14, 2026) — and the 403 turns into a 200.