Tesla Fleet API in Home Assistant: HTTPS Host, Keys, nginx
The ask was small: I wanted to see the battery state of charge in Home Assistant and control the charging current. That’s it. No fleet, no app, no business model — one car, one house, one slider.
By the end of this “small ask” I had: a self-generated keypair, a publicly reachable server with a valid Let’s Encrypt certificate, an nginx config with WebSocket headers and DNS resolver tricks — and a file that must be served from a path specified down to the character:
/.well-known/appspecific/com.tesla.3p.public-key.pem
That single line is where most people get stuck. Not Home Assistant. Not the car. A static file.
Why Tesla demands this at all
Understanding the reason helps enormously, versus pasting configs blindly.
Tesla shut down the old unofficial route (impersonate the app’s credentials and talk to the internal API) and replaced it with the Fleet API. In that world you are no longer a user logging in — you are a third-party developer with an identity. And vehicle commands are cryptographically signed rather than fired off as plaintext requests.
For that to work, Tesla needs two things:
- A domain you own. You register it in the developer console.
- A public key hosted on that domain, at exactly the well-known path above, reachable over valid HTTPS.
Tesla fetches that file itself. It’s domain-ownership proof and key distribution in one: whoever can place a file there controls the domain, and the key inside is the anchor that signed commands get verified against.
From Tesla’s perspective this is clean. That’s a bold statement, but let’s grant it. From the perspective of someone who just wants to charge their own car, it means: you are now running infrastructure. A public HTTPS host with a certificate, so I’m allowed to tell a car parked outside my own front door how much current to draw. Absurd is the right word for it.
Tesla changes Fleet API details regularly — scope names, button labels in the portal, which actions must go through the command proxy. What’s below is my real setup. Verify the exact naming against Tesla’s current developer documentation, not against a blog post from last month.
The Home Assistant path
The HA side is the easy part, which is almost funny:
Settings → Devices & Services → Add Integration → Tesla Fleet
Home Assistant then shows setup instructions for Tesla OAuth / app credentials. You’ll need client credentials from your Tesla developer app, the redirect URI exactly as HA displays it, and then a browser login with your Tesla account.
The Tesla developer-app portion is, to put it kindly, the annoying bit. To put it unkindly: I’m now a “third-party developer” because I want to see my own battery level. That’s where the work is — not in HA.
The keypair
You generate an EC keypair (prime256v1). The private key stays with you and is never served:
# Private key — keep it secret, never publish it
openssl ecparam -name prime256v1 -genkey -noout -out tesla-private.pem
# Public key — this is the one that gets published
openssl ec -in tesla-private.pem -pubout -out com.tesla.3p.public-key.pem
sudo mkdir -p /etc/nginx/certs
sudo cp com.tesla.3p.public-key.pem /etc/nginx/certs/
The filename is not negotiable, and the private key does not belong in
the same directory you’re serving. I deliberately keep mine outside
/etc/nginx/certs.
The nginx configuration
This is my actual setup, with hostnames anonymised. Replace
ha.example.com with your public domain and ha-backend.example.com with
your internal HA address (yours might be a fixed IP like 192.168.1.50 —
mine isn’t, and yours will certainly differ from either).
# Needed at http level, otherwise nginx doesn't know $connection_upgrade
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# HTTP (80) — allow ACME, redirect everything else to HTTPS
server {
listen 80;
listen [::]:80;
server_name ha.example.com;
# This must STAY reachable on port 80, or every certificate
# renewal fails silently.
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
# HTTPS (443)
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name ha.example.com;
ssl_certificate /etc/letsencrypt/live/ha.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ha.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;
add_header Referrer-Policy no-referrer-when-downgrade;
# Home Assistant reverse proxy
location / {
# resolver + variable = nginx resolves the name at RUNTIME.
# Without it nginx caches the IP at startup — fatal behind DDNS.
resolver 1.1.1.1 8.8.8.8 valid=300s ipv6=off;
set $backend "http://ha-backend.example.com:8123";
proxy_pass $backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Without these two lines the HA frontend won't load properly
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# HA keeps long-lived connections open
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;
}
# IMPORTANT: its own location block, so the proxy above
# doesn't swallow this path.
location /.well-known/appspecific/com.tesla.3p.public-key.pem {
root /etc/nginx/certs;
}
}
Three parts deserve attention:
The ACME path on port 80. It’s tempting to redirect port 80 wholesale. Then you get a certificate once, none in 90 days — and you find out when Tesla can no longer fetch your key file.
The dedicated location block for the Tesla file. Otherwise location /
with proxy_pass hands the request to Home Assistant, which dutifully
returns 404. nginx prefers the longer exact prefix here, and that’s most of
the battle.
resolver plus set $backend. nginx resolves a hostname hardcoded in
proxy_pass exactly once, at startup. My HA sits behind dynamic DNS; when
the IP changes, nginx happily proxies into the void until someone reloads
it. Going through a variable forces runtime resolution. This isn’t
decoration — it’s the reason the setup doesn’t quietly die overnight.
Then:
sudo nginx -t && sudo systemctl reload nginx
curl -sI https://ha.example.com/.well-known/appspecific/com.tesla.3p.public-key.pem
You want a 200 OK. Anything else, fix it now rather than later inside
the Tesla portal.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Key URL returns 404 | location / swallows the path |
Add a dedicated location block for the PEM, nginx -t, reload |
| Key URL returns the HA login | Same problem, proxy matches first | As above; the block must live in the 443 server |
| Tesla won’t accept the domain | Cert missing/expired, or HTTP only | Set up Let’s Encrypt, test renewal |
| Certificate never issued | ACME path is being redirected to HTTPS | Keep /.well-known/acme-challenge/ open on port 80 |
| File downloads instead of displaying | Content-Type set oddly | Usually harmless; don’t override it if Tesla can fetch it |
| OAuth fails at login | Wrong Tesla region or wrong account | Use the account for the correct region; create the app there |
| Login hangs | 2FA not completed | Actually confirm the second factor inside the flow |
| Entities appear, commands fail | Missing Fleet API permissions/scopes | Check scopes on the developer app, re-authorize |
| Redirect error | Redirect URI mismatch | Copy the value from HA exactly, character for character |
| HA frontend half-loads | WebSocket headers missing | Set Upgrade/Connection, add the map block |
| Dead connection after an IP change | nginx cached DNS | Use resolver + set $backend |
What does NOT work / what wasted my time
- A self-signed certificate. Tesla fetches the file with a normal, validating client. No publicly trusted chain, no chance.
- An IP instead of a domain. The path has to live under a domain you registered in the console.
- Dropping the file into the HA config directory and hoping. Home
Assistant does not serve
/.well-known/the way you’d like. The web server in front of it is the right place. - Throwing a Cloudflare tunnel or similar layer in blindly. It can work
— but every layer that rewrites headers or paths is one more place the
file stops arriving cleanly. Test from outside with
curl, not from your own network. - Trailing slashes and casing. The path is exact.
com.tesla.3p.public-key.pem, nothing else. - Clicking around the Tesla portal too early. Get
curlgreen from the outside first, then go to the portal. The other way round you’ll debug the wrong layer.
Security: please actually think about this
With this setup your Home Assistant instance sits on the open internet. That’s a completely different risk class than a box on your LAN.
- Strong, unique passwords, and enable 2FA in HA.
- Keep HA and nginx updated. Automatically, not “when I remember”.
- Fail2ban or rate limiting in front is not paranoia.
- And the key point: the only thing that strictly must be public is the static PEM file. Nothing else. You can just as well run a minimal nginx host that serves only that one path and returns 404 for everything else, while Home Assistant itself stays behind a VPN or Tailscale. I run the full reverse proxy because I wanted remote access anyway. If you don’t want it, don’t build it.
Verdict
This is a lot of infrastructure for a car. Keypair, domain, certificate, reverse proxy, developer account — for a slider and a state of charge. And whether that slider still works next year isn’t decided by my server. It’s decided by Tesla.
Self-hosting is worth it if you already run a server with a public domain, if nginx and certificates are everyday tools for you, and if you’d rather not add another cloud service between you and your car. Then it’s a two-hour job and it runs.
Self-hosting is not worth it if this article is your first encounter
with nginx -t. Take a managed service instead: Teslemetry and
Tessie handle exactly this part for you — both are paid subscriptions,
both integrate well with Home Assistant. The community Tesla Custom
integration and the local Tesla BLE route also exist; BLE is appealing
because it skips the cloud entirely, but it only works within Bluetooth
range and is a project of its own. Which option supports which vehicles and
commands today changes fast, so check before you commit.
My takeaway: the hard part of this integration is neither Home Assistant nor Tesla. It’s one static file that has to sit in exactly one place — and discovering how many ways there are to miss that one file.