| name | volvo-api-integration |
| description | Guide for integrating the Volvo Kotlin API library into a project. Use this skill when the user wants to add Volvo vehicle API support to their app, set up the Gradle dependency, configure authentication (OAuth2 or test tokens), make API calls to Volvo Connected Vehicle/Energy/Location endpoints, handle errors, or query vehicle data like location, fuel, battery, door locks, or remote commands. Also trigger when the user asks about Volvo API setup, VCC API key, Volvo ID tokens, or how to call any Volvo vehicle endpoint from Kotlin. |
Integrating the Volvo Kotlin API
Setup
1. Add the repository and dependency
The library is published to GitHub Packages, which requires authentication even for public packages.
repositories {
maven {
url = uri("https://maven.pkg.github.com/AYastrebov/Volvo-Kotlin-API")
credentials {
username = providers.gradleProperty("gpr.user").orNull
?: System.getenv("GITHUB_ACTOR")
password = providers.gradleProperty("gpr.token").orNull
?: System.getenv("GITHUB_TOKEN")
}
}
}
Store credentials in ~/.gradle/gradle.properties (never commit these):
gpr.user=your-github-username
gpr.token=ghp_your-personal-access-token
The GitHub token needs read:packages scope. Generate one at https://github.com/settings/tokens.
2. Add the dependency
dependencies {
implementation("com.github.ayastrebov.volvo:volvo-api-client:0.6.0")
}
3. Add a Ktor HTTP engine
The library needs a Ktor engine at runtime. Pick one for your platform:
dependencies {
implementation("io.ktor:ktor-client-okhttp:3.4.3")
implementation("io.ktor:ktor-client-darwin:3.4.3")
implementation("io.ktor:ktor-client-js:3.4.3")
implementation("io.ktor:ktor-client-cio:3.4.3")
}
Without an engine, you get a runtime crash: No Ktor HttpClient engine configured.
Authentication
You need two things from the Volvo Developer Portal:
- VCC API Key — create an application to get one
- OAuth2 credentials — publish your app to receive
client_id and client_secret
Production: OAuth2 with automatic token refresh
This is the recommended approach. The client automatically refreshes expired tokens.
val client = VolvoCars(
VolvoCarsConfig(
apiKey = "your-vcc-api-key",
oauth = OAuthConfig(
accessToken = storedAccessToken,
refreshToken = storedRefreshToken,
clientId = "your-client-id",
clientSecret = "your-client-secret",
onTokensRefreshed = { newAccess, newRefresh ->
tokenStorage.save(newAccess, newRefresh)
}
)
)
)
The initial accessToken and refreshToken come from completing the OAuth2 authorization code flow (redirect user to Volvo ID login, exchange code for tokens). The library handles refresh from there.
Testing: Static token
For quick testing, generate a test token from the Developer Portal:
val client = VolvoCars(apiKey = "your-vcc-api-key", token = "your-test-token")
Test tokens expire and cannot be refreshed. Good for prototyping, not production.
Getting the initial OAuth2 tokens (PKCE)
Volvo enforces PKCE for all new applications. The library provides a helper:
val pkce = Pkce.generate()
val authUrl = "https://volvoid.eu.volvocars.com/as/authorization.oauth2" +
"?response_type=code" +
"&client_id=$clientId" +
"&redirect_uri=${URLEncoder.encode(redirectUri, "UTF-8")}" +
"&scope=${URLEncoder.encode("openid conve:vehicle_relation conve:commands", "UTF-8")}" +
"&code_challenge=${pkce.codeChallenge}" +
"&code_challenge_method=${pkce.codeChallengeMethod}" +
"&state=$randomState"
Making API calls
All API methods are suspending functions — call them from a coroutine scope.
List vehicles
val vehicles = client.getVehicleList()
vehicles.data?.forEach { vehicle ->
println("VIN: ${vehicle.vin}")
}
Vehicle status
val vin = "YV1XZ..."
val location = client.getVehicleLocation(vin)
val coords = location.data?.geometry
println("Lat: ${coords?.latitude}, Lon: ${coords?.longitude}")
val fuel = client.getFuelAmount(vin)
val energy = client.getEnergyState(vin)
val doors = client.getDoorAndLockStatus(vin)
val odo = client.getOdometer(vin)
val tyres = client.getTyreStatus(vin)
val engine = client.getEngineStatus(vin)
Remote commands
client.invokeLock(vin)
client.invokeUnlock(vin)
client.invokeHonk(vin)
client.invokeFlash(vin)
client.invokeHonkFlash(vin)
client.invokeEngineStart(vin, EngineStartRequest(duration = 15))
client.invokeEngineStop(vin)
client.invokeClimatizationStart(vin)
client.invokeClimatizationStop(vin)
Distributed tracing
Include a W3C traceparent header for end-to-end observability:
val trace = Traceparent.generate()
val location = client.getVehicleLocation(
vin = vin,
requestOptions = RequestOptions(
headers = mapOf("traceparent" to trace.value)
)
)
println("Trace ID: ${trace.traceId}")
Error handling
All API errors are typed exceptions. Catch them specifically:
try {
val location = client.getVehicleLocation(vin)
} catch (e: AuthenticationException) {
} catch (e: PermissionException) {
} catch (e: RateLimitException) {
} catch (e: InvalidRequestException) {
} catch (e: VolvoServerException) {
} catch (e: VolvoTimeoutException) {
} catch (e: VolvoException) {
}
Access error details:
catch (e: VolvoAPIException) {
println("HTTP ${e.statusCode}: ${e.error.detail?.message}")
}
Advanced configuration
Retry with backoff
Retries are enabled by default (3 retries on 429/502/503/504 with exponential backoff + jitter). The client also parses Retry-After headers from 429 responses.
VolvoCarsConfig(
apiKey = "...",
token = "...",
retry = RetryStrategy(
maxRetries = 5,
base = 2.0,
maxDelay = 120.seconds,
retryOnStatusCodes = setOf(429, 502, 503, 504)
)
)
Circuit breaker
Stop hitting a failing server:
VolvoCarsConfig(
apiKey = "...",
token = "...",
circuitBreaker = CircuitBreakerConfig(
failureThreshold = 5,
resetTimeout = 30.seconds
)
)
Logging
VolvoCarsConfig(
apiKey = "...",
token = "...",
logging = LoggingConfig(
logLevel = LogLevel.Headers,
logger = HttpLogger { println(it) },
sanitize = true
)
)
Timeouts
VolvoCarsConfig(
apiKey = "...",
token = "...",
timeout = Timeout(
socket = 30.seconds,
connect = 10.seconds,
request = 60.seconds
)
)
Proxy
VolvoCarsConfig(
apiKey = "...",
token = "...",
proxy = ProxyConfig.Http("http://proxy.corp.com:8080")
)
Resource management
Create one client instance and reuse it. Close when done:
val client = VolvoCars(config)
try {
} finally {
client.close()
}
VolvoCars(config).use { client ->
val vehicles = client.getVehicleList()
}
The client is thread-safe — safe to share across coroutines.
Available scopes
Each API endpoint requires specific OAuth2 scopes. Common ones:
| Scope | Access |
|---|
openid | Required for all requests |
conve:vehicle_relation | Vehicle list |
conve:commands | List available commands |
conve:lock | Lock/unlock |
conve:engine_status | Engine status |
conve:doors_status | Door status |
conve:windows_status | Window status |
energy:capability:read | Energy capabilities |
energy:state:read | Energy state |
Request all scopes your app needs during the initial OAuth2 authorization.
API rate limits
- Status endpoints: 100 requests/minute per Volvo ID + client ID combination
- Command endpoints: 10 requests/minute
- Exceeding limits returns HTTP 429 (auto-retried if retry is configured)