Adding a country

  1. 1. Implement the driver
  2. 2. Parse whatever your API returns - you are in control
    1. The ArrayReader helper
    2. Transport, 404, and empty responses
  3. 3. Register it
  4. 4. Score it (optionally per country)
  5. 5. Certify it

Adding a country (or any registry) is one driver + one registration. Nothing in the core, the checks, or the framework bridges changes.

1. Implement the driver

A RegistryDriver declares what it can answer and how to look it up. The easiest base is AbstractRegistryDriver, which derives supports() from your declarations:

use Gawrys\Counterparty\Registry\AbstractRegistryDriver;
use Gawrys\Counterparty\Registry\{LookupRequest, LookupResult};
use Gawrys\Counterparty\Enum\RegistryCapability;
use Gawrys\Counterparty\Http\JsonHttpClient;
use Gawrys\Counterparty\Support\ArrayReader;

final readonly class GermanRegistryDriver extends AbstractRegistryDriver
{
    public function __construct(private JsonHttpClient $http) {}

    public function capabilities(): array
    {
        return [RegistryCapability::LegalEntityData, RegistryCapability::BusinessRegistration];
    }

    public function countries(): array
    {
        return ['DE'];
    }

    public function lookup(LookupRequest $request): LookupResult
    {
        // ... see below
    }
}

2. Parse whatever your API returns - you are in control

lookup() is your code. Call any API, map its fields however you like, and return a LookupResult. You decide what goes into data (exposed to checks and risk rules), proofId (kept as due-diligence proof), and sourceUrl (grounding / audit link).

public function lookup(LookupRequest $request): LookupResult
{
    $nip = $request->counterparty->nip;
    if ($nip === null) {
        return LookupResult::notFound('https://handelsregister.de/');
    }

    // Suppose the API returns:
    // { "status": "ACTIVE", "company": { "legalName": "Muster GmbH", "regId": "HRB 12345" } }
    $json = $this->http->getJson("https://my-registry.example/v2/company?taxId={$nip}");
    $r = ArrayReader::of($json);

    if ($r->string('status') !== 'ACTIVE') {
        return LookupResult::notFound('https://handelsregister.de/');
    }

    $company = $r->nested('company');

    return LookupResult::found(
        data: [                                       // anything you want downstream
            'legalName' => $company->string('legalName'),
            'status'    => $r->string('status'),
        ],
        proofId: $company->string('regId'),            // -> CheckResult::$proofId
        sourceUrl: 'https://handelsregister.de/',       // grounding / audit link
    );
}

The ArrayReader helper

Decoded JSON is array<string, mixed>. ArrayReader extracts values type-safely so you stay strict (PHPStan max / Psalm level 1) without scattering is_* checks:

Method Returns
string($key) ?string
bool($key) ?bool
float($key) ?float (accepts numeric strings)
nested($key) a child ArrayReader (empty if absent)
stringList($key) list<string>
each($key) list<ArrayReader> (for arrays of objects)
has($key) bool

You are not required to use it - parse with plain PHP if you prefer.

Transport, 404, and empty responses

  • JsonHttpClient throws HttpRequestFailed on a non-2xx status or invalid JSON; the Verifier converts an unexpected throw into an inconclusive result, so a flaky upstream never crashes verification.
  • A 2xx with an empty body (e.g. HTTP 204) is treated as no content and decodes to [], so “not in this registry” becomes a clean not-found rather than an error.
  • Model “found but negative” explicitly: return LookupResult::found(...) with a status field your risk rule can inspect, rather than notFound().

3. Register it

Plain PHP:

$registries->extend('de', fn () => new GermanRegistryDriver($http));

Laravel (in a service provider or via the facade):

Counterparty::extendRegistry('de', fn ($cfg) => new GermanRegistryDriver($http));

Symfony - tag a service; a compiler pass collects it into the shared manager:

services:
    App\Registry\GermanRegistryDriver:
        arguments: ['@Gawrys\Counterparty\Http\JsonHttpClient']
        tags:
            - { name: counterparty.registry_driver, alias: de }

That is all. A RegistryCheck for LegalEntityData will now route DE counterparties to your driver, and the result’s data is available to risk rules.

4. Score it (optionally per country)

The data you returned lands in CheckResult::$raw, and risk rules receive the country, so you can score German results differently from Polish ones - see Country-specific scoring.

5. Certify it

Make your driver pass the shipped contract test - this is what keeps the ecosystem trustworthy.


Counterparty Verification - a due-diligence aid, not a compliance product. MIT licensed.

This site uses Just the Docs, a documentation theme for Jekyll.