Skip to content
PN Scripts

Development

Validating Bulgarian EGN, EIK, VAT numbers and IBANs in Laravel

  • PN Scripts
  • 8 min read

Almost every Bulgarian business form asks for at least one number with a structure: the EGN (personal number) of a person, the LNCh of a foreigner, the EIK or BULSTAT of a company, a VAT number, an IBAN. Most Laravel applications validate them with digits:10 or a regular expression, which accepts 1234567890 as an EGN and a mistyped IBAN as a valid account. Each of these numbers carries a check digit, and checking it catches most typing mistakes before they reach an invoice, a contract or a bank. This guide explains how each number is built and gives small, tested PHP functions you can wrap in Laravel rules.

#The EGN: a date, a serial number and a check digit

The EGN has ten digits:

  • digits 1 to 6 are the birth date as YYMMDD, with the century hidden in the month: 01 to 12 for 1900 to 1999, 21 to 32 for 1800 to 1899, and 41 to 52 for 2000 to 2099. Someone born on 3 March 2005 has an EGN starting 054303;
  • digits 7 to 9 are a serial number, and the parity of the ninth digit gives the sex: even for men, odd for women;
  • digit 10 is the check digit.

The check digit is the weighted sum of the first nine digits with the weights 2, 4, 8, 5, 10, 9, 7, 3, 6, modulo 11; a remainder of 10 becomes 0. A correct check needs both parts, a real calendar date and the checksum:

function egn_valid(string $egn): bool
{
    if (! preg_match('/^\d{10}$/', $egn)) {
        return false;
    }
    $d = array_map('intval', str_split($egn));
    $year = $d[0] * 10 + $d[1];
    $month = $d[2] * 10 + $d[3];
    $day = $d[4] * 10 + $d[5];

    if ($month > 40) {
        [$year, $month] = [$year + 2000, $month - 40];
    } elseif ($month > 20) {
        [$year, $month] = [$year + 1800, $month - 20];
    } else {
        $year += 1900;
    }
    if (! checkdate($month, $day, $year)) {
        return false;
    }

    $sum = 0;
    foreach ([2, 4, 8, 5, 10, 9, 7, 3, 6] as $i => $weight) {
        $sum += $d[$i] * $weight;
    }

    return $sum % 11 % 10 === $d[9];
}

We ran this function against 3,000 synthetic EGNs with dates from 1906 to 2033: it accepted all of them and rejected every one whose last digit we changed. Add your own rules on top: most services should also reject birth dates in the future, and some need a minimum age.

#Turning it into a Laravel rule

Since Laravel 10, a custom rule is a class with a validate method. php artisan make:rule Egn creates the file:

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class Egn implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (! is_string($value) || ! egn_valid($value)) {
            $fail('The :attribute is not a valid EGN.');
        }
    }
}

Use it like any other rule: 'egn' => ['required', new Egn]. Put the message in your Bulgarian language file as well, because the people filling in these fields usually read Bulgarian.

#LNCh: the number of a foreigner

The LNCh also has ten digits, but no date inside. Its check digit is the sum of the first nine digits multiplied by 21, 19, 17, 13, 11, 9, 7, 3 and 1, modulo 10. Many forms label one field "ЕГН/ЛНЧ"; in that case accept a value that passes either check.

#EIK and BULSTAT

A company's EIK has 9 digits; branches have 13. For the 9-digit number, multiply the first eight digits by 1 to 8 and take the sum modulo 11. If the result is 10, repeat with the weights 3 to 10; if that is 10 again, the check digit is 0:

function eik9_valid(string $eik): bool
{
    if (! preg_match('/^\d{9}$/', $eik)) {
        return false;
    }
    $d = array_map('intval', str_split($eik));
    $sum = 0;
    for ($i = 0; $i < 8; $i++) {
        $sum += $d[$i] * ($i + 1);
    }
    $check = $sum % 11;
    if ($check === 10) {
        $sum = 0;
        for ($i = 0; $i < 8; $i++) {
            $sum += $d[$i] * ($i + 3);
        }
        $check = $sum % 11 % 10;
    }

    return $check === $d[8];
}

The 13-digit number is the 9-digit EIK followed by a branch number and a second check digit, calculated over digits 9 to 12 with its own two sets of weights.

#Bulgarian VAT numbers

A Bulgarian VAT number is BG followed by the 9-digit EIK of a company, or by 10 digits for a person: an EGN, an LNCh or a number issued to another foreigner. Strip the prefix, then run the matching check. A valid format is not a valid registration, though: whether the number is registered for VAT today is answered only by the European Commission's VIES service, which you should query when the answer changes your invoice, for example for a reverse charge.

#IBAN

A Bulgarian IBAN has 22 characters: BG, two check digits, a four-letter bank code, four digits for the branch, two for the account type and eight characters for the account. The check is the ISO 13616 mod 97 test: move the first four characters to the end, replace each letter with a number (A is 10, B is 11 and so on) and the remainder modulo 97 must be 1:

function bg_iban_valid(string $iban): bool
{
    $iban = strtoupper(str_replace(' ', '', $iban));
    if (! preg_match('/^BG\d{2}[A-Z]{4}\d{6}[A-Z0-9]{8}$/', $iban)) {
        return false;
    }
    $moved = substr($iban, 4).substr($iban, 0, 4);
    $digits = preg_replace_callback('/[A-Z]/', fn ($m) => (string) (ord($m[0]) - 55), $moved);
    $rest = 0;
    foreach (str_split($digits) as $digit) {
        $rest = ($rest * 10 + (int) $digit) % 97;
    }

    return $rest === 1;
}

The digit-by-digit loop avoids integer overflow, since the number is far longer than PHP's integers.

#What an offline check cannot tell you

Every check above runs on your own server, with no network request, and confirms one thing: the number is well formed and its check digit matches. It does not say that the EGN belongs to the person typing it, that a company with that EIK exists, that the VAT number is registered or that the bank account is open. Use the checksum to stop typing mistakes early; use the Commercial Register, VIES or your bank when you need the second kind of answer.

Treat EGNs as personal data under the GDPR. Collect them only when you need them, and never use real ones in tests or seeders: generate synthetic, checksum-valid numbers instead.

#A ready package: Proverka

We needed these rules in client projects often enough to package them as Proverka, Bulgarian validation for Laravel 12 and 13. It checks the EGN with its date, century and check digit, the LNCh, a combined EGN or LNCh field, 9 and 13 digit EIK/BULSTAT numbers, VAT numbers, IBANs, phone numbers and postal codes. Each check exists as a rule object with options, such as Rules\Egn::make()->female()->minAge(18), and as a string rule such as bg_egn:female,18, with English and Bulgarian messages. Helpers read the birth date, sex and age from an EGN, format IBANs, and turn phone numbers typed in any notation into E.164 for storage; generators build synthetic numbers for your factories and tests.

Proverka is covered by 70 PHPUnit and Orchestra Testbench tests on Laravel 13.35 and 12.69, and its release package was installed into a fresh Laravel application and checked through a real form. It is listed as Coming soon.

#Frequently asked questions

Does a valid check digit prove that an EGN is real?

No. It proves the number is well formed. About one random ten-digit number in ten passes the checksum alone, and a well-formed number may still not be issued or may belong to someone else.

Why does an EGN of someone born in 2005 have a month of 41 to 52?

Because the century is stored in the month: 40 is added for births from 2000 to 2099, and 20 for births in the nineteenth century.

Should I check VAT numbers in VIES on every form submission?

Validate the format and checksum on every submission, because it is instant and offline. Query VIES when the answer matters, for example before issuing an invoice without VAT, and keep a record of the result.

Can I use these checks for numbers from other countries?

The IBAN mod 97 test works for every country's IBAN; the length and layout differ. The EGN, LNCh and EIK rules are Bulgarian only.

#Sources

Keep reading

Keep reading

Comments

Comments

Be the first to leave a comment.

Leave a comment

Next step

pnscripts.com/contact

Talk to the team

Ask about an article, or tell us about a project you want built. We reply within one business day.

Write to us

The PN Scripts family

Other PN Scripts sites

Hosting, games and the blog each have their own site, run by the same company.

  • pnscripts.com

    PN Scripts

    Software engineering

    Custom web, mobile, API and game development, plus our open-source products and plugins.

  • games.pnscripts.com

    Games

    Games and game servers

    The home for PN Scripts games: free browser games you can play right now, with game servers coming soon.

  • hosting.pnscripts.com

    Hosting

    Hosting and infrastructure

    Shared hosting, KVM VPS, dedicated servers and domains, from the same company that builds your project.

  • blog.pnscripts.com

    Blog

    Articles and field notes

    Practical articles on software development, hosting, open source and games, written by the people who build them.

    You are here