Developer Documentation

VINdecode API 3.0 Documentation

Decode vehicle identification numbers into structured vehicle data for inventory, valuation, compliance, and customer-facing applications.

API access uses API Consumers and JWT bearer tokens. Create credentials in your dashboard, keep Client Secrets on your server, and request an audience-specific token before calling this API. See the API access and authentication guidance.

Our VINdecode API provides detailed vehicle specifications based on the submitted vehicle identification number (VIN). These specifications include the year, make, model, engine and transmission details, color, and both standard and optional equipment. This API method supports the submission of one VIN at a time and returns comprehensive specifications. The returned data includes the vehicle's country of origin, year/make/model details, engine and powertrain specifics, manufacturer-included options, and other specifications such as gross vehicle weight (GVW) and MSRP.


Our records show that some users with unlimited access to the VINdecode API tend to make excessive and sometimes unnecessary API calls. To maintain the overall health of our system, we may block traffic from a specific source once it reaches a certain level. This ensures that one user's actions do not negatively impact the larger community. We reserve the right to throttle excessive API calls or even refuse service if necessary.

REST API Description

Our VINdecode API provides programmatic access to our VIN decode database.

Parameter Name Parameter Value Used in Request
JWT Bearer Token * Authorization header * Bearer <jwt-token> Authorization: Bearer <jwt-token>
Vehicle Identification Number * VIN * XXXXXXXXXXXXXXXXX GET https://ws.vinquery.com/vindecode/v3?VIN=XXXXXXXXXXXXXXXXX&reportType=3
Data Type * reportType * 0, 1, 2, 3 GET https://ws.vinquery.com/vindecode/v3?VIN=XXXXXXXXXXXXXXXXX&reportType=3
Partial VIN ** partialVIN ** TRUE or FALSE (default: FALSE) GET https://ws.vinquery.com/vindecode/v3?VIN=XXXXXXXXXXXXXXXXX&reportType=3&partialVIN=TRUE
Data Format format XML or JSON (default: XML) GET https://ws.vinquery.com/vindecode/v3?VIN=XXXXXXXXXXXXXXXXX&reportType=3&partialVIN=TRUE&format=JSON
* The Authorization header, VIN, and reportType are mandatory.
Request a JWT first from POST https://identity.vinquery.com/connect/token using an API Consumer with audience vinquery:api:vindecode.
** Set the optional parameter "partialVIN" to "TRUE" if you'd like our system NOT to validate the VIN. The system will NOT do checksum test before submitting the VIN to our decoding engine for processing. Set the value of this parameter to "FALSE" otherwise.

Data Types

Data Code Data Type Data Format
0 Basic Data ws_vindecode_bas.xml  |   ws_vindecode_bas.json
1 Standard Data ws_vindecode_std.xml  |   ws_vindecode_std.json
2 Extended Data ws_vindecode_ext.xml  |   ws_vindecode_ext.json
3 Lite Data ws_vindecode_lit.xml  |   ws_vindecode_lit.json

Lookup Tables

These tables are static. They don't change often, if at all.

Body Style Show the complete list of all possible body styles used in our system.
COUPE 2-DR CREW CAB PICKUP 4-DR CROSSOVER 4-DR EXTENDED CAB PICKUP 4-DR HATCHBACK 2-DR HATCHBACK 4-DR PASSENGER VAN REGULAR CAB PICKUP 2-DR SEDAN 4-DR SPORT UTILITY 4-DR SPORTS VAN WAGON 4-DR WAGON 5-DR
Vehice Type Show the complete list of all possible vehicle types used in our system.
BUS MOTORCYCLE MPV PASSENGER RECREATIONAL TRAILER VAN LOW SPEED VEHICLE ELECTRIC VEHICLE DUMP TRUCK TRACTOR TRUCK SNOWMOBILE ATV
Fuel Type
CNG DIESEL ELECTRIC FFV GAS HYBRID PLUG-IN HYBRID HYDROGEN LPG No data
Engine Shape
H12 H4 H6 L2 L3 L4 L5 L6 No data NO ENGINE ROTARY V10 V12 V20 V4 V6 V8 VL W12 W16 W8
Driveline
10X4 10X6 12X6 4WD 4X2 4X4 6X2 6X4 8X4 8X6 AWD FWD FWD | AWD RWD RWD | AWD 2WD No data

Error Codes

Error Code (Key) Description (Value)
0 Database Errors.
2 Valid VIN number. However, no data available for it at this moment.
3 Invalid VIN number: This VIN number did not pass checksum test
4 Invalid VIN number: A valid VIN number must be exactly 17 digits
5 Invalid VIN number: This VIN number contains invalid letters: I,O or Q.
6 Invalid report type. A valid report type should be 0 for BASIC, 1 for STANDARD, 2 for EXTENDED or 3 for LITE.
8 Service access denied: check that the bearer token is valid, unexpired, and issued for the VINdecode audience.
9 Report type missing: A report type (0,1,2 or 3) must be specified.
10 Insufficient balance for Basic Reports.
11 Insufficient balance for Standard Reports.
12 Insufficient balance for Extended Reports.
13 VIN missing: A VIN must be submitted as part of the querystring.
14 Invalid VIN number: The 10th digit of a VIN number cannot be letter U, letter Z or number 0.
15 Insufficient balance for Lite Reports.
16 A VIN can only contain alphanumeric characters.
XML

A sample error message returned in XML format:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<VINdecode version="3.0" report_type="EXTENDED" Date="8/27/2022">
  <VIN number="1FTRF02W24KXXXXXX" status="FAILED">
    <Message Key="3" Value="Invalid VIN number: This VIN number did not pass checksum test." />
  </VIN>
</VINdecode>
        
JSON

A sample error message returned in JSON format:

{
  "service": "vindecode",
  "version": "3.0",
  "date": "8/27/2022",
  "vin": "1FTRF02W24KXXXXXX",
  "status": "FAILED",
  "message_key": "3",
  "message": "Invalid VIN number: This VIN number did not pass the checksum test."
}        

Known Issues

  • A single VIN returns data related to multiple trim levels.
  • Occasionally, information coded by the manufacturer in a submitted VIN may not be sufficient for our VIN decode engine to narrow it down to a single trim level so all available records for that particular VIN will be returned. In this case, all available trim levels may have to be presented to end users so they can make a selection from those returned choices.

  • A single trim level returns multiple transmissions, exterior colors, interior trims, ABS(Non-ABS |* 2-Wheel ABS |* 4-Wheel ABS) etc.
  • For instance, a decoded VIN may offer all exterior color choices that were available for a particular vehicle, but can't tell you what color the vehicle actually is - therefore all available color choices for the vehicle might have to be presented to end users so they can select the color or have the option to enter another color choice in case the vehicle was painted over.

    * "|" is used as a delimiters or separator when a data item contains multiple choices.

Coding Examples (JWT Authentication)

These server-side examples keep API Consumer credentials private, cache and reuse a valid VINdecode JWT, renew shortly before expiration, and perform one refresh-and-retry after an unexpected 401. The implementations are copied from the Server-Side Proxy Guide.

JavaScript
import express from "express";

const app = express();

// Store API Consumer credentials only in trusted server-side configuration.
// These values must never be shipped to browser JavaScript, mobile apps, or public source code.
const clientId = process.env.VINQUERY_VINDECODE_CLIENT_ID;
const clientSecret = process.env.VINQUERY_VINDECODE_CLIENT_SECRET;
const audience = "vinquery:api:vindecode";
const identityTokenUrl = "https://identity.vinquery.com/connect/token";
const vindecodeUrl = "https://ws.vinquery.com/vindecode/v3";

// Renew shortly before expiry so a user request is not made with a nearly expired token.
const refreshBufferMs = 60 * 1000;

// This in-memory cache is process-local. In a multi-instance deployment, each instance
// should maintain its own token cache rather than asking Identity for every API call.
let cachedToken = null;

// A shared promise prevents simultaneous requests from all refreshing the token at once.
let refreshInFlight = null;

function tokenIsUsable() {
  return cachedToken && Date.now() < cachedToken.expiresAtMs - refreshBufferMs;
}

async function getJwtToken() {
  // Reuse the cached JWT when it is still comfortably valid.
  if (tokenIsUsable()) {
    return cachedToken.jwtToken;
  }

  // If another request is already refreshing, wait for the same refresh operation.
  if (refreshInFlight) {
    return refreshInFlight;
  }

  refreshInFlight = (async () => {
    if (!clientId || !clientSecret) {
      throw new Error("VINquery API Consumer credentials are not configured.");
    }

    const response = await fetch(identityTokenUrl, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ clientId, clientSecret, audience })
    });

    if (!response.ok) {
      throw new Error(`Identity token request failed with HTTP ${response.status}.`);
    }

    // Current Identity contract returns jwtToken, expiresIn, and expiresUtc.
    const token = await response.json();
    if (!token.jwtToken || !token.expiresUtc) {
      throw new Error("Identity response did not include jwtToken and expiresUtc.");
    }

    cachedToken = {
      jwtToken: token.jwtToken,
      expiresAtMs: Date.parse(token.expiresUtc)
    };

    return cachedToken.jwtToken;
  })().finally(() => {
    refreshInFlight = null;
  });

  return refreshInFlight;
}

function invalidateRejectedToken(rejectedToken) {
  // Do not erase a newer token that another request may already have refreshed.
  if (cachedToken?.jwtToken === rejectedToken) {
    cachedToken = null;
  }
}

app.get("/api/vindecode", async (req, res) => {
  try {
    // Validate client input before calling VINquery.
    const vin = String(req.query.vin || "").trim();
    if (!/^[A-HJ-NPR-Z0-9]{17}$/i.test(vin)) {
      return res.status(400).json({ message: "A valid 17-character VIN is required." });
    }

    // The browser calls this proxy. The proxy adds the bearer token server-side.
    let jwtToken = await getJwtToken();
    const apiUrl = `${vindecodeUrl}?VIN=${encodeURIComponent(vin)}&reportType=3&format=JSON`;
    let apiResponse = await fetch(apiUrl, {
      headers: { Authorization: `Bearer ${jwtToken}` }
    });

    // A token can be revoked before expiresUtc. Invalidate and retry exactly once.
    if (apiResponse.status === 401) {
      invalidateRejectedToken(jwtToken);
      jwtToken = await getJwtToken();
      apiResponse = await fetch(apiUrl, {
        headers: { Authorization: `Bearer ${jwtToken}` }
      });
    }

    res.status(apiResponse.status).type("application/json").send(await apiResponse.text());
  } catch (error) {
    // Return a safe error. Log full details server-side, not in the browser response.
    console.error("VINdecode proxy failed", error);
    res.status(502).json({ message: "VINquery request could not be completed." });
  }
});
TypeScript
import express, { Request, Response } from "express";

const app = express();

type TokenResponse = {
  jwtToken: string;
  expiresIn: number;
  expiresUtc: string;
};

type CachedToken = {
  jwtToken: string;
  expiresAtMs: number;
};

// Credentials and audience are server-side configuration. Never expose them to the client.
const clientId = process.env.VINQUERY_VINDECODE_CLIENT_ID || "";
const clientSecret = process.env.VINQUERY_VINDECODE_CLIENT_SECRET || "";
const audience = "vinquery:api:vindecode";
const refreshBufferMs = 60_000;
let cachedToken: CachedToken | null = null;
let refreshInFlight: Promise<string> | null = null;

function cachedTokenIsValid(): boolean {
  return !!cachedToken && Date.now() < cachedToken.expiresAtMs - refreshBufferMs;
}

async function requestFreshToken(): Promise<string> {
  if (!clientId || !clientSecret) {
    throw new Error("VINquery API Consumer credentials are missing.");
  }

  const response = await fetch("https://identity.vinquery.com/connect/token", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ clientId, clientSecret, audience })
  });

  if (!response.ok) {
    throw new Error(`Identity token request failed with HTTP ${response.status}.`);
  }

  const token = (await response.json()) as TokenResponse;
  if (!token.jwtToken || !token.expiresUtc) {
    throw new Error("Identity response did not include jwtToken and expiresUtc.");
  }

  cachedToken = {
    jwtToken: token.jwtToken,
    expiresAtMs: Date.parse(token.expiresUtc)
  };

  return cachedToken.jwtToken;
}

async function getJwtToken(): Promise<string> {
  // Use the cached token until it is close to expiration.
  if (cachedTokenIsValid()) {
    return cachedToken!.jwtToken;
  }

  // Share one refresh promise across concurrent requests.
  refreshInFlight ??= requestFreshToken().finally(() => {
    refreshInFlight = null;
  });

  return refreshInFlight;
}

function invalidateRejectedToken(rejectedToken: string): void {
  // Preserve a newer token installed concurrently by another request.
  if (cachedToken?.jwtToken === rejectedToken) {
    cachedToken = null;
  }
}

app.get("/api/vindecode", async (req: Request, res: Response) => {
  try {
    const vin = String(req.query.vin || "").trim();
    if (!/^[A-HJ-NPR-Z0-9]{17}$/i.test(vin)) {
      return res.status(400).json({ message: "A valid 17-character VIN is required." });
    }

    let jwtToken = await getJwtToken();
    const apiUrl = `https://ws.vinquery.com/vindecode/v3?VIN=${encodeURIComponent(vin)}&reportType=3&format=JSON`;
    let apiResponse = await fetch(apiUrl, {
      headers: { Authorization: `Bearer ${jwtToken}` }
    });

    if (apiResponse.status === 401) {
      invalidateRejectedToken(jwtToken);
      jwtToken = await getJwtToken();
      apiResponse = await fetch(apiUrl, {
        headers: { Authorization: `Bearer ${jwtToken}` }
      });
    }

    return res.status(apiResponse.status).type("application/json").send(await apiResponse.text());
  } catch (error) {
    console.error("VINdecode proxy failed", error);
    return res.status(502).json({ message: "VINquery request could not be completed." });
  }
});
Python
from datetime import datetime, timezone, timedelta
import os
import requests
from threading import Lock
from fastapi import FastAPI, HTTPException, Response

app = FastAPI()

# API Consumer credentials stay in trusted server-side configuration.
CLIENT_ID = os.environ.get("VINQUERY_VINDECODE_CLIENT_ID", "")
CLIENT_SECRET = os.environ.get("VINQUERY_VINDECODE_CLIENT_SECRET", "")
AUDIENCE = "vinquery:api:vindecode"
IDENTITY_TOKEN_URL = "https://identity.vinquery.com/connect/token"
VINDECODE_URL = "https://ws.vinquery.com/vindecode/v3"
REFRESH_BUFFER = timedelta(seconds=60)

_cached_token = None
_token_expires_at = datetime.min.replace(tzinfo=timezone.utc)
_refresh_lock = Lock()

def _token_is_usable() -> bool:
    # Refresh before the exact expiry time so normal traffic does not hit an expired token.
    return bool(_cached_token) and datetime.now(timezone.utc) < (_token_expires_at - REFRESH_BUFFER)

def get_jwt_token() -> str:
    global _cached_token, _token_expires_at

    if _token_is_usable():
        return _cached_token

    # The lock prevents concurrent requests from creating duplicate token refreshes.
    with _refresh_lock:
        if _token_is_usable():
            return _cached_token

        if not CLIENT_ID or not CLIENT_SECRET:
            raise RuntimeError("VINquery API Consumer credentials are missing.")

        token_response = requests.post(
            IDENTITY_TOKEN_URL,
            json={"clientId": CLIENT_ID, "clientSecret": CLIENT_SECRET, "audience": AUDIENCE},
            timeout=15
        )
        token_response.raise_for_status()

        # Current Identity response contract: jwtToken, expiresIn, expiresUtc.
        token_json = token_response.json()
        if not token_json.get("jwtToken") or not token_json.get("expiresUtc"):
            raise RuntimeError("Identity response did not include jwtToken and expiresUtc.")

        _cached_token = token_json["jwtToken"]
        _token_expires_at = datetime.fromisoformat(token_json["expiresUtc"].replace("Z", "+00:00"))
        return _cached_token

def invalidate_rejected_token(rejected_token: str) -> None:
    global _cached_token, _token_expires_at
    # Compare under the same lock so a concurrent refresh is never discarded.
    with _refresh_lock:
        if _cached_token == rejected_token:
            _cached_token = None
            _token_expires_at = datetime.min.replace(tzinfo=timezone.utc)

@app.get("/api/vindecode")
def vindecode(vin: str):
    try:
        if len(vin.strip()) != 17:
            raise HTTPException(status_code=400, detail="A valid 17-character VIN is required.")

        jwt_token = get_jwt_token()
        api_response = requests.get(
            VINDECODE_URL,
            params={"VIN": vin, "reportType": 3, "format": "JSON"},
            headers={"Authorization": f"Bearer {jwt_token}"},
            timeout=30
        )

        if api_response.status_code == 401:
            api_response.close()
            invalidate_rejected_token(jwt_token)
            jwt_token = get_jwt_token()
            api_response = requests.get(
                VINDECODE_URL,
                params={"VIN": vin, "reportType": 3, "format": "JSON"},
                headers={"Authorization": f"Bearer {jwt_token}"},
                timeout=30
            )

        return Response(
            content=api_response.text,
            status_code=api_response.status_code,
            media_type="application/json"
        )
    except HTTPException:
        raise
    except Exception as exc:
        # Log the detailed exception server-side and return a safe message to the client.
        print(f"VINdecode proxy failed: {exc}")
        raise HTTPException(status_code=502, detail="VINquery request could not be completed.")
PHP
<?php
// This example uses APCu for process-local in-memory caching and a lock file to
// prevent duplicate token refreshes. Keep credentials in server environment variables.
$clientId = getenv("VINQUERY_VINDECODE_CLIENT_ID");
$clientSecret = getenv("VINQUERY_VINDECODE_CLIENT_SECRET");
$audience = "vinquery:api:vindecode";
$identityTokenUrl = "https://identity.vinquery.com/connect/token";
$vindecodeUrl = "https://ws.vinquery.com/vindecode/v3";
$refreshBufferSeconds = 60;

function getJwtToken() {
    global $clientId, $clientSecret, $audience, $identityTokenUrl, $refreshBufferSeconds;

    if (!function_exists("apcu_fetch") || !function_exists("apcu_store")) {
        throw new RuntimeException("APCu must be enabled for this in-memory token cache example.");
    }

    $cached = apcu_fetch("vinquery_vindecode_jwt");
    if ($cached && time() < ($cached["expiresAt"] - $refreshBufferSeconds)) {
        return $cached["jwtToken"];
    }

    $lock = fopen(sys_get_temp_dir() . "/vinquery-vindecode-token.lock", "c");
    if (!$lock || !flock($lock, LOCK_EX)) {
        throw new RuntimeException("Could not acquire token refresh lock.");
    }

    try {
        // Re-check inside the lock because another request may have refreshed the token.
        $cached = apcu_fetch("vinquery_vindecode_jwt");
        if ($cached && time() < ($cached["expiresAt"] - $refreshBufferSeconds)) {
            return $cached["jwtToken"];
        }

        if (!$clientId || !$clientSecret) {
            throw new RuntimeException("VINquery API Consumer credentials are missing.");
        }

        $tokenPayload = json_encode([
            "clientId" => $clientId,
            "clientSecret" => $clientSecret,
            "audience" => $audience
        ]);

        $curl = curl_init($identityTokenUrl);
        curl_setopt_array($curl, [
            CURLOPT_POST => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
            CURLOPT_POSTFIELDS => $tokenPayload,
            CURLOPT_TIMEOUT => 15
        ]);

        $body = curl_exec($curl);
        $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
        curl_close($curl);

        if ($status < 200 || $status >= 300) {
            throw new RuntimeException("Identity token request failed with HTTP " . $status . ".");
        }

        // Current Identity response contract: jwtToken, expiresIn, expiresUtc.
        $token = json_decode($body, true);
        if (empty($token["jwtToken"]) || empty($token["expiresUtc"])) {
            throw new RuntimeException("Identity response did not include jwtToken and expiresUtc.");
        }

        $expiresAt = strtotime($token["expiresUtc"]);
        apcu_store("vinquery_vindecode_jwt", [
            "jwtToken" => $token["jwtToken"],
            "expiresAt" => $expiresAt
        ], max(1, $expiresAt - time()));

        return $token["jwtToken"];
    } finally {
        flock($lock, LOCK_UN);
        fclose($lock);
    }
}

function invalidateRejectedToken(string $rejectedToken): void {
    $lock = fopen(sys_get_temp_dir() . "/vinquery-vindecode-token.lock", "c");
    if (!$lock || !flock($lock, LOCK_EX)) {
        throw new RuntimeException("Could not acquire token refresh lock.");
    }
    try {
        $cached = apcu_fetch("vinquery_vindecode_jwt");
        // Delete only the rejected token while holding the same lock used by refresh.
        if ($cached && hash_equals($cached["jwtToken"], $rejectedToken)) {
            apcu_delete("vinquery_vindecode_jwt");
        }
    } finally {
        flock($lock, LOCK_UN);
        fclose($lock);
    }
}

function callVinDecode(string $vindecodeUrl, string $vin, string $jwtToken): array {
    $curl = curl_init($vindecodeUrl . "?VIN=" . urlencode($vin) . "&reportType=3&format=JSON");
    curl_setopt_array($curl, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => ["Authorization: Bearer " . $jwtToken],
        CURLOPT_TIMEOUT => 30
    ]);
    $body = curl_exec($curl);
    $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
    curl_close($curl);
    return [$status, $body];
}

try {
    $vin = strtoupper(trim($_GET["vin"] ?? ""));
    if (!preg_match("/^[A-HJ-NPR-Z0-9]{17}$/", $vin)) {
        http_response_code(400);
        header("Content-Type: application/json");
        echo json_encode(["message" => "A valid 17-character VIN is required."]);
        exit;
    }

    $jwtToken = getJwtToken();
    [$apiStatus, $apiBody] = callVinDecode($vindecodeUrl, $vin, $jwtToken);

    if ($apiStatus === 401) {
        invalidateRejectedToken($jwtToken);
        $jwtToken = getJwtToken();
        [$apiStatus, $apiBody] = callVinDecode($vindecodeUrl, $vin, $jwtToken);
    }

    http_response_code($apiStatus);
    header("Content-Type: application/json");
    echo $apiBody;
} catch (Throwable $ex) {
    error_log("VINdecode proxy failed: " . $ex->getMessage());
    http_response_code(502);
    header("Content-Type: application/json");
    echo json_encode(["message" => "VINquery request could not be completed."]);
}
?>
Go
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
    "sync"
    "time"
)

const (
    audience         = "vinquery:api:vindecode"
    identityTokenURL = "https://identity.vinquery.com/connect/token"
    vindecodeURL     = "https://ws.vinquery.com/vindecode/v3"
    refreshBuffer    = time.Minute
)

type tokenResponse struct {
    JwtToken   string    `json:"jwtToken"`
    ExpiresIn  int       `json:"expiresIn"`
    ExpiresUtc time.Time `json:"expiresUtc"`
}

type tokenCache struct {
    sync.Mutex
    jwtToken  string
    expiresAt time.Time
}

var cache tokenCache

func getJWTToken() (string, error) {
    cache.Lock()
    defer cache.Unlock()

    // Reuse the JWT until it is close to expiration.
    if cache.jwtToken != "" && time.Now().UTC().Before(cache.expiresAt.Add(-refreshBuffer)) {
        return cache.jwtToken, nil
    }

    clientID := os.Getenv("VINQUERY_VINDECODE_CLIENT_ID")
    clientSecret := os.Getenv("VINQUERY_VINDECODE_CLIENT_SECRET")
    if clientID == "" || clientSecret == "" {
        return "", fmt.Errorf("VINquery API Consumer credentials are missing")
    }

    payload, _ := json.Marshal(map[string]string{
        "clientId":     clientID,
        "clientSecret": clientSecret,
        "audience":     audience,
    })

    req, _ := http.NewRequest("POST", identityTokenURL, bytes.NewReader(payload))
    req.Header.Set("Content-Type", "application/json")

    response, err := http.DefaultClient.Do(req)
    if err != nil {
        return "", err
    }
    defer response.Body.Close()

    if response.StatusCode < 200 || response.StatusCode >= 300 {
        return "", fmt.Errorf("Identity token request failed with HTTP %d", response.StatusCode)
    }

    var token tokenResponse
    if err := json.NewDecoder(response.Body).Decode(&token); err != nil {
        return "", err
    }
    if token.JwtToken == "" || token.ExpiresUtc.IsZero() {
        return "", fmt.Errorf("Identity response did not include jwtToken and expiresUtc")
    }

    cache.jwtToken = token.JwtToken
    cache.expiresAt = token.ExpiresUtc
    return cache.jwtToken, nil
}

func invalidateRejectedToken(rejectedToken string) {
    cache.Lock()
    defer cache.Unlock()
    // Keep a newer token if another request refreshed while this call was running.
    if cache.jwtToken == rejectedToken {
        cache.jwtToken = ""
        cache.expiresAt = time.Time{}
    }
}

func vinDecodeHandler(w http.ResponseWriter, r *http.Request) {
    vin := r.URL.Query().Get("vin")
    if len(vin) != 17 {
        http.Error(w, "A valid 17-character VIN is required.", http.StatusBadRequest)
        return
    }

    jwtToken, err := getJWTToken()
    if err != nil {
        fmt.Println("VINdecode token failure:", err)
        http.Error(w, "VINquery request could not be completed.", http.StatusBadGateway)
        return
    }

    apiURL := vindecodeURL + "?VIN=" + url.QueryEscape(vin) + "&reportType=3&format=JSON"
    req, _ := http.NewRequest("GET", apiURL, nil)
    req.Header.Set("Authorization", "Bearer "+jwtToken)

    apiResponse, err := http.DefaultClient.Do(req)
    if err != nil {
        fmt.Println("VINdecode API failure:", err)
        http.Error(w, "VINquery request could not be completed.", http.StatusBadGateway)
        return
    }

    if apiResponse.StatusCode == http.StatusUnauthorized {
        io.Copy(io.Discard, apiResponse.Body)
        apiResponse.Body.Close()
        invalidateRejectedToken(jwtToken)

        jwtToken, err = getJWTToken()
        if err != nil {
            http.Error(w, "VINquery request could not be completed.", http.StatusBadGateway)
            return
        }
        retryRequest, _ := http.NewRequest("GET", apiURL, nil)
        retryRequest.Header.Set("Authorization", "Bearer "+jwtToken)
        apiResponse, err = http.DefaultClient.Do(retryRequest)
        if err != nil {
            http.Error(w, "VINquery request could not be completed.", http.StatusBadGateway)
            return
        }
    }
    defer apiResponse.Body.Close()

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(apiResponse.StatusCode)
    io.Copy(w, apiResponse.Body)
}
C#
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json.Serialization;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient();

// Register one token provider for the application process. It owns the in-memory
// JWT cache and prevents every request from calling the Identity service.
builder.Services.AddSingleton<VinqueryTokenProvider>();

var app = builder.Build();

app.MapGet("/api/vindecode", async (
    string vin,
    IHttpClientFactory httpClientFactory,
    VinqueryTokenProvider tokenProvider) =>
{
    if (string.IsNullOrWhiteSpace(vin) || vin.Length != 17)
    {
        return Results.BadRequest(new { message = "A valid 17-character VIN is required." });
    }

    try
    {
        // The browser never receives the API Consumer secret. It calls this proxy,
        // and the proxy attaches the cached bearer token server-side.
        var jwtToken = await tokenProvider.GetJwtTokenAsync();
        var requestUrl = $"https://ws.vinquery.com/vindecode/v3?VIN={Uri.EscapeDataString(vin)}&reportType=3&format=JSON";

        using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken);

        using var response = await httpClientFactory.CreateClient().SendAsync(request);
        if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
        {
            tokenProvider.InvalidateRejectedToken(jwtToken);
            jwtToken = await tokenProvider.GetJwtTokenAsync();

            using var retryRequest = new HttpRequestMessage(HttpMethod.Get, requestUrl);
            retryRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken);
            using var retryResponse = await httpClientFactory.CreateClient().SendAsync(retryRequest);
            return Results.Content(
                await retryResponse.Content.ReadAsStringAsync(),
                "application/json",
                statusCode: (int)retryResponse.StatusCode);
        }

        return Results.Content(
            await response.Content.ReadAsStringAsync(),
            "application/json",
            statusCode: (int)response.StatusCode);
    }
    catch (Exception ex)
    {
        app.Logger.LogError(ex, "VINdecode proxy failed.");
        return Results.Problem("VINquery request could not be completed.", statusCode: 502);
    }
});

app.Run();

public sealed class VinqueryTokenProvider
{
    private static readonly TimeSpan RefreshBuffer = TimeSpan.FromSeconds(60);
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IConfiguration _configuration;
    private readonly SemaphoreSlim _refreshLock = new(1, 1);
    private TokenResponse? _cachedToken;

    public VinqueryTokenProvider(IHttpClientFactory httpClientFactory, IConfiguration configuration)
    {
        _httpClientFactory = httpClientFactory;
        _configuration = configuration;
    }

    public async Task<string> GetJwtTokenAsync()
    {
        if (TokenIsUsable())
        {
            return _cachedToken!.JwtToken;
        }

        await _refreshLock.WaitAsync();
        try
        {
            if (TokenIsUsable())
            {
                return _cachedToken!.JwtToken;
            }

            var clientId = _configuration["VINquery:VINdecode:ClientId"];
            var clientSecret = _configuration["VINquery:VINdecode:ClientSecret"];
            if (string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(clientSecret))
            {
                throw new InvalidOperationException("VINquery API Consumer credentials are missing.");
            }

            var response = await _httpClientFactory.CreateClient().PostAsJsonAsync(
                "https://identity.vinquery.com/connect/token",
                new
                {
                    clientId,
                    clientSecret,
                    audience = "vinquery:api:vindecode"
                });

            response.EnsureSuccessStatusCode();
            _cachedToken = await response.Content.ReadFromJsonAsync<TokenResponse>()
                ?? throw new InvalidOperationException("Identity token response could not be parsed.");

            if (string.IsNullOrWhiteSpace(_cachedToken.JwtToken))
            {
                throw new InvalidOperationException("Identity response did not include jwtToken.");
            }

            return _cachedToken.JwtToken;
        }
        finally
        {
            _refreshLock.Release();
        }
    }

    public void InvalidateRejectedToken(string rejectedToken)
    {
        // Atomic compare/exchange prevents an old 401 from deleting a newer token.
        var cached = _cachedToken;
        if (cached is not null &&
            string.Equals(cached.JwtToken, rejectedToken, StringComparison.Ordinal))
        {
            Interlocked.CompareExchange(ref _cachedToken, null, cached);
        }
    }

    private bool TokenIsUsable()
    {
        // Use expiresUtc from the current Identity response and refresh before
        // exact expiry to avoid edge-of-expiration request failures.
        return _cachedToken is not null
            && !string.IsNullOrWhiteSpace(_cachedToken.JwtToken)
            && DateTimeOffset.UtcNow < _cachedToken.ExpiresUtc.Subtract(RefreshBuffer);
    }
}

public sealed class TokenResponse
{
    [JsonPropertyName("jwtToken")]
    public string JwtToken { get; set; } = "";

    [JsonPropertyName("expiresIn")]
    public int ExpiresIn { get; set; }

    [JsonPropertyName("expiresUtc")]
    public DateTimeOffset ExpiresUtc { get; set; }
}
Java
@RestController
public class VinDecodeProxyController {
    private final RestClient restClient = RestClient.create();

    // This lock prevents multiple simultaneous requests from all refreshing the
    // same JWT at the same time. The cache is intentionally process-local.
    private final ReentrantLock refreshLock = new ReentrantLock();
    private final Duration refreshBuffer = Duration.ofSeconds(60);
    private volatile TokenResponse cachedToken;

    @GetMapping("/api/vindecode")
    public ResponseEntity<String> vinDecode(@RequestParam String vin) {
        if (vin == null || vin.trim().length() != 17) {
            return ResponseEntity.badRequest().body("{\"message\":\"A valid 17-character VIN is required.\"}");
        }

        try {
            // The client application calls this proxy. Only the proxy knows the
            // Client Secret and only the proxy sends Authorization to VINquery.
            String jwtToken = getJwtToken();
            ResponseEntity<String> response = callVinDecode(vin, jwtToken);

            if (response.getStatusCode() == HttpStatus.UNAUTHORIZED) {
                invalidateRejectedToken(jwtToken);
                jwtToken = getJwtToken();
                response = callVinDecode(vin, jwtToken);
            }

            return ResponseEntity.status(response.getStatusCode())
                .contentType(MediaType.APPLICATION_JSON)
                .body(response.getBody());
        } catch (Exception ex) {
            // Log full details server-side and return a safe message to the client.
            System.err.println("VINdecode proxy failed: " + ex.getMessage());
            return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
                .body("{\"message\":\"VINquery request could not be completed.\"}");
        }
    }

    private String getJwtToken() {
        if (tokenIsUsable()) {
            return cachedToken.jwtToken();
        }

        refreshLock.lock();
        try {
            if (tokenIsUsable()) {
                return cachedToken.jwtToken();
            }

            String clientId = System.getenv("VINQUERY_VINDECODE_CLIENT_ID");
            String clientSecret = System.getenv("VINQUERY_VINDECODE_CLIENT_SECRET");
            if (clientId == null || clientId.isBlank() || clientSecret == null || clientSecret.isBlank()) {
                throw new IllegalStateException("VINquery API Consumer credentials are missing.");
            }

            Map<String, String> payload = Map.of(
                "clientId", clientId,
                "clientSecret", clientSecret,
                "audience", "vinquery:api:vindecode"
            );

            cachedToken = restClient.post()
                .uri("https://identity.vinquery.com/connect/token")
                .contentType(MediaType.APPLICATION_JSON)
                .body(payload)
                .retrieve()
                .body(TokenResponse.class);

            if (cachedToken == null || cachedToken.jwtToken() == null || cachedToken.jwtToken().isBlank()) {
                throw new IllegalStateException("Identity response did not include jwtToken.");
            }

            return cachedToken.jwtToken();
        } finally {
            refreshLock.unlock();
        }
    }

    private void invalidateRejectedToken(String rejectedToken) {
        refreshLock.lock();
        try {
            // Do not invalidate a newer token installed by another request.
            if (cachedToken != null && cachedToken.jwtToken().equals(rejectedToken)) {
                cachedToken = null;
            }
        } finally {
            refreshLock.unlock();
        }
    }

    private ResponseEntity<String> callVinDecode(String vin, String jwtToken) {
        // exchange() preserves a 401 response instead of throwing before retry logic runs.
        return restClient.get()
            .uri("https://ws.vinquery.com/vindecode/v3?VIN={vin}&reportType=3&format=JSON", vin)
            .header("Authorization", "Bearer " + jwtToken)
            .exchange((request, response) -> ResponseEntity
                .status(response.getStatusCode())
                .headers(response.getHeaders())
                .body(new String(response.getBody().readAllBytes(), java.nio.charset.StandardCharsets.UTF_8)));
    }

    private boolean tokenIsUsable() {
        // Use expiresUtc from Identity and renew shortly before expiration.
        return cachedToken != null
            && cachedToken.jwtToken() != null
            && Instant.now().isBefore(cachedToken.expiresUtc().minus(refreshBuffer));
    }

    // Current Identity response contract: jwtToken, expiresIn, expiresUtc.
    public record TokenResponse(String jwtToken, int expiresIn, Instant expiresUtc) { }
}