Skip to content

Instantly share code, notes, and snippets.

@Ruud-cb
Created July 16, 2026 14:55
Show Gist options
  • Select an option

  • Save Ruud-cb/652d4831d636059ab84a41b6b2840230 to your computer and use it in GitHub Desktop.

Select an option

Save Ruud-cb/652d4831d636059ab84a41b6b2840230 to your computer and use it in GitHub Desktop.
# Copy this file to .env and fill in your own values:
# cp .env.example .env
#
# Do NOT commit .env to git (add it to .gitignore).
# Required -- from your app on developer.tesla.com
CLIENT_ID=paste_your_client_id
CLIENT_SECRET=paste_your_client_secret
# Required for registration (tesla_register.py): your registered domain,
# without https:// and without a path. Your public key must be hosted here.
DOMAIN=example.com
# Optional -- defaults are shown below
# REGION: eu, na or cn
REGION=eu
# PORT: must match the redirect URI in your Tesla app (http://localhost:PORT/callback)
PORT=8080

To generate new access token:

  • Follow the normal EVCC instructions: register with Tesla developer portal such.

  • Use your favorite AI friend to help you through the setup in combination with these files.

  • fill in the .env file

  • Run tesla-register.py - generates a keypair. The public one needs to be hosted on a https domain name in /.well-known/appspecific/

  • Run the tesla-auth.py - truly authenticates you and gets an access token.

  • In the terminal you'll receive the info that needs to be put in EVCC eventually.

  • Run the tesla-register.py script again to register in both countries to avoid a known error in EVCC when it is trying to connect to the wrong Tesla country endpoint.

  • If tesla-auth.py fails, script can be run with --code [code from url] to complete the request

#!/usr/bin/env python3
"""
One-time Tesla Fleet API token generator (READ-ONLY).
Run this on your laptop: python3 tesla_token.py
Requires only Python 3 (stdlib) -- no pip installs, no public address.
Already have a code from the browser (e.g. after an aborted run)? Exchange it
directly without logging in again:
python3 tesla_token.py --code EU_xxxxxxxx
Config is read from a .env file next to this script (see .env.example) or from
real environment variables. Environment variables take precedence over .env.
Prerequisite: you have completed the partner registration (public key at your
well-known path + register call for your region). Without that step the token
exchange will fail.
"""
import argparse
import http.server
import json
import os
import secrets
import sys
import urllib.error
import urllib.parse
import urllib.request
import webbrowser
def load_dotenv(path):
"""Minimal .env parser (stdlib, no dependencies).
Only sets values that aren't already present as environment variables,
so real env vars take precedence over the .env file."""
if not os.path.exists(path):
return
with open(path, encoding="utf-8") as fh:
for raw in fh:
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, val = line.partition("=")
os.environ.setdefault(key.strip(), val.strip().strip('"').strip("'"))
# Load .env: defaults to next to this script; override with env var TESLA_ENV_FILE
_here = os.path.dirname(os.path.abspath(__file__))
load_dotenv(os.environ.get("TESLA_ENV_FILE", os.path.join(_here, ".env")))
# ---------- CONFIG (from .env or environment) ----------
CLIENT_ID = os.environ.get("CLIENT_ID", "") # from developer.tesla.com dashboard
CLIENT_SECRET = os.environ.get("CLIENT_SECRET", "") # same; stays local, never in browser
REGION = os.environ.get("REGION", "eu") # "eu", "na" or "cn"
PORT = int(os.environ.get("PORT", "8080")) # must match the redirect URI
# -------------------------------------------------------
if not CLIENT_ID or not CLIENT_SECRET:
sys.exit(
"CLIENT_ID and/or CLIENT_SECRET are missing.\n"
"Put them in a .env file next to this script (see .env.example),\n"
"or pass them as environment variables."
)
REDIRECT_URI = f"http://localhost:{PORT}/callback"
# Read-only data -> no vehicle_cmds / vehicle_charging_cmds.
# Without command scopes this token technically CANNOT send commands to the car.
SCOPE = "openid offline_access vehicle_device_data"
AUTH_URL = "https://auth.tesla.com/oauth2/v3/authorize"
TOKEN_URL = "https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/token"
AUDIENCE = f"https://fleet-api.prd.{REGION}.vn.cloud.tesla.com"
state = secrets.token_urlsafe(16)
result = {}
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
parsed = urllib.parse.urlparse(self.path)
params = urllib.parse.parse_qs(parsed.query)
code = params.get("code", [None])[0]
# Ignore anything that isn't the real callback-with-code (e.g. /favicon.ico).
if parsed.path != "/callback" or not code:
self.send_response(404)
self.end_headers()
return
result["code"] = code
result["state"] = params.get("state", [None])[0]
self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers()
self.wfile.write(
"Done. Close this tab and return to the terminal.".encode("utf-8")
)
def log_message(self, *args): # suppress log lines
pass
def exchange_code(code):
"""Exchange an authorization code for access + refresh tokens and print them."""
data = urllib.parse.urlencode({
"grant_type": "authorization_code",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"code": code,
"audience": AUDIENCE,
"redirect_uri": REDIRECT_URI,
}).encode("utf-8")
req = urllib.request.Request(
TOKEN_URL, data=data,
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
try:
with urllib.request.urlopen(req) as resp:
tok = json.load(resp)
except urllib.error.HTTPError as e:
body = e.read().decode(errors="replace")
hint = ""
if "invalid_auth_code" in body or e.code in (400, 401):
hint = ("\nHint: the code is probably expired or already used. "
"Run the script again without --code to get a fresh code.")
sys.exit(f"Token exchange failed ({e.code}): {body}{hint}")
print("\n=== Paste these into your EVCC vehicle config ===")
print(f"clientId: {CLIENT_ID}")
print(f"accessToken: {tok['access_token']}")
print(f"refreshToken: {tok['refresh_token']}")
print("\nEVCC will refresh the access token itself from now on using the refreshToken.")
def get_code_via_browser():
"""Open the browser, catch the callback and return the authorization code."""
params = {
"client_id": CLIENT_ID,
"redirect_uri": REDIRECT_URI,
"response_type": "code",
"scope": SCOPE,
"state": state,
"prompt": "login",
}
url = AUTH_URL + "?" + urllib.parse.urlencode(params)
print("If the browser doesn't open by itself, paste this URL:\n" + url + "\n")
webbrowser.open(url)
# Handle requests until the real callback with a code arrives
# (the browser sometimes fires /favicon.ico first -- we ignore that).
srv = http.server.HTTPServer(("127.0.0.1", PORT), Handler)
print(f"Waiting for the redirect to {REDIRECT_URI} ...")
while "code" not in result:
srv.handle_request()
if result.get("state") != state:
# Not a hard stop: a local, self-started script makes CSRF unlikely.
# Don't throw the code away -- just report it.
print("WARNING: state did not match. For a locally started script this is "
"usually harmless; continuing with the received code.")
return result["code"]
def main():
parser = argparse.ArgumentParser(
description="Generate Tesla Fleet API tokens (read-only) for EVCC."
)
parser.add_argument(
"--code",
help="Exchange an already-obtained authorization code directly "
"(skip the browser flow).",
)
args = parser.parse_args()
code = args.code if args.code else get_code_via_browser()
exchange_code(code)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
One-time Tesla Fleet API partner registration (for EVCC).
Fixes the "412 Precondition Failed" in EVCC. That error means your partner
account isn't registered with Tesla yet -- not that your tokens are wrong.
Registration must happen in BOTH regions (eu + na), because EVCC detects the
region by trying the NA endpoint first.
Usage (run on your laptop, same folder as your .env):
python3 tesla_register.py
Flow:
1st run -> generates an EC keypair and explains where to host the public key.
It then stops until the file is online.
2nd run -> sees that the key is reachable and registers in eu + na.
Requires: Python 3 (stdlib) + openssl in PATH (or the cryptography package as a
fallback). No pip installs needed when openssl is available.
"""
import json
import os
import shutil
import subprocess
import sys
import urllib.error
import urllib.parse
import urllib.request
def load_dotenv(path):
"""Minimal .env parser; real env vars take precedence."""
if not os.path.exists(path):
return
with open(path, encoding="utf-8") as fh:
for raw in fh:
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, val = line.partition("=")
os.environ.setdefault(key.strip(), val.strip().strip('"').strip("'"))
_here = os.path.dirname(os.path.abspath(__file__))
load_dotenv(os.environ.get("TESLA_ENV_FILE", os.path.join(_here, ".env")))
CLIENT_ID = os.environ.get("CLIENT_ID", "")
CLIENT_SECRET = os.environ.get("CLIENT_SECRET", "")
DOMAIN = os.environ.get("DOMAIN", "").replace("https://", "").strip("/")
if not CLIENT_ID or not CLIENT_SECRET or not DOMAIN:
sys.exit(
"CLIENT_ID, CLIENT_SECRET and/or DOMAIN are missing.\n"
"Put them in your .env file (see .env.example)."
)
# Read-only data -> no command scopes.
SCOPE = os.environ.get("PARTNER_SCOPE", "openid vehicle_device_data")
TOKEN_URL = "https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/token"
REGIONS = {
"eu": "https://fleet-api.prd.eu.vn.cloud.tesla.com",
"na": "https://fleet-api.prd.na.vn.cloud.tesla.com",
}
PRIV_KEY = os.path.join(_here, "tesla-private-key.pem")
PUB_KEY = os.path.join(_here, "com.tesla.3p.public-key.pem")
WELL_KNOWN = f"https://{DOMAIN}/.well-known/appspecific/com.tesla.3p.public-key.pem"
def sh(cmd):
subprocess.run(cmd, check=True, capture_output=True)
def _keygen_openssl(openssl):
print(f"Generating EC keypair (prime256v1) via openssl ({openssl}) ...")
sh([openssl, "ecparam", "-name", "prime256v1", "-genkey", "-noout",
"-out", PRIV_KEY])
sh([openssl, "ec", "-in", PRIV_KEY, "-pubout", "-out", PUB_KEY])
def _keygen_cryptography():
"""Fallback without openssl, via the 'cryptography' package."""
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
print("Generating EC keypair (P-256) via the cryptography package ...")
priv = ec.generate_private_key(ec.SECP256R1())
with open(PRIV_KEY, "wb") as fh:
fh.write(priv.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.TraditionalOpenSSL,
serialization.NoEncryption(),
))
with open(PUB_KEY, "wb") as fh:
fh.write(priv.public_key().public_bytes(
serialization.Encoding.PEM,
serialization.PublicFormat.SubjectPublicKeyInfo,
))
def ensure_keypair():
"""Generate a NIST P-256 EC keypair if it doesn't exist yet.
Uses openssl if available, otherwise the cryptography package."""
if os.path.exists(PRIV_KEY) and os.path.exists(PUB_KEY):
print(f"Keypair already exists: {PRIV_KEY}")
return
openssl = shutil.which("openssl") # also finds openssl.exe on Windows
if openssl:
_keygen_openssl(openssl)
else:
try:
_keygen_cryptography()
except ImportError:
sys.exit(
"No openssl found and the 'cryptography' package is missing.\n"
"Pick one of the two:\n"
" - install openssl (Linux: apt install openssl | "
"macOS: brew install openssl | Windows: via Git for Windows or "
"https://slproweb.com/products/Win32OpenSSL.html), or\n"
" - pip install cryptography\n"
"then run this script again."
)
print(f" private key: {PRIV_KEY} (keep SECRET, do not share)")
print(f" public key: {PUB_KEY}")
def _norm(text):
return "".join(text.split())
def public_key_is_live():
"""True if the hosted public key matches the local one."""
with open(PUB_KEY, encoding="utf-8") as fh:
local = _norm(fh.read())
try:
req = urllib.request.Request(WELL_KNOWN, headers={"User-Agent": "curl/8"})
with urllib.request.urlopen(req, timeout=10) as resp:
remote = _norm(resp.read().decode("utf-8", errors="replace"))
except Exception as e:
print(f" Could not fetch {WELL_KNOWN}: {e}")
return False
if remote != local:
print(" File is reachable, but its content does NOT match your local "
"public key. Host exactly com.tesla.3p.public-key.pem.")
return False
return True
def hosting_instructions():
print("\n>>> Action needed: host your public key on your domain <<<")
print(f"Place the file {os.path.basename(PUB_KEY)} so that this URL works"
" exactly:")
print(f" {WELL_KNOWN}")
print("Requirements: https with a valid certificate, NO redirect, and serve"
" the raw .pem (no HTML around it).")
print("Quick check once it's online:")
print(f" curl -sSL {WELL_KNOWN}")
print("Then run this script again to finish the registration.\n")
def partner_token(audience):
data = urllib.parse.urlencode({
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"scope": SCOPE,
"audience": audience,
}).encode("utf-8")
req = urllib.request.Request(
TOKEN_URL, data=data,
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
with urllib.request.urlopen(req, timeout=15) as resp:
return json.load(resp)["access_token"]
def register(base, token):
body = json.dumps({"domain": DOMAIN}).encode("utf-8")
req = urllib.request.Request(
f"{base}/api/1/partner_accounts", data=body,
headers={"Authorization": f"Bearer {token}",
"Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=15) as resp:
return resp.status, resp.read().decode("utf-8", errors="replace")
def main():
print(f"Domain: {DOMAIN}\n")
ensure_keypair()
print(f"\nChecking whether the public key is reachable at:\n {WELL_KNOWN}")
if not public_key_is_live():
hosting_instructions()
sys.exit(0)
print(" OK, hosted key matches.\n")
ok = True
for region, base in REGIONS.items():
print(f"[{region}] fetching partner token and registering ...")
try:
token = partner_token(base)
status, body = register(base, token)
print(f"[{region}] HTTP {status}")
if status != 200:
ok = False
print(f"[{region}] response: {body}")
except urllib.error.HTTPError as e:
ok = False
print(f"[{region}] ERROR HTTP {e.code}: "
f"{e.read().decode(errors='replace')}")
except Exception as e:
ok = False
print(f"[{region}] ERROR: {e}")
print()
if ok:
print("Done: registered in both regions. Restart EVCC or re-add the "
"Tesla -- the 412 should now be gone.")
print("Read-only setup: pairing the key with your car is NOT needed "
"(that's only for signed commands).")
else:
print("Not fully successful -- see the error messages above.")
sys.exit(1)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment