seth-freund

volvo-cars-api-skill

Use this skill whenever the user wants to interact with a Volvo car via the Volvo Cars Connected Vehicle API v2 or Energy API v2 — checking vehicle status (battery/fuel level, odometer, location, doors, windows, lock state, tyre pressure, diagnostics, warnings), reading trip and statistics history, checking charging status / target SoC / estimated time to full, or sending remote commands (lock/unlock, start/stop climate, honk/flash, engine start/stop). Triggers on any mention of "my Volvo", a VIN, the Volvo Cars Developer Portal, a `vcc-api-key`, OAuth against `volvoid.eu.volvocars.com`, or general Volvo telematics, charging, or connected-car questions — even if the user does not name the API explicitly.

seth-freund 0 Updated 1w ago

Resources

10
GitHub

Install

npx skillscat add seth-freund/claude-skill-volvo-cars

Install via the SkillsCat registry.

SKILL.md

Volvo Cars API Skill

This skill helps Claude call the Volvo Cars Connected Vehicle API v2. The API exposes vehicle data (status, diagnostics, location, statistics) and remote commands (lock, unlock, climate, engine, honk/flash) for Volvo cars enrolled in the owner's Volvo ID.

When to use this skill

Use it whenever the user wants to do anything with a Volvo vehicle programmatically: read state, send a command, build a small dashboard or script, debug an auth error, look up scopes, or wire the API into something else. The user may not name the API — phrasing like "is my car locked?", "what's the SoC on my XC40?", or "warm up the cabin from my laptop" all qualify.

Do not use this skill for the Volvo Extended Vehicle API (different product, different paths under /extended-vehicle/v1) or for Volvo Energy / Location APIs unless the user explicitly mixes them in — those have their own specs.

Quick mental model

Every request needs two credentials in the headers:

authorization: Bearer <access_token>
vcc-api-key:    <vcc_api_key>
  • VCC API key is issued when the user creates an application on the developer portal (both an unpublished and a published app get one). It doesn't expire on a short cycle; treat it like an API key.
  • Access token comes from one of two places, and which one depends on whether the user's app is published:
    • Unpublished (test mode): the user generates a short-lived test token from the developer portal page for a given API. It lasts ~30 minutes. Important nuance the docs don't make obvious: the token is bound to whichever Volvo ID is signed in to the portal at the time of generation, so if the user is signed in with their own Volvo ID, the token gives access to the VINs bonded to that ID (not just a generic demo car). Commands may still be restricted — confirm empirically. Read endpoints work against real VINs.
    • Published: full OAuth 2.0 Authorization Code with PKCE against volvoid.eu.volvocars.com. The token lasts ~30 min and the bundled client auto-refreshes it. Publishing requires Volvo review (~3 weeks).

The bundled scripts/auth.py supports both: python -m scripts.auth test-token for the paste-a-token flow, python -m scripts.auth login for the full OAuth flow.

Base URL for v2: https://api.volvocars.com/connected-vehicle/v2

The first call is usually GET /vehicles — it returns the list of VINs the authenticated identity has access to (one demo VIN in test mode, the user's bonded VINs in published mode). After that, all per-car endpoints are scoped by VIN: GET /vehicles/{vin}/....

How to actually use it

  1. Check the user has credentials. Minimum is a VCC_API_KEY — that's what an unpublished app gets. If the app is published, they also need VOLVO_CLIENT_ID, VOLVO_CLIENT_SECRET, and a registered redirect URI. If they don't have any of this, point them at references/setup.mddo not try to fake credentials or invent endpoints.

  2. Pick the right scopes. Each endpoint requires specific OAuth scopes (e.g. conve:fuel_status, conve:lock). When asking the user to authorize, request only what the task needs — but err toward including a few extra related scopes since reauthorizing later is annoying. Full list in references/scopes.md.

  3. Use the bundled client. scripts/volvo_client.py is a thin Python wrapper around requests that handles the two required headers and auto-refreshes the access token. Prefer it over writing raw HTTP from scratch — it already has the right error handling for the API's quirks (e.g. 401 → refresh and retry once, 422 on commands when the car is offline).

  4. Use the CLI for one-off tasks. scripts/cli.py exposes the common operations as commands (vehicles, status, lock, unlock, climate-start, etc.). If the user just wants to check something quickly, run the CLI rather than writing a new script.

  5. Look up endpoints in the reference. references/endpoints.md is the authoritative catalog of paths, methods, required scopes, and example responses. Read it whenever you're about to call something you haven't called recently in this session — the spec changes occasionally and the catalog is what's been verified.

Auth: the part that bites people

Test-token mode (unpublished apps)

Volvo's portal issues test tokens scoped per API — a token generated from a Connected Vehicle page only carries conve:* scopes, and a token from an Energy page only carries energy:* scopes. The skill stores them separately:

  • ~/.volvo/tokens.cv.json — Connected Vehicle token
  • ~/.volvo/tokens.energy.json — Energy API token

To populate them:

# CV token: generate on a Connected Vehicle endpoint page, then:
python -m scripts.auth test-token --api cv

# Energy token: generate on an Energy endpoint page (e.g. /state), then:
python -m scripts.auth test-token --api energy

Each is independent — only get the ones you'll use. Tokens last ~30 minutes; rerun the corresponding command when they expire. The client picks the right token automatically based on which API it's calling, so a single status invocation can read both CV and Energy data in one go (assuming both tokens are fresh).

Test tokens are bound to whichever Volvo ID generated them. If the user was signed in to the portal with their own Volvo ID, reads AND writes against their real VINs work — confirmed empirically. Unsupported commands return 404 with "description": "<cmd> is not supported by this vehicle", not 403. So before assuming a command will work, call GET /vehicles/{vin}/commands to see what the car actually exposes.

Backward compatibility: older installs with a single ~/.volvo/tokens.json continue to work — it's treated as the CV token if tokens.cv.json doesn't exist.

OAuth mode (published apps)

The Volvo OAuth flow is browser-based (PKCE). The bundled scripts/auth.py will:

  1. Generate a PKCE code verifier + challenge.
  2. Open the user's browser to the consent page on volvoid.eu.volvocars.com.
  3. Spin up a tiny local HTTP server to catch the redirect with the authorization code.
  4. Exchange the code for access_token + refresh_token and stash them in ~/.volvo/tokens.json (mode 0600).

Once tokens exist, every subsequent CLI/script call uses them. If access_token is expired, the client refreshes silently.

Common OAuth pitfalls:

  • The redirect URI in the request must exactly match what was registered with the app on the developer portal — including trailing slash. If you see invalid_redirect_uri, this is why.
  • Scopes are space-separated in the auth URL. Including a scope the app isn't approved for produces invalid_scope.
  • vcc-api-key is required even on the token-refresh endpoint in some flows — when in doubt, send it.

State freshness gotcha

Status endpoints return whatever the car last reported. If the car has been parked and asleep, that data can be many minutes — sometimes hours — stale. The timestamp field on each value envelope tells you when. To force fresh state, send any command (e.g., command-accessibility poll, or a no-op-ish read that wakes the car); a side effect is that all subsequent status reads return a fresh timestamp. This was confirmed empirically — a lock command bumped all door timestamps forward by ~55 minutes in one go.

Commands are synchronous

Each command call holds the connection until the car has reported back, then returns {vin, invokeStatus, message}. invokeStatus: COMPLETED means the car ack'd; other values (DELIVERY_TIMEOUT, REJECTED, etc.) indicate failure — surface the message to the user. There is no separate polling step; the wait happens inside the request.

Common operations cheat sheet

from scripts.volvo_client import VolvoClient

client = VolvoClient()  # reads tokens + key from env / ~/.volvo/

# Discovery
vins = client.list_vehicles()           # → [{"vin": "...", ...}, ...]
info = client.get_vehicle(vin)          # model, year, color, image URLs

# Status snapshots
fuel  = client.get_fuel(vin)            # fuel level + battery SoC if EV/PHEV
odo   = client.get_odometer(vin)
doors = client.get_doors(vin)           # per-door open/closed + lock state
tyres = client.get_tyres(vin)
diag  = client.get_diagnostics(vin)     # service warnings, oil, washer fluid
warns = client.get_warnings(vin)

# Commands (all are POSTs; return an invocation id you can poll)
client.lock(vin)
client.unlock(vin)
client.climate_start(vin)
client.honk_flash(vin)

Refer to references/endpoints.md for the full surface.

Units

Volvo returns metric (km, L, km/h, L/100km, kWh/100km, °C). If the user prefers US-imperial, set VOLVO_UNITS=imperial in their environment (or pass units="imperial" to VolvoClient). The client then converts every response on the way back:

Metric Imperial
km mi
km/h mph
L US gal
L/100km mpg
kWh/100km mi/kWh
°C °F

Conversion happens on the {value, unit, timestamp} envelope — the value is updated and the unit string is replaced. Pure enum states (LOCKED, NO_WARNING, etc.) are never touched.

Handling errors

The API uses standard HTTP status codes plus a JSON error envelope:

{ "error": { "message": "...", "description": "...", "statusCode": 422 } }

Notable behaviors worth remembering:

  • 401: token expired or invalid. The client refreshes and retries once; if it still 401s, the refresh token is likely revoked — re-run auth.py.
  • 403: the requested scope was not granted, or the VIN isn't on this Volvo ID. Check references/scopes.md.
  • 404: VIN not found. List vehicles first to confirm.
  • 422: command rejected (often the car is offline, in deep sleep, or unsupported by the model). Surface the description to the user — it's usually meaningful.
  • 429: rate limited. The client backs off with the Retry-After header.

When you hit an error you don't recognize, dump the full JSON body to the user rather than swallowing it. The descriptions are useful.

Adding a new endpoint

If the user asks for something not in the wrapper, add it to scripts/volvo_client.py and to references/endpoints.md. The wrapper pattern is:

def get_brakes(self, vin: str) -> dict:
    return self._get(f"/vehicles/{vin}/brakes")

Then verify against the live spec page (https://developer.volvocars.com/apis/connected-vehicle/v2/endpoints/<category>/) before committing — Volvo occasionally renames fields between minor revs.

Reference files in this skill

  • references/endpoints.md — full endpoint catalog with paths, methods, scopes, example responses.
  • references/scopes.md — every OAuth scope, what it grants, and which endpoints require it.
  • references/setup.md — step-by-step app registration on the developer portal, with screenshots descriptions.
  • examples/get_vehicle_status.py — end-to-end example: auth → list vehicles → print a status report.

Read these when the SKILL.md doesn't have the specific detail you need.

Categories