Overview
This guide walks through transitioning your existing integration from per-vehicle user tokens to application-level access tokens using API Authentication.
Important: This migration is required to use API v3.0, which only supports application-level (M2M) access tokens. Per-vehicle user tokens are a v2.0 concept and cannot be used with v3.0.
The Vehicles API v2.0 will be deprecated by Q4 of 2026. We recommend migrating to the latest version as soon as possible to ensure continued support and access to new features.
Before You Begin
Ensure you have the following:
- Active Smartcar developer account with dashboard access
- Existing integration using per-vehicle user tokens and Smartcar Connect
- Ability to update your backend database schema
- Development and staging environments ready for testing
- Team familiarity with your current token storage approach
Migration Overview
The transition follows four steps. You control the pace.
- Create API credentials — Generate your Client ID and Secret in the Dashboard
- Capture
user_id from Connect — Store the user_id returned in the Connect redirect URL
- Backfill existing connections — Use the Connections endpoint to retrieve user IDs for existing connections
- Switch to application tokens — Replace per-vehicle user tokens with your application access token +
sc-user-id header
Phase 1: Enable API Authentication (Parallel Operation)
Create API credentials in Dashboard
- Log in to your Smartcar Dashboard
- Navigate to API Credentials in your application settings
- Click Create Secret
- Copy the
client_id and client_secret and store them securely
Store these credentials as environment variables. Never commit them to version control.
Continue using per-vehicle user tokens
Keep your existing token refresh and storage logic unchanged. You’ll phase this out in Phase 3.
Test API Authentication
Exchange your credentials for an access token:curl -X POST https://iam.smartcar.com/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET"
const axios = require('axios');
async function getAppToken() {
const response = await axios.post('https://iam.smartcar.com/oauth2/token', {
grant_type: 'client_credentials',
client_id: process.env.SMARTCAR_CLIENT_ID,
client_secret: process.env.SMARTCAR_CLIENT_SECRET
}, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
return response.data.access_token;
}
getAppToken().then(token => console.log('Access Token:', token));
import os
import requests
def get_app_token():
url = "https://iam.smartcar.com/oauth2/token"
data = {
"grant_type": "client_credentials",
"client_id": os.getenv("SMARTCAR_CLIENT_ID"),
"client_secret": os.getenv("SMARTCAR_CLIENT_SECRET")
}
response = requests.post(url, data=data)
return response.json()["access_token"]
token = get_app_token()
print(f"Access Token: {token}")
Access tokens are valid for 1 hour. Cache them and refresh when expired. Build a token refresh loop in Phase 2.
Run Phase 1 in staging
Deploy your API credentials to your staging environment. Make test API calls using the access token. Verify responses match your per-vehicle user token calls.
Phase 2: Update Integration
Capture userId from Connect redirect
When users complete Smartcar Connect, the redirect now includes a userId parameter:https://example.com/callback?code=CODE&userId=a4c82e9f-...&state=STATE
Update your callback handler to extract and store this userId:app.get('/callback', (req, res) => {
const { code, userId, state } = req.query;
// Verify state token (CSRF protection)
if (state !== req.session.state) {
return res.status(400).send('Invalid state');
}
// Store userId for later
req.session.smartcarUserId = userId;
// Exchange code for per-vehicle user token (Phase 1 & 2)
const tokenData = await exchangeCodeForToken(code);
// Store both for now (Phase 1 & 2)
req.user.smartcarUserId = userId;
req.user.smartcarToken = tokenData.access_token;
await req.user.save();
res.redirect('/dashboard');
});
from flask import request, session
from urllib.parse import parse_qs
@app.route('/callback')
def callback():
code = request.args.get('code')
user_id = request.args.get('userId')
state = request.args.get('state')
# Verify state token (CSRF protection)
if state != session.get('state'):
return 'Invalid state', 400
# Store userId
session['smartcar_user_id'] = user_id
# Exchange code for per-vehicle user token
token_data = exchange_code_for_token(code)
# Store both
current_user.smartcar_user_id = user_id
current_user.smartcar_token = token_data['access_token']
db.session.commit()
return redirect('/dashboard')
Store userId ↔ user ID mappings
Create a new database column to store the Smartcar userId alongside your internal user ID. See the schema changes section below.
Query connections by userId
Start querying the connections endpoint with the userId filter:curl -X GET "https://vehicle.api.smartcar.com/v3/connections?filter[user_id]=a4c82e9f-..." \
-H "Authorization: Bearer {access_token}" \
-H "sc-user-id: a4c82e9f-..."
const axios = require('axios');
async function getUserConnections(appToken, userId) {
const response = await axios.get(
`https://vehicle.api.smartcar.com/v3/connections?filter[user_id]=${userId}`,
{
headers: {
'Authorization': `Bearer ${appToken}`,
'sc-user-id': userId
}
}
);
return response.data;
}
import requests
def get_user_connections(app_token, user_id):
url = f"https://vehicle.api.smartcar.com/v3/connections?filter[user_id]={user_id}"
headers = {
"Authorization": f"Bearer {app_token}",
"sc-user-id": user_id
}
response = requests.get(url, headers=headers)
return response.json()
The sc-user-id header tells Smartcar which user’s connections you’re accessing. Always include it.
Handle webhooks with user.id
Smartcar webhooks now include user.id in the payload. Use this to route events to the correct user:{
"eventId": "b9e4d2a6-5f18-43c7-a0b3-8d1f6e9c7a54",
"eventType": "VEHICLE_STATE",
"data": {
"vehicle": {
"id": "vehicle123",
"make": "Tesla",
"model": "Model S",
"year": 2020,
"vin": "5YJSA1E26FFP12345"
},
"signals": [],
"user": {
"id": "a4c82e9f-..."
}
},
"triggers": [],
"meta": {}
}
Update your webhook handler to extract and use the user ID:app.post('/webhooks/smartcar', (req, res) => {
const { data, eventType } = req.body;
const userId = data.user.id;
const vehicle = data.vehicle;
// Route to correct user
const userRecord = User.findBySmartcarId(userId);
if (eventType === 'VEHICLE_STATE') {
handleVehicleStateChange(userRecord, vehicle);
}
res.status(200).send('OK');
});
@app.route('/webhooks/smartcar', methods=['POST'])
def handle_webhook():
payload = request.json
data = payload['data']
user_id = data['user']['id']
event_type = payload['eventType']
vehicle = data['vehicle']
# Route to correct user
user_record = User.query.filter_by(smartcar_user_id=user_id).first()
if event_type == 'VEHICLE_STATE':
handle_vehicle_state_change(user_record, vehicle)
return 'OK', 200
Phase 3: Full Migration
Switch to API Authentication
Update all API calls to use the access token and sc-user-id header:// Before (per-vehicle user token per vehicle)
async function getVehicleInfo(vehicleId, vehicleToken) {
const response = await axios.get(
`https://api.smartcar.com/v2.0/vehicles/${vehicleId}/info`,
{
headers: {
'Authorization': `Bearer ${vehicleToken}`
}
}
);
return response.data;
}
// After (application access token + userId)
async function getVehicleInfo(vehicleId, appToken, userId) {
const response = await axios.get(
`https://vehicle.api.smartcar.com/v3/vehicles/${vehicleId}`,
{
headers: {
'Authorization': `Bearer ${appToken}`,
'sc-user-id': userId
}
}
);
return response.data;
}
# Before (per-vehicle user token per vehicle)
def get_vehicle_info(vehicle_id, vehicle_token):
headers = {'Authorization': f'Bearer {vehicle_token}'}
response = requests.get(
f'https://api.smartcar.com/v2.0/vehicles/{vehicle_id}/info',
headers=headers
)
return response.json()
# After (application access token + userId)
def get_vehicle_info(vehicle_id, app_token, user_id):
headers = {
'Authorization': f'Bearer {app_token}',
'sc-user-id': user_id
}
response = requests.get(
f'https://vehicle.api.smartcar.com/v3/vehicles/{vehicle_id}',
headers=headers
)
return response.json()
Remove per-vehicle user token refresh logic
Delete the code that refreshes per-vehicle user tokens. Application access tokens are re-requested from the token endpoint when expired.Ensure your access token cache and refresh loop are working before removing per-vehicle user token logic.
Decommission token storage
Once all API calls use API Authentication, remove the vehicle_access_token column from your database. You only need to store the smartcar_user_id per user.
Database Schema Changes
Before (Per-Vehicle User Tokens)
CREATE TABLE users (
id INT PRIMARY KEY,
email VARCHAR(255),
name VARCHAR(255),
created_at TIMESTAMP
);
CREATE TABLE vehicles (
id INT PRIMARY KEY,
user_id INT,
vin VARCHAR(17),
vehicle_access_token VARCHAR(255),
token_created_at TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
After (API Authentication with userId Mapping)
CREATE TABLE users (
id INT PRIMARY KEY,
email VARCHAR(255),
name VARCHAR(255),
smartcar_user_id VARCHAR(36),
created_at TIMESTAMP
);
CREATE TABLE vehicles (
id INT PRIMARY KEY,
user_id INT,
vin VARCHAR(17),
smartcar_vehicle_id VARCHAR(36),
FOREIGN KEY (user_id) REFERENCES users(id)
);
You can keep the smartcar_vehicle_id for reference, but you no longer need token storage.
Code Changes: Before & After
// Old approach: Store and refresh per-vehicle tokens
async function getVehicleData(user) {
const vehicle = await Vehicle.findOne({ userId: user.id });
// Check token expiration
if (isTokenExpired(vehicle.tokenCreatedAt)) {
const newToken = await refreshVehicleToken(vehicle.refreshToken);
vehicle.vehicleAccessToken = newToken;
await vehicle.save();
}
// Make API call with vehicle token
const response = await axios.get(
`https://vehicle.api.smartcar.com/v3/vehicles/${vehicle.id}/signals`,
{
headers: {
'Authorization': `Bearer ${vehicle.vehicleAccessToken}`
}
}
);
return response.data;
}
// New approach: Use application access token + user ID header
async function getVehicleData(user) {
const vehicle = await Vehicle.findOne({ userId: user.id });
const appToken = await getAppToken(); // Cached and refreshed automatically
// Make API call with application access token
const response = await axios.get(
`https://vehicle.api.smartcar.com/v3/vehicles/${vehicle.id}/signals`,
{
headers: {
'Authorization': `Bearer ${appToken}`,
'sc-user-id': user.smartcarUserId
}
}
);
return response.data;
}
Testing Your Migration
Use this checklist to validate each phase:
Phase 1 Validation:
Phase 2 Validation:
Phase 3 Validation:
Rollback Plan
If you encounter issues, rolling back is simple:
-
Phase 1 Rollback: Delete API credentials from the Dashboard. No code changes needed.
-
Phase 2 Rollback: Stop consuming
userId from Connect redirect. Revert webhook handler to previous version. Per-vehicle user tokens still work.
-
Phase 3 Rollback: Restore per-vehicle user token refresh logic from version control. Redeploy previous code. Vehicles remain connected.
Because per-vehicle user tokens continue to work until the Vehicles API v2.0 is deprecated, you can roll back at any phase before then without losing access to vehicles.
What’s Next