A PHP client for two DVLA APIs: the Vehicle Enquiry Service, which returns a vehicle’s details from its registration, and KADOE, which returns who kept a vehicle on a given date. It builds on Lendable’s open-source Vehicle Enquiry client; I added the KADOE client and JWT authentication that refreshes itself.
rolemaintainerextended an open-source client
stackPHP 8.1+PSR-7 · PSR-18 · value objects
scale2 DVLA APIsVehicle Enquiry · KADOE
dates2025June 2025
1 · the problem
Two government APIs, two kinds of auth, and secrets that must not end up in a log.
Looking up a vehicle, or who kept it on the date of an event, means two DVLA APIs that authenticate differently: a static API key for one, short-lived tokens from a sign-in call for the other.
Calling them by hand spreads raw arrays, retry-free token handling and credentials through an application. A small library can make the requests typed, the failures explicit and the secrets invisible.
2 · what I built
Typed requests in, typed responses out, auth wrapped around any HTTP client.
The library never chooses an HTTP client for you: it takes any PSR-18 client and wraps it in decorators that add authentication. Lendable’s original gave the Vehicle Enquiry side its shape; I followed the same design for KADOE.
▸A KADOE clientAsk by registration number or by chassis/VIN, with the enquirer, reason code, event date and reference the API requires, and get back keeper and address value objects.
▸JWT authentication that refreshes itselfA token provider signs in with credentials, caches the token and fetches a new one once it expires; a decorator adds it to every request.
▸Auth as decorators over PSR-18API-key and JWT authentication wrap whichever PSR-18 client the application already uses, so the library adds no HTTP stack of its own.
▸Secrets that do not leakAPI keys and tokens are wrapped so they never show up in a var_dump, a stack trace or a log line.
▸Value objects throughoutRegistration numbers, VINs, dates, MOT and tax status are objects that validate themselves, so a bad value fails at the edge, not inside the API call.
▸Failures you can catch by kindA request that failed, one the API rejected with an error or a message, and a response that could not be decoded are separate exception types.
How a lookup travels through the library
Select any box to see what it does. Your application only ever touches the client and the value objects.
architecture
the librarycalltypedthrowsrequesttokensendhttpssign in
selectedclient · scopes[PHP 8.1]
One client per API with a scope for its resource: vehicle details for the Vehicle Enquiry Service, and the keeper at a date for KADOE. It turns value objects into requests and responses back into value objects.
3 · infrastructure
A library has no servers. Its CI is the infrastructure.
Select any box. Every push runs a matrix of six jobs, so the library is proven against the oldest and newest dependencies it allows.
infrastructure
GitHub Actionsruns
selected6 jobs[GITHUB ACTIONS]
Two PHP versions times three dependency sets: the lowest versions the constraints allow, the highest, and the lock file. A library has to work for whoever installs it, not just on my machine.
It already had the right shape: PSR-18, value objects, typed errors. Adding KADOE in the same style beat writing a second library with a different feel.
cost
Its conventions came with it, so KADOE follows them even where I might have chosen differently.
4.2 · Bring your own HTTP client
chose
Depend on PSR-18 interfaces only
because
Applications already have an HTTP client with their own timeouts and logging. Forcing another one causes version conflicts.
cost
The caller has to supply a client and a request factory, which is a little more set-up.
4.3 · Refresh tokens inside the library
chose
A caching token provider behind the JWT decorator
because
Token expiry is the API’s concern, not the application’s. A worker that runs for hours should never have to think about it.
cost
The library now holds credentials and state, so they are hidden from dumps and the refresh logic is unit-tested.
5 · testing & delivery
Strict analysis on every push, across the versions people actually run.
Every push runs composer validate, a parallel lint, php-cs-fixer and Rector as dry runs, and PHPStan at level 8 with strict rules, on PHP 8.1 and 8.2 against the lowest, highest and locked dependency sets.
The tests mock the PSR-18 client, so they exercise the real request building, auth and decoding without calling the DVLA.