sashalenz/nhtsa-api
NHTSA vPIC API for Laravel
A small, typed Laravel integration for the NHTSA vPIC API. It decodes VINs and WMIs into predictable DTOs, with built-in retries, optional proxying, and opt-in request-history logging.
Requirements
- PHP 8.4 or 8.5
- Laravel 12 or 13
Installation
composer require sashalenz/nhtsa-api
The service provider and the NhtsaApi facade are auto-discovered. Publish the config only if you need to change the defaults:
php artisan vendor:publish --tag="nhtsa-api-config"
Configuration
config/nhtsa-api.php:
| Key | Env | Default | Description |
|---|---|---|---|
base_url |
NHTSA_API_BASE_URL |
https://vpic.nhtsa.dot.gov/api/ |
vPIC base URL. |
timeout |
— | 10.0 |
Request and connection timeout, in seconds. |
format |
NHTSA_API_FORMAT |
json |
Response format query parameter. |
retry.times |
— | 3 |
Attempts before failing. Set 0 to disable retries. |
retry.sleep |
— | 150 |
Delay between attempts, in milliseconds. |
proxy |
NHTSA_API_PROXY |
null |
HTTP proxy: a URL string, a Closure resolved per request, or null. |
Usage
Every call goes through the NhtsaApi facade and accepts a request DTO.
Decode a VIN — flat key/value (recommended)
decodeVinValues returns one result keyed by NHTSA variable name. It is the most convenient shape for reading individual fields:
use Sashalenz\NhtsaApi\Data\Requests\DecodeVinRequestData;
use Sashalenz\NhtsaApi\Facades\NhtsaApi;
$result = NhtsaApi::decodeVinValues(
DecodeVinRequestData::from(['vin' => '58ADA1C18MU003613'])
)->firstResult();
$make = $result?->get('Make'); // "LEXUS"
$model = $result?->get('Model'); // "ES"
$body = $result?->get('BodyClass'); // "Sedan/Saloon"
$year = $result?->get('ModelYear'); // "2021"
$fuel = $result?->get('FuelTypePrimary');
get() accepts a default: $result->get('Trim', 'n/a'). Use ->results() to iterate every result row.
Decode a VIN — variable list
decodeVin returns the classic NHTSA variable list as VinDecodedVariableData items (variable, value, valueId, variableId):
$response = NhtsaApi::decodeVin(DecodeVinRequestData::from([
'vin' => '5UXWX7C5*BA',
'modelYear' => 2011, // optional, narrows ambiguous VIN patterns
]));
$make = $response->variables()->firstWhere('variable', 'Make')?->value;
Decode a WMI
use Sashalenz\NhtsaApi\Data\Requests\DecodeWmiRequestData;
$wmi = NhtsaApi::decodeWmi(DecodeWmiRequestData::from(['wmi' => '5UX']));
Façade methods
| Method | Returns | Notes |
|---|---|---|
decodeVin |
DecodeVinResponseData |
Classic variable list. |
decodeVinExtended |
DecodeVinExtendedResponseData |
Adds extended / NCSA variables. |
decodeVinValues |
VinDecodeFlatResponseData |
Flat key/value result. |
decodeVinValuesExtended |
VinDecodeFlatResponseData |
Flat, extended. |
decodeWmi |
DecodeWmiResponseData |
WMI metadata. |
Service layer (request history)
NhtsaApiService wraps the client and logs every decode to the nhtsa_api_requests table — VIN, vehicle descriptor, make, model, model year, the raw decoded values, and a polymorphic link to the initiator.
use Sashalenz\NhtsaApi\Data\Requests\DecodeVinRequestData;
use Sashalenz\NhtsaApi\Services\NhtsaApiService;
$service = app(NhtsaApiService::class);
// Resolve just the make (and log the request, optionally attributed to a model).
$make = $service->determineMake(
DecodeVinRequestData::from(['vin' => 'WP0AA2A7GL', 'modelYear' => 2016]),
initiator: $user, // optional
);
// Full flat decode, logged.
$response = $service->decodeVinWithHistory(
DecodeVinRequestData::from(['vin' => 'WP0AA2A7GL'])
);
// How many times this VIN has been looked up.
$count = $service->getVinRequestCount('WP0AA2A7GL');
Run the package migration to create the history table:
php artisan migrate
Error handling
A non-2xx response (after the configured retries are exhausted) raises NhtsaApiRequestException:
use Sashalenz\NhtsaApi\Exceptions\NhtsaApiRequestException;
try {
NhtsaApi::decodeVin(DecodeVinRequestData::from(['vin' => $vin]));
} catch (NhtsaApiRequestException $e) {
// vPIC was unavailable — fall back to manual entry, queue a retry, etc.
}
Testing
The client is built on Laravel's HTTP client, so Http::fake() intercepts every request in your tests:
use Illuminate\Support\Facades\Http;
Http::fake([
'*/DecodeVinValues/*' => Http::response([
'Count' => 1,
'Message' => 'Results returned successfully',
'Results' => [['Make' => 'LEXUS', 'Model' => 'ES']],
]),
]);
Run the package's own suite:
vendor/bin/pest
License
The MIT License (MIT). See LICENSE for details.
Related Packages
Powerful PHP database abstraction layer (DBAL) with many features for database s...
Laravel Serializable Closure provides an easy and secure way to serialize closur...
Cli error handling for console/command-line PHP applications.