Why does the License API alone hold the Statamic credential, when other services sometimes need a license?
Because the alternative is credential sprawl, and sprawl gets more expensive with every service that joins. Each outside vendor is reached by exactly one internal service, and that service is the only holder of that vendor’s credentials. Everyone else who needs the capability asks the owner.
So the hub has no Statamic token and no opinion about how Statamic shapes its errors. It has a small client that calls the License API’s license endpoints, the same endpoints any authorized consumer calls. If Statamic changes its API tomorrow, one codebase changes: the driver from Chapter 3. Every other service keeps asking for licenses and never notices.
This is the contract from Chapter 3 applied a second time. Inside the License API, LicenseContract hides which provider is underneath from the rest of the application. Across the system, the owning service hides it from every other application.
It also tells you what to do when one leaf seems to need another. It doesn’t get a second credential and a direct line. The hub, which has already spoken to both, hands it what it needs.
The Client on the Other Side
This book has built the License API from the inside. The hub sees it from the outside, as a consumer, and the code it uses to call it deserves the same care, because every rule this book asked of consumers now applies to you.
Start by configuring the client once. Laravel’s HTTP client lets you register a named, preconfigured client as a macro:
// app/Providers/AppServiceProvider.php, in boot()
Http::macro('licenses', function (): PendingRequest {
$config = config('services.licenses');
return Http::baseUrl($config['url'])
->withToken($config['token'])
->acceptJson()
->connectTimeout(3)
->timeout(10)
->withHeader('X-Trace-Id', Context::get('trace_id'));
});
Every call to Http::licenses() now has the base URL, the token, the Accept header, both timeouts from Chapter 3, and the trace ID from Chapter 7, so that one operation can be followed across both services’ logs. Nobody calling the License API from the hub can forget any of them, because nobody writes them again. (Laravel documentation: HTTP Client › Macros.)
Then wrap the endpoints in a small class that speaks the hub’s language:
// app/Services/LicenseClient.php
class LicenseClient
{
public function create(Site $site): string
{
// The same site gives the same key on every retry.
$key = Uuid::uuid5(
Uuid::NAMESPACE_URL,
"webplo:site:{$site->id}",
)->toString();
return Http::licenses()
->withHeader('Idempotency-Key', $key)
->post('/api/licenses', [
'name' => $site->name,
'domain' => $site->domain,
])
->throw()
->json('data.key');
}
}
Three things in that method are this book’s advice turned around.
The idempotency key is derived, not random. It is a UUID, because that is what Chapter 8’s middleware accepts, and a version 5 UUID is a hash of a name. Built from the site’s ID, it is the same on every retry of the job that calls this. That is Chapter 8’s contract kept from the client’s side, and Chapter 10’s stable names, in one line.
It throws. A failed call becomes an exception, the job fails, and the queue’s retry policy takes over. The client doesn’t retry by itself, for the reason Chapter 18 gave: one retry policy, in one place.
It returns what the hub needs, not what the API sent. The hub wants a license key. It doesn’t want an array to dig through at every call site. If the License API’s response shape changes with a new version, this method is the only line in the hub that knows.
That last point is the Resource from Chapter 2, mirrored. A Resource decides what leaves a service. A client class decides what enters the next one. Between them, the JSON on the wire is a detail that two files know about and nothing else does.