Quickstart
Get your first API call working in under 5 minutes.
Prerequisites
- Node.js 18+ installed
- Your client ID from the JLR Developer Portal (there is no client secret)
- The
jose,uuid, and@peculiar/x509npm packages
npm install jose uuid @peculiar/x509
Step 1: Generate a key pair and X.509 certificate
Create one PS256 (RSASSA-PSS) key pair, then build a self-signed X.509 certificate from it. You upload the certificate's PEM to the portal when creating your client; you keep the private key to sign your client assertion and your DPoP proofs.
const x509 = require('@peculiar/x509');
const { webcrypto } = require('node:crypto');
const { importPKCS8, exportJWK } = require('jose');
const crypto = webcrypto;
x509.cryptoProvider.set(crypto);
// PS256 = RSASSA-PSS with SHA-256.
const alg = {
name: 'RSA-PSS',
hash: 'SHA-256',
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
saltLength: 32,
};
async function setup() {
// 1. Key pair
const keys = await crypto.subtle.generateKey(alg, true, ['sign', 'verify']);
// 2. Self-signed X.509 certificate (PEM) - upload this to the portal
const cert = await x509.X509CertificateGenerator.createSelfSigned({
serialNumber: '01',
name: 'CN=YOUR_CLIENT_ID',
notBefore: new Date(),
notAfter: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year
signingAlgorithm: alg,
keys,
extensions: [new x509.BasicConstraintsExtension(false)],
});
const certificatePem = cert.toString('pem');
// 3. Private key as PKCS8 PEM, re-imported for jose signing
const pkcs8 = await crypto.subtle.exportKey('pkcs8', keys.privateKey);
const pkcs8Pem =
'-----BEGIN PRIVATE KEY-----\n' +
Buffer.from(pkcs8).toString('base64').replace(/(.{64})/g, '$1\n') +
'\n-----END PRIVATE KEY-----\n';
const privateKey = await importPKCS8(pkcs8Pem, 'PS256');
// 4. Public key as JWK - used in the DPoP proof header
const publicJwk = await exportJWK(keys.publicKey);
// Store the private key and certificate securely and reuse them.
return { privateKey, publicJwk, certificatePem };
}
Write certificatePem to a file (for example certificate.pem) and upload it on the
public certificate step when you create your client in the portal. Keep the
private key on your backend - it is never uploaded and there is no client secret.
Step 2: Create a DPoP proof helper
const { SignJWT } = require('jose');
const { v4: uuidv4 } = require('uuid');
async function createDPoPProof(privateKey, publicJwk, method, url) {
return await new SignJWT({
jti: uuidv4(),
htm: method,
htu: url,
iat: Math.floor(Date.now() / 1000),
})
.setProtectedHeader({
alg: 'PS256',
typ: 'dpop+jwt',
jwk: publicJwk,
})
.sign(privateKey);
}
Step 3: Obtain an access token
Authentication uses a signed client assertion
(private_key_jwt) - there is no client secret. Build the assertion with the private
key whose public certificate you uploaded when you created your credentials, and
there is no scope parameter (access is set on your client in the portal).
const tokenUrl = 'https://auth.jlr-vcdp.com/realms/public/protocol/openid-connect/token';
async function createClientAssertion(privateKey, clientId) {
const now = Math.floor(Date.now() / 1000);
return await new SignJWT({})
.setProtectedHeader({ alg: 'PS256', typ: 'JWT' })
.setIssuer(clientId)
.setSubject(clientId)
.setAudience(tokenUrl)
.setJti(uuidv4())
.setIssuedAt(now)
.setExpirationTime(now + 300)
.sign(privateKey);
}
async function getToken(privateKey, publicJwk, clientId) {
const dpopProof = await createDPoPProof(privateKey, publicJwk, 'POST', tokenUrl);
const assertion = await createClientAssertion(privateKey, clientId);
const response = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'DPoP': dpopProof,
},
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: clientId,
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
client_assertion: assertion,
}),
});
return await response.json();
}
Step 4: Make an API call
async function getVehicle(privateKey, publicJwk, accessToken, vin) {
const url = `https://data.jlr-vcdp.com/v1/vehicles/${vin}`;
const dpopProof = await createDPoPProof(privateKey, publicJwk, 'GET', url);
const response = await fetch(url, {
headers: {
'Authorization': `DPoP ${accessToken}`,
'DPoP': dpopProof,
},
});
return await response.json();
}
Step 5: Put it all together
async function main() {
const { privateKey, publicJwk } = await setup();
// Get access token
const tokenResponse = await getToken(
privateKey, publicJwk,
'YOUR_CLIENT_ID'
);
console.log('Token type:', tokenResponse.token_type); // "DPoP"
// Fetch vehicle data
const vehicle = await getVehicle(
privateKey, publicJwk,
tokenResponse.access_token,
'SALGA2BN6PH123456'
);
console.log('Vehicle:', vehicle.Vehicle.VehicleIdentification.Model);
}
main().catch(console.error);
What's next
- Error Handling - Handle errors gracefully
- Rate Limiting - Stay within usage limits
- API Reference - Explore all available endpoints