Skip to main content

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/x509 npm 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 };
}
Upload the certificate

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​