VAT ID by asign
CommunityTax ID and VAT number validation for Filament: offline format and checksum rules for EU VAT, PL NIP/REGON, CZ IČO/DIČ and UA ЄДРПОУ/РНОКПП, plus an input that looks companies up in VIES, ARES, the Polish white list and GUS.
Author:
asign
Package health
Automated checks of this plugin's Composer package
15 checks
- Passed: GitHub Actions pinned to SHA
- Skipped: GitLab CI includes pinned to SHA
- Passed: Open security advisories
- Passed: Dependabot PR responsiveness — No open Dependabot PRs.
- Skipped: Renovate MR responsiveness
- Passed: Dependabot or Renovate configured
- Passed: Dependency update cooldown configured
- Passed: Provides a security policy
- Passed: Abandoned or archived — No consulted source marks the package abandoned (packagist, github).
- Passed: Commit and release recency — Active: last commit 0 days ago; last release 0 days ago.
-
Passed:
composer.lock not committed by library
—
composer.lockis absent from the released dist archive. - Passed: Dist archive is lean
-
Passed:
Current Laravel version supported
—
Package dependencies resolve together with current Laravel
13.0. -
Passed:
Current PHP version supported
—
Constraint
^8.3supports current PHP8.5. - Skipped: Current Symfony version supported
filament/
namespace. Review the source and install at your own risk. Found
malware or an unresolved security issue the author won't
address?
Report it
.
Documentation
- Screenshots
- Requirements
- Installation
- Usage
- Configuration
- Gotchas
- Data and privacy
- Translations
- AI agents
- Testing
- Changelog
- License
Validate and look up tax identifiers in Filament forms. Offline rules check the format and the checksum of EU VAT numbers (all 27 member states and Northern Ireland), Polish NIP and REGON, Czech IČO and DIČ and Ukrainian ЄДРПОУ and РНОКПП. An optional "Look up" button asks a company registry (VIES, the Polish white list, GUS, ARES) and fills the name, address, REGON and bank accounts into the sibling fields. No API key is needed except for GUS.
#Screenshots
One click on the search icon looks the company up and fills name, address, REGON and bank account:

Numbers are checked offline first — format and check digits — before any registry is asked:


Dark mode:

#Requirements
- PHP 8.3+
- Laravel 12 or 13
- Filament 5
#Installation
composer require asignua/filament-vat-id
Registering the plugin is optional (the fields and rules work without it); it only gives the panel provider a place to add registries:
use Asignua\FilamentVatId\VatIdPlugin;
$panel->plugin(VatIdPlugin::make());
Publish the config when you need to change anything: php artisan vendor:publish --tag=filament-vat-id-config.
#Usage
#The input
use Asignua\FilamentVatId\Enums\TaxIdType;
use Asignua\FilamentVatId\Forms\Components\TaxIdInput;
TaxIdInput::make('vat_number')
->countryField('country') // a sibling field holding the ISO country; or ->country('PL')
->type(TaxIdType::EuVat) // the default
->vies() // verify against a registry on save (network)
->lookup() // "Look up" button
->fill([ // CompanyData path => form state path (relative to the field's container)
'name' => 'company_name',
'address' => 'address',
'regon' => 'regon',
'bankAccounts.0' => 'iban',
]);
An EU VAT number carries its country prefix (PL5260250995) or takes the country from country() / countryField()
(5260250995 + PL); a prefix that contradicts the country is an error. Greece is EL in VIES; GR works as the country.
Separators (spaces, dots, dashes) are ignored. ->normalized() saves the value without them.
The other types have a fixed country, so they need no country:
TaxIdInput::make('nip')->type(TaxIdType::PlNip)->lookup()->fill(['name' => 'company', 'address' => 'address']);
TaxIdInput::make('edrpou')->type(TaxIdType::UaEdrpou);
->type() keeps Filament's meaning for strings (->type('tel') is still the HTML input type); only a TaxIdType (or a
closure returning one) sets the identifier type.
What the button does: it validates the number offline, asks the registries that support the country and type (in the configured order, the first with data wins), fills the mapped fields, shows the company name under the field and sends a notification. "Not found", "registry unavailable" and "no registry for this country" are notifications, not exceptions. Mapped values that are empty are skipped, so a lookup never wipes what the user typed.
->vies() adds a remote check on save, after the offline rule. It is answered only by registries that are authoritative for
the type, never by a domestic list:
| Type | Remote check answered by | Otherwise |
|---|---|---|
EuVat |
VIES only (a domestic register such as the white list does not prove a VAT-UE registration) | - |
CzIco, CzDic |
ARES | a birth-number DIČ of an individual cannot be resolved: the check is skipped, not failed |
PlNip, PlRegon |
GUS (existence in REGON; needs a key) | without a key the check is skipped. A white-list miss is never treated as "does not exist": it only means "not an active VAT payer" |
UaEdrpou, UaRnokpp |
none | skipped |
A company the registry marks inactive (closed in GUS, ended in ARES) is rejected too. The lookup button, in contrast, asks
every registry that supports the type, in the configured order, so the white list still fills bank accounts.
What happens when no registry answers is decided by on_unavailable (see Configuration; it applies to ->vies() and to
RegisteredTaxId alike). ->registry(Vies::class) pins the check and the lookup to one registry. A non-EU country on an
EU VAT field (UA, US…) means there is nothing to validate: the value passes offline and remotely.
#Rules
use Asignua\FilamentVatId\Rules\TaxId;
['required', new TaxId(TaxIdType::PlNip)]
['nullable', TaxId::make(TaxIdType::EuVat)->country(fn () => request('country'))]
TaxId works offline; a blank value passes (add required). Rules\RegisteredTaxId is the remote counterpart. To use
the checks outside validation:
use Asignua\FilamentVatId\Support\TaxIdValidator;
TaxIdValidator::isValid('PL 526-025-09-95', TaxIdType::EuVat); // true
TaxIdValidator::isValid('45274649', TaxIdType::EuVat, 'CZ'); // true
TaxIdValidator::normalize('526-025-09-95', TaxIdType::PlNip); // "5260250995"
TaxIdValidator::format('5260250995', TaxIdType::PlNip); // "526-025-09-95"
#Offline checks
| Type | Checked |
|---|---|
| EU VAT | format of every member state + XI; checksum for AT, BE, CZ, DE, DK, EE, EL, ES, FI, FR, HR, HU, IE, IT, LT, LU, LV, MT, NL, PL, PT, RO, SE, SI, SK, XI. Format only: BG, CY (and French numbers with a letter in the key, Latvian natural persons) |
PlNip |
10 digits, weights 6 5 7 2 3 4 5 6 7, mod 11 |
PlRegon |
9 digits (weights 8 9 2 3 4 5 6 7) or 14 digits (plus weights 2 4 8 5 0 9 7 3 6 1 2 4 8) |
CzIco |
8 digits, weights 8 7 6 5 4 3 2, mod 11 |
CzDic |
8 digits = IČO; 9-10 digits = birth number (date + mod 11); 9 digits starting with 6 = format only |
UaEdrpou |
8 digits, the official two-pass weighting (1..7, then 3..9; 7 1 2 3 4 5 6 for numbers 30 000 000 - 60 000 000) |
UaRnokpp |
10 digits, weights -1 5 7 9 4 6 10 5 7, mod 11, mod 10 |
A checksum proves the number is well-formed, not that the company exists. Use the registries for that.
#The entry
use Asignua\FilamentVatId\Infolists\Components\TaxIdEntry;
TaxIdEntry::make('vat_number')->countryField('country'); // "PL 5260250995", copyable
TaxIdEntry::make('nip')->type(TaxIdType::PlNip); // "526-025-09-95"
#With asignua/filament-iban
There is no dependency in either direction. The lookup writes plain strings, so map a returned IBAN into any field:
use Asignua\FilamentIban\Forms\Components\IbanInput; // asignua/filament-iban
TaxIdInput::make('nip')->type(TaxIdType::PlNip)->lookup()->fill(['name' => 'company', 'bankAccounts.0' => 'iban']),
IbanInput::make('iban'),
Only the Polish white list returns bank accounts (as IBANs, PL + the 26-digit account number). A company can have
dozens; bankAccounts.0 is simply the first one the registry lists.
#Registries
| Country | Source | Class | Key | Returns |
|---|---|---|---|---|
| EU + XI | VIES REST | Vies |
no | valid flag; name and address where the member state shares them (--- becomes empty) |
| PL | MF white list (Biała lista) | BialaLista |
no | name, address (+ street / postcode / city), VAT status, NIP, REGON, KRS, bank accounts |
| PL | GUS BIR 1.1 (SOAP) | GusBir |
yes (FILAMENT_VAT_ID_GUS_KEY) |
name, address, NIP, REGON, whether the business was closed |
| CZ | ARES REST | Ares |
no | name, address (+ street / postcode / city), DIČ, IČO, ended or not. IČO (8 digits) only; a birth-number DIČ is not found |
| UA | none free | - | - | validation only |
Each registry answers supports($country, $type) without touching the network. CompanyData holds name, address,
street, city, postcode, country, vatNumber, regon, ico, registryId (KRS for Poland), active, status,
bankAccounts (IBANs), source and the registry's own record in raw.
#Ukraine and other countries
There is no free, stable, key-less registry for Ukrainian companies, so UaEdrpou and UaRnokpp are validated offline only.
To add one (a paid provider, your own database, a different country), implement the contract and list the class:
use Asignua\FilamentVatId\Contracts\CompanyRegistry;
class MyUkrainianRegistry implements CompanyRegistry
{
public function supports(string $country, TaxIdType $type): bool
{
return $country === 'UA' && $type === TaxIdType::UaEdrpou;
}
public function canVerify(string $country, TaxIdType $type): bool
{
return false; // true only if "not found" really means "does not exist" (then ->vies() uses it too)
}
public function lookup(string $country, TaxIdType $type, string $number): ?CompanyData // null = not found
{
// throw Asignua\FilamentVatId\Exceptions\RegistryUnavailable when the provider cannot answer,
// Asignua\FilamentVatId\Exceptions\NumberNotSupported when it cannot handle this kind of number
}
}
// config/filament-vat-id.php: 'registries' => [..., MyUkrainianRegistry::class]
// or on the panel:
$panel->plugin(VatIdPlugin::make()->registry(new MyUkrainianRegistry));
#Configuration
php artisan vendor:publish --tag=filament-vat-id-config
| Key | Default | |
|---|---|---|
registries |
white list, GUS, ARES, VIES | Registry classes in order of preference; resolved from the container |
timeouts.connect / timeouts.request |
3 / 6 s | Per registry call |
total_timeout |
12 s | Upper bound for one lookup or verification across all registries; verification also stops at the first unavailable registry |
cache.enabled / ttl / store / prefix |
on / 3600 s / default store | "Found" and "not found" are cached per registry, country, type and number; "unavailable" never. The GUS session id is cached separately and always (about 50 minutes), whatever cache.enabled says |
on_unavailable |
warn |
When remote verification (->vies(), RegisteredTaxId) gets no answer: allow accepts silently; warn accepts and shows a notification (only inside a Filament form); fail rejects |
gus.key |
FILAMENT_VAT_ID_GUS_KEY |
GUS is disabled while empty |
gus.environment |
FILAMENT_VAT_ID_GUS_ENV = prod |
test uses the public test endpoint and key abcde12345abcde12345 (scrambled data) |
#Gotchas
- VIES is only as available as the member state behind it.
MS_UNAVAILABLE(DE and ES are frequent offenders, usually at night and weekends) means "try later", not "invalid". It surfaces asRegistryUnavailable, andon_unavailabledecides whether the form still saves. For a hard requirement usefail; for a form people must be able to finish usewarn. - The Polish white list is rate limited (per IP, with a daily quota); HTTP 429 is reported as "unavailable". Keep the cache on, and do not run bulk imports through it. When it is down or throttled the manager falls through to GUS (if a key is configured) and then VIES.
- GUS needs a key: register at api.stat.gov.pl and set
FILAMENT_VAT_ID_GUS_KEY. The test environment works with the public key but returns made-up addresses. The session id is cached for 50 minutes and renewed when GUS drops it. A wrong key is answered with an empty session and shows up as "unavailable". - No ext-soap: GUS is spoken as raw SOAP 1.2 over the HTTP client, the MTOM answer is parsed by cutting out the
envelope. If GUS changes the wrapper,
GusBir::extractResult()is the one place to look. - One registry saying "not found" does not hide another one being down: with
[BialaLista, Vies], "not found" + "unavailable" is reported as unavailable, because the second might have known the company. - Ukraine has no free registry (see above); the lookup button reports "no registry for this country".
- The hint under the field (company name after a lookup) is kept in the session, keyed by the field's state path and tied to the number it was found for; it never touches the form data and disappears when the number changes.
- Registries outside a panel.
VatIdPlugin::registry()only runs when a panel boots (and is deduplicated by class). Registries needed in jobs, commands or APIs belong in theregistriesconfig. - HTTP-client logging. The GUS key travels in the login SOAP body. Telescope, Debugbar or any HTTP-client logger that
records request bodies will record it: exclude these requests (host
wyszukiwarkaregon.stat.gov.pl) or the key. - Rules run on submit. The remote check runs after the offline one and is skipped for a malformed number, so a typo never costs a network call.
#Data and privacy
A lookup or a ->vies() check sends the identifier (and nothing else) to the registry: VIES (European Commission), the
Ministry of Finance white list, GUS or ARES, depending on the country. Those are public registers, but you are still
transmitting a number your user typed, so mention it in your privacy notice when the number identifies a sole proprietor
(a Polish NIP, a Czech birth-number DIČ, a Ukrainian РНОКПП). Results are kept in your cache for cache.ttl seconds and are
never written to a database by this package. Set cache.enabled to false to keep nothing. The package sends a
User-Agent: asignua-filament-vat-id header and no tracking of any kind.
#Translations
The interface ships in English, Ukrainian, German, Spanish, French, Italian, Dutch, Polish, Brazilian Portuguese and Turkish
under the filament-vat-id::filament-vat-id namespace. A test keeps every language in step with the English keys. Override a
string by publishing the translations (--tag=filament-vat-id-translations) and editing the copy in
lang/vendor/filament-vat-id.
#AI agents
The package ships Laravel Boost guidelines (resources/boost/guidelines/core.blade.php) so a
coding agent wires it up correctly.
#Testing
composer install
vendor/bin/phpunit
vendor/bin/phpstan analyse --memory-limit=1G
vendor/bin/pint --test
#Changelog
See CHANGELOG.md.
#License
The MIT License (MIT). See LICENSE.md.
The author
asign is a small web-dev company from Lviv, Ukraine. We build business applications on Laravel and Filament — CRMs, automation systems for standard and non-standard business processes, booking and content management systems, including our own Filament-based CMS. We open-source the parts that prove useful beyond a single project
From the same author
Relation Manager Tabs
Render relation managers as ordinary tabs of the record form, so an edit or view page has exactly one row of tabs.
Author:
asign
SEO Files
Generate and edit sitemap.xml with hreflang and a sitemap index for large sites, robots.txt and llms.txt / llms-full.txt from your Filament panel
Author:
asign
Row Layout
Lays Filament table records out in several lines while keeping the native column manager, sorting, selection, grouping, and actions.
Author:
asign
Rich Links
Internal links for the RichEditor: pick a record, store its ID instead of a URL, and resolve it to the current address in every language — slug changes and unpublished pages handled.
Author:
asign
Featured Plugins
A selection of plugins curated by the Filament team
Custom Dashboards
Let your users build and share their own dashboards with a drag-and-drop interface. Define your data sources in PHP and let them do the rest.
Filament
Custom Fields
Eliminate custom field migrations forever. Let your users create and manage form fields directly in Filament admin panels with 20+ built-in field types, validation, and zero database changes.
Relaticle
Spotlight Pro
Browse your Filament Panel with ease. Filament Spotlight Pro adds a Spotlight like Command Palette to your Filament Panel.
Dennis Koch