Owner & Builder

Tesla Fleet API × Home Assistant

Getting the Tesla Fleet API running in Home Assistant — and why the "just host a file" step is the whole problem.

  • Tesla Model Y
  • Tesla Fleet API
  • Cloudflare Worker
  • Home Assistant
Tesla Fleet API × Home Assistant

Why the Fleet API

Most “Tesla in Home Assistant” integrations run on Tesla’s private owner endpoints — the same API the official mobile app uses. It works until it doesn’t: Tesla rate-limits, throttles, and breaks it on a schedule no one controls, and poll-based integrations wake the car to ask each question, draining battery for the privilege. The Tesla Fleet API is the official route, and in exchange for a bit more setup it offers three things:

  • Scoped OAuth — the app only gets what you grant it.
  • Signed commands — vehicle commands are signed with your key and delivered through the official channel.
  • Fleet Telemetry — a push stream where the car sends data to a server, instead of the server waking the car to poll.

Here is the full setup, end to end. Each step below is what I did for my Model Y, and each step links to the official Tesla Fleet API docs so you can cross-check the details.

Reference: What is Fleet API? (getting started) · Fleet Telemetry overview

Prerequisites

  • A Tesla account with multi-factor authentication enabled (required before you can register an application).
  • A vehicle — in my case a Model Y — that you can interact with in the Tesla app.
  • A domain on Cloudflare (any subdomain will do, e.g. fleet.example.com).
  • Home Assistant with HACS (the integration is a custom one).
  • openssl on whatever machine you generate the keys on.

Step 1 — Generate the key pair

Every Fleet API application uses an ECDSA key pair (P-256 curve):

  • The private key signs the requests that matter — it is what lets the app issue Vehicle Commands. Keep it secret.
  • The public key is what Tesla reads from your domain to prove you control it.

Generate both from one machine:

openssl ecparam -name prime256v1 -genkey -noout -out private-key.pem
openssl ec -in private-key.pem -pubout -out public-key.pem

Then place them deliberately:

  • private-key.pem → on the machine that will sign with it. In this setup, that is the Home Assistant instance, where the integration keeps its secret. One machine, zero repos — even a private one.
  • public-key.pem → into the Worker in the next step. It is public by construction; nothing sensitive about it.

Docs: What is Fleet API? — Step 3: Generate a Public/Private Key Pair (the official openssl commands above come from this page).

Step 2 — Host the public key (the step the docs undersell)

Tesla requires the public key to be served at an exact, fixed path on the application’s domain:

https://<your-domain>/.well-known/appspecific/com.tesla.3p.public-key.pem

This looks like “just host a file” — and that mental model is the trap:

  • It is an ownership anchor, not a one-time formality. Tesla treats control of that HTTPS endpoint as proof you own the app’s domain, and it re-checks. Their own troubleshooting docs list a missing key at this path as a cause of pairing failures — availability is a requirement for the rest of the app’s life.
  • A home server is usually out: home networks are behind CGNAT, and the endpoint that anchors the app’s identity should not depend on house uptime.
  • A static website is the wrong home: a static site serves committed files, which turns a liveness requirement into a repo edit.

The solution I use is a Cloudflare Worker on a subdomain — free tier, no infrastructure, boring on purpose.

Setup:

  1. In the Cloudflare dashboard, create a Worker (e.g. tesla-key).
  2. Attach the subdomain you want to use (fleet.example.com) to the Worker as a custom domain. This sends every request on that subdomain to the Worker; the code below returns the key only on the exact path and a 404 everywhere else.
  3. Replace the default Worker code with the snippet below, pasting your public key between the backticks.
  4. Deploy, then verify:
    • curl https://<your-domain>/.well-known/appspecific/com.tesla.3p.public-key.pem must return 200 with the key body.
    • curl https://<your-domain>/ must return 404.

The whole worker:

const TESLA_PUBLIC_KEY = `<YOUR PUBLIC KEY>`;

export default {
  async fetch(request) {
    const url = new URL(request.url);
    if (url.pathname === "/.well-known/appspecific/com.tesla.3p.public-key.pem") {
      return new Response(TESLA_PUBLIC_KEY, {
        headers: {
          "Content-Type": "application/x-pem-file",
          "Cache-Control": "public, max-age=86400"
        }
      });
    }

    return new Response("Not found", { status: 404 });
  }
};

Keep it boring: 200 on the key path with the PEM content type, no redirects, 404 everywhere else. Rotating the key later is just a new pair plus a redeploy — no other moving parts.

Docs: the register endpoint reference states the requirement verbatim — a PEM EC public key on the secp256r1 curve “must be and remain hosted at https://<app domain>/.well-known/appspecific/com.tesla.3p.public-key.pem” — and the partner endpoints include the check you can use to confirm the registered key. “Must be and remain hosted” is the sentence the getting-started guide’s “host a file” wording hides.

Step 3 — Register the application with Tesla

  1. From the Fleet API getting-started page, click Create Application and Access Dashboard.
  2. On the registration form, fill in:
    • your application name and a short description of what it does,
    • the scopes the integration needs — for this setup: offline_access, vehicle_device_data, vehicle_location, and vehicle_cmds. Skip everything else; scopes are checkboxes, and a smaller grant is a smaller attack surface.
  3. Finish the flow — Tesla issues a partner auth token for the application.
  4. Call the register endpoint with that token — once per region of operation. This is the step that quietly bites people who drive a car into a second country: the app is only “registered” in the regions you called.

Docs: What is Fleet API? — Step 4: Call the Register Endpoint · register endpoint reference · Authentication overview (scopes) · Regions and Countries.

Step 4 — Pair the vehicle

Pairing is deliberately an owner action in the Tesla app:

  1. Open https://tesla.com/_ak/<your-domain> — the domain that hosts the public key.
  2. Follow the prompt to add the application to the vehicle in the Tesla app.
  3. If it fails, checklist:
    • The Tesla app is signed in with the same Tesla account used to authorize the application.
    • The app was registered in the car’s region (Step 3, item 4).
    • The public key is still reachable at the /.well-known/ path (Step 2, item 4).

Docs: What is Fleet API? — Next Steps points to “Pairing a public key to a vehicle”; the Fleet Telemetry overview documents the tesla.com/_ak/ pairing deep-link and the same three failure modes (Tesla-app account mismatch, app not registered in the car’s region, key no longer available at the /.well-known/ path).

Step 5 — Install the integration in Home Assistant

  1. In HACS, add the tesla_fleet integration. This is the community integration I used; its README in the HACS entry covers installation and options.
  2. Restart or reload custom integrations, then Add Integration → Tesla Fleet in Home Assistant.
  3. Complete the OAuth flow with your Tesla credentials. The integration keeps the private key locally and signs Vehicle Command traffic through Tesla’s signed command flow — the private key never leaves the instance.
  4. After pairing (Step 4), the car appears with its full entity set. On my Model Y that is roughly 80 entities, including:
    • charge state, charge rate, cable and port status
    • battery level and range, odometer, speed, power draw
    • tire pressures (all four corners)
    • doors, windows, locks, frunk/trunk/sunroof covers
    • climate plus seat and steering-wheel heaters
    • location and route device trackers
    • buttons: wake, honk, flash lights, HomeLink
    • an update entity for OTA software releases

Docs: the Vehicle Commands reference documents signed command traffic, and the Vehicle Endpoints reference lists the state behind the entities in step 4.

What’s next: Fleet Telemetry

The end state is push, not pull. Instead of polling at all, the car streams fields — speed, location, state of charge, doors, tire pressures — to a server at intervals as fast as 500 ms, re-sending a field only when its value changes and its interval has elapsed. Tesla’s own pricing example is telling: a lean field configuration costs roughly a cent per hour of driving.

The remaining design question is where the WebSocket endpoint lives:

  • a tunnel from Home Assistant out to the public internet,
  • a Worker-terminated stream, or
  • the integration’s own server.

I have left this open deliberately — the car is not on telemetry yet, and the choice is an infrastructure decision, not an API one.

Docs: the Fleet Telemetry overview covers the server setup requirements (a publicly reachable WebSocket endpoint with a vehicle-trusted certificate), the vehicle configuration flow, the field-streaming behavior, and the cost example cited above; the Available Data page lists every streamable field.

Gotchas

The ones that actually cost time:

  • The key endpoint is a liveness requirement, not a one-time check. The app is only as alive as the URL that proves its domain.
  • Registration is per region. A car in a region you forgot will report the application as unregistered.
  • MFA on the Tesla account is required before you can register the application at all.
  • The private key is the only thing that can sign commands. One machine, zero repos.
  • Keep the VIN out of any public write-up — a full VIN is a serial number on a moving vehicle.

Outcome

The car in Home Assistant now rides on the official API: live state feeding the dashboards and automations the house already runs, and commands going through the signed, documented channel — no dependency on rate-limited private endpoints. The part that took the most real effort was the twenty-line Worker, and that is the point: the Fleet API’s onboarding is designed around domain ownership, and solving that step boringly — exact path, long uptime, no secrets — is what makes the integration keep working. Push telemetry is the documented next step, replacing the last of the polling with a stream the car sends on its own.