Read VIN barcodes from submitted images and return structured XML or JSON results for intake, inspection, and mobile capture workflows.
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 VINbarcode API only works with Code 39. Generally speaking, unlike a VIN captured by our VINocr API, a VIN captured through VINbarcode API requires no sanity check.
To achieve the highest possible recognition speed, the image size is a key factor.
Avoid Submitting The Same Image More Than Once
Our system deducts one credit from your account balance for each XML or JSON result returned, regardless of the outcome. Therefore, please ensure your system has a mechanism in place to prevent sending the same image to the API service more than once.
REST API Description
This API uses multipart POST HTTP method. Ensure your POST is a multipart/form-data request. The following three parameters and their values are required.
Parameter Name
Parameter
Value
Used in Request
JWT Bearer Token
Authorization header
Bearer <jwt-token>
Authorization: Bearer <jwt-token>
Input Image*
Image File*
JPG, JPEG, PNG, GIF, BMP
POST https://www.recognition.ws/vinbarcode/v3?saveimage=TRUE with multipart/form-data file
Save Image**
saveimage**
TRUE or FALSE (default: FALSE)
POST https://www.recognition.ws/vinbarcode/v3?saveimage=TRUE
VIN Decode***
vindecode***
TRUE or FALSE (default: FALSE)
POST https://www.recognition.ws/vinbarcode/v3?saveimage=TRUE&vindecode=TRUE
Data Format****
format****
XML or JSON (default: XML)
POST https://www.recognition.ws/vinbarcode/v3?saveimage=TRUE&vindecode=TRUE&format=JSON
Request a JWT first from POST https://identity.vinquery.com/connect/token using an API Consumer with audience vinquery:api:vinbarcode.
* Only one image file is allowed in a POST request. A POST request with multiple files would be rejected immediately. Only the first found barcode will be processed if an input image contains more than one barcode.
** Set parameter "saveimage" to "TRUE" if you'd like our system to save the input image so that later you can log in to your account to review it. Set the value of this parameter to "FALSE" otherwise.
*** Set parameter "vindecode" to "TRUE" if you'd like the service to retrieve make/model/year for the captured VIN code. Set the value of this parameter to "FALSE" otherwise.
**** Set parameter "format" to "JSON" if you'd like the service to send data in JSON format. If you don't include this parameter at all in the API URL, or explicitly set the value of this parameter to "XML" to get data in XML format.
Output in XML or JSON
VIN: VIN number in ASCII
Position of the barcdoe in an input image: Top(pixels), Left(pixels), Width(pixels), Height(pixels). The origin(0,0) is the top-left corner of an input image.
VINbarcode without VINdecode output: POST https://www.recognition.ws/vinbarcode/v3 with Authorization: Bearer <jwt-token>
VINbarcode with VINdecode output in JSON format: POST https://www.recognition.ws/vinbarcode/v3?vindecode=TRUE&format=JSON with Authorization: Bearer <jwt-token>
These server-side examples keep API Consumer credentials private, cache and reuse a valid VINbarcode JWT, renew shortly before expiration, upload the image in the inputimage multipart field, and perform one refresh-and-retry after an unexpected 401. Each retry creates a fresh multipart request body. The implementations follow 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_VINBARCODE_CLIENT_ID;
const clientSecret = process.env.VINQUERY_VINBARCODE_CLIENT_SECRET;
const audience = "vinquery:api:vinbarcode";
const identityTokenUrl = "https://identity.vinquery.com/connect/token";
const vinbarcodeUrl = "https://www.recognition.ws/vinbarcode/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.post("/api/vinbarcode", express.raw({ type: ["image/*", "application/octet-stream"], limit: "10mb" }), async (req, res) => {
try {
const imageBytes = req.body;
if (!Buffer.isBuffer(imageBytes) || imageBytes.length === 0) {
return res.status(400).json({ message: "An image is required." });
}
const callVinBarcode = (token) => {
// Build a fresh multipart body for the initial call and for the one retry.
const form = new FormData();
form.append("inputimage", new Blob([imageBytes], { type: req.type || "image/jpeg" }), "vin-image.jpg");
return fetch(`${vinbarcodeUrl}?format=JSON`, {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
body: form
});
};
let jwtToken = await getJwtToken();
let apiResponse = await callVinBarcode(jwtToken);
// A token can be revoked before expiresUtc. Invalidate and retry exactly once.
if (apiResponse.status === 401) {
invalidateRejectedToken(jwtToken);
jwtToken = await getJwtToken();
apiResponse = await callVinBarcode(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("VINbarcode 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_VINBARCODE_CLIENT_ID || "";
const clientSecret = process.env.VINQUERY_VINBARCODE_CLIENT_SECRET || "";
const audience = "vinquery:api:vinbarcode";
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.post("/api/vinbarcode", express.raw({ type: ["image/*", "application/octet-stream"], limit: "10mb" }), async (req: Request, res: Response) => {
try {
const imageBytes = req.body as Buffer;
if (!Buffer.isBuffer(imageBytes) || imageBytes.length === 0) {
return res.status(400).json({ message: "An image is required." });
}
const callVinBarcode = (token: string): Promise<globalThis.Response> => {
const form = new FormData();
form.append("inputimage", new Blob([imageBytes], { type: req.type || "image/jpeg" }), "vin-image.jpg");
return fetch("https://www.recognition.ws/vinbarcode/v3?format=JSON", {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
body: form
});
};
let jwtToken = await getJwtToken();
let apiResponse = await callVinBarcode(jwtToken);
if (apiResponse.status === 401) {
invalidateRejectedToken(jwtToken);
jwtToken = await getJwtToken();
apiResponse = await callVinBarcode(jwtToken);
}
return res.status(apiResponse.status).type("application/json").send(await apiResponse.text());
} catch (error) {
console.error("VINbarcode 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, File, HTTPException, Response, UploadFile
app = FastAPI()
# API Consumer credentials stay in trusted server-side configuration.
CLIENT_ID = os.environ.get("VINQUERY_VINBARCODE_CLIENT_ID", "")
CLIENT_SECRET = os.environ.get("VINQUERY_VINBARCODE_CLIENT_SECRET", "")
AUDIENCE = "vinquery:api:vinbarcode"
IDENTITY_TOKEN_URL = "https://identity.vinquery.com/connect/token"
VINBARCODE_URL = "https://www.recognition.ws/vinbarcode/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.post("/api/vinbarcode")
async def vinbarcode(inputimage: UploadFile = File(...)):
try:
image_bytes = await inputimage.read()
if not image_bytes:
raise HTTPException(status_code=400, detail="An image is required.")
def call_vinbarcode(token: str):
# A new files dictionary creates a fresh multipart body for each attempt.
return requests.post(
VINBARCODE_URL,
params={"format": "JSON"},
files={"inputimage": (inputimage.filename or "vin-image.jpg", image_bytes, inputimage.content_type or "image/jpeg")},
headers={"Authorization": f"Bearer {token}"},
timeout=30
)
jwt_token = get_jwt_token()
api_response = call_vinbarcode(jwt_token)
if api_response.status_code == 401:
api_response.close()
invalidate_rejected_token(jwt_token)
jwt_token = get_jwt_token()
api_response = call_vinbarcode(jwt_token)
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"VINbarcode 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_VINBARCODE_CLIENT_ID");
$clientSecret = getenv("VINQUERY_VINBARCODE_CLIENT_SECRET");
$audience = "vinquery:api:vinbarcode";
$identityTokenUrl = "https://identity.vinquery.com/connect/token";
$vinbarcodeUrl = "https://www.recognition.ws/vinbarcode/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_vinbarcode_jwt");
if ($cached && time() < ($cached["expiresAt"] - $refreshBufferSeconds)) {
return $cached["jwtToken"];
}
$lock = fopen(sys_get_temp_dir() . "/vinquery-vinbarcode-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_vinbarcode_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_vinbarcode_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-vinbarcode-token.lock", "c");
if (!$lock || !flock($lock, LOCK_EX)) {
throw new RuntimeException("Could not acquire token refresh lock.");
}
try {
$cached = apcu_fetch("vinquery_vinbarcode_jwt");
// Delete only the rejected token while holding the same lock used by refresh.
if ($cached && hash_equals($cached["jwtToken"], $rejectedToken)) {
apcu_delete("vinquery_vinbarcode_jwt");
}
} finally {
flock($lock, LOCK_UN);
fclose($lock);
}
}
function callVinBarcode(string $vinbarcodeUrl, string $imagePath, string $fileName, string $contentType, string $jwtToken): array {
$curl = curl_init($vinbarcodeUrl . "?format=JSON");
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . $jwtToken],
CURLOPT_POSTFIELDS => ["inputimage" => curl_file_create($imagePath, $contentType, $fileName)],
CURLOPT_TIMEOUT => 30
]);
$body = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
return [$status, $body];
}
try {
if (empty($_FILES["inputimage"]["tmp_name"])) {
http_response_code(400);
header("Content-Type: application/json");
echo json_encode(["message" => "An image is required."]);
exit;
}
$imagePath = $_FILES["inputimage"]["tmp_name"];
$fileName = $_FILES["inputimage"]["name"] ?: "vin-image.jpg";
$contentType = $_FILES["inputimage"]["type"] ?: "image/jpeg";
$jwtToken = getJwtToken();
[$apiStatus, $apiBody] = callVinBarcode($vinbarcodeUrl, $imagePath, $fileName, $contentType, $jwtToken);
if ($apiStatus === 401) {
invalidateRejectedToken($jwtToken);
$jwtToken = getJwtToken();
[$apiStatus, $apiBody] = callVinBarcode($vinbarcodeUrl, $imagePath, $fileName, $contentType, $jwtToken);
}
http_response_code($apiStatus);
header("Content-Type: application/json");
echo $apiBody;
} catch (Throwable $ex) {
error_log("VINbarcode 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"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"sync"
"time"
)
const (
audience = "vinquery:api:vinbarcode"
identityTokenURL = "https://identity.vinquery.com/connect/token"
vinbarcodeURL = "https://www.recognition.ws/vinbarcode/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_VINBARCODE_CLIENT_ID")
clientSecret := os.Getenv("VINQUERY_VINBARCODE_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 callVinBarcode(imageBytes []byte, fileName, jwtToken string) (*http.Response, error) {
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("inputimage", filepath.Base(fileName))
if err != nil { return nil, err }
if _, err = part.Write(imageBytes); err != nil { return nil, err }
writer.Close()
request, err := http.NewRequest("POST", vinbarcodeURL+"?format=JSON", &body)
if err != nil { return nil, err }
request.Header.Set("Authorization", "Bearer "+jwtToken)
request.Header.Set("Content-Type", writer.FormDataContentType())
return http.DefaultClient.Do(request)
}
func vinBarcodeHandler(w http.ResponseWriter, r *http.Request) {
file, header, err := r.FormFile("inputimage")
if err != nil {
http.Error(w, "An image is required.", http.StatusBadRequest)
return
}
defer file.Close()
imageBytes, err := io.ReadAll(file)
if err != nil || len(imageBytes) == 0 {
http.Error(w, "An image is required.", http.StatusBadRequest)
return
}
jwtToken, err := getJWTToken()
if err != nil {
fmt.Println("VINbarcode token failure:", err)
http.Error(w, "VINquery request could not be completed.", http.StatusBadGateway)
return
}
apiResponse, err := callVinBarcode(imageBytes, header.Filename, jwtToken)
if err != nil {
fmt.Println("VINbarcode 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
}
apiResponse, err = callVinBarcode(imageBytes, header.Filename, jwtToken)
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.MapPost("/api/vinbarcode", async (
IFormFile inputimage,
IHttpClientFactory httpClientFactory,
VinqueryTokenProvider tokenProvider) =>
{
if (inputimage is null || inputimage.Length == 0)
{
return Results.BadRequest(new { message = "An image is required." });
}
try
{
await using var imageStream = inputimage.OpenReadStream();
using var imageBuffer = new MemoryStream();
await imageStream.CopyToAsync(imageBuffer);
var imageBytes = imageBuffer.ToArray();
async Task<HttpResponseMessage> CallVinBarcodeAsync(string token)
{
using var upload = new MultipartFormDataContent();
var image = new ByteArrayContent(imageBytes);
image.Headers.ContentType = new MediaTypeHeaderValue(inputimage.ContentType ?? "image/jpeg");
upload.Add(image, "inputimage", inputimage.FileName ?? "vin-image.jpg");
using var request = new HttpRequestMessage(HttpMethod.Post, "https://www.recognition.ws/vinbarcode/v3?format=JSON")
{
Content = upload
};
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
return await httpClientFactory.CreateClient().SendAsync(request);
}
var jwtToken = await tokenProvider.GetJwtTokenAsync();
using var response = await CallVinBarcodeAsync(jwtToken);
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
tokenProvider.InvalidateRejectedToken(jwtToken);
jwtToken = await tokenProvider.GetJwtTokenAsync();
using var retryResponse = await CallVinBarcodeAsync(jwtToken);
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, "VINbarcode 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:VINbarcode:ClientId"];
var clientSecret = _configuration["VINquery:VINbarcode: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:vinbarcode"
});
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 VinBarcodeProxyController {
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;
@PostMapping(value = "/api/vinbarcode", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<String> vinBarcode(@RequestPart("inputimage") MultipartFile inputimage) {
if (inputimage == null || inputimage.isEmpty()) {
return ResponseEntity.badRequest().body("{\"message\":\"An image is required.\"}");
}
try {
byte[] imageBytes = inputimage.getBytes();
String fileName = inputimage.getOriginalFilename() == null ? "vin-image.jpg" : inputimage.getOriginalFilename();
String jwtToken = getJwtToken();
ResponseEntity<String> response = callVinBarcode(imageBytes, fileName, jwtToken);
if (response.getStatusCode() == HttpStatus.UNAUTHORIZED) {
invalidateRejectedToken(jwtToken);
jwtToken = getJwtToken();
response = callVinBarcode(imageBytes, fileName, 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("VINbarcode 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_VINBARCODE_CLIENT_ID");
String clientSecret = System.getenv("VINQUERY_VINBARCODE_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:vinbarcode"
);
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> callVinBarcode(byte[] imageBytes, String fileName, String jwtToken) {
MultipartBodyBuilder upload = new MultipartBodyBuilder();
upload.part("inputimage", new ByteArrayResource(imageBytes) {
@Override public String getFilename() { return fileName; }
});
// A new multipart body is built for the initial request and the one retry.
return restClient.post()
.uri("https://www.recognition.ws/vinbarcode/v3?format=JSON")
.header("Authorization", "Bearer " + jwtToken)
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(upload.build())
.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) { }
}