# Integrate EU KYC verification for the Embedded Components onramp Collect MiCA identifiers, CARF TINs, and complete L2 verification for EU users. ## Before you begin Before you begin, [integrate the Embedded Components Onramp](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md) (including Link authentication) and review the [KYC tier system](https://docs.stripe.com/crypto/onramp/kyc-integration-guide.md). In the European Union (EU), users must complete additional identity verification steps beyond the standard KYC tiers before they can transact. Two EU regulations drive these requirements: - **CRS/CARF**: Common Reporting Standard and Crypto-Asset Reporting Framework. This requires a Tax Identification Number (TIN) for every EU country where the user is a tax resident, plus a signed attestation that the information provided is accurate. The user self-declares tax residency. - **MiCA**: Markets in Crypto-Assets Regulation. This requires a national identifier for each nationality or country of residence that is one of the following countries: Estonia (EE), Spain (ES), Iceland (IS), Italy (IT), Malta (MT), or Poland (PL). `L2` verification (document and selfie) is mandatory for all EU users. A user can’t transact until they complete `L2` and all EU-specific identifier requirements. See [Determining a user’s current KYC tier](https://docs.stripe.com/crypto/onramp/kyc-integration-guide.md#determining-a-users-current-kyc-tier) for details about tier statuses. ## EU signup flow overview The full EU verification flow is: 1. For existing users, check if EU KYC collection is needed by inspecting `kyc_region`, `verifications`, and `kyc_tiers` on the [CryptoCustomer object](https://docs.stripe.com/api/crypto/customers/object.md) 1. Collect basic KYC info including **nationalities** and submit with `submitKycInfo` 1. Call `getMissingIdentifiers` to determine which MiCA identifiers are needed and whether CARF TINs are required 1. If `carf_tin_required` is `true`, collect TINs from the user for their countries of tax residency. Collect any MiCA identifiers from the `identifiers` array. Submit all with `updateKycInfo` 1. Present the Stripe Terms of Service (ToS) with `promptUserAttestation` 1. Complete identity verification (document + selfie) with `verifyDocuments` ## Step 1: Check if EU KYC collection is needed (existing users) [Server-side] For existing users, [retrieve the CryptoCustomer](https://docs.stripe.com/api/crypto/customers/retrieve.md) and inspect the response to determine if the EU-specific flow is needed. First, check `kyc_region`—this value is derived from the user’s country of residence and indicates whether the user is subject to EU or US KYC requirements. If `kyc_region` isn’t `eu`, this flow doesn’t apply. `kyc_region` is `null` if the user hasn’t yet submitted basic KYC information ([Step 2](https://docs.stripe.com/crypto/onramp/eu-kyc-integration-guide.md#step-2-submit-basic-kyc-info)). Then check the `kyc_tiers` array for the `l2` entry: - If `l2` has `verification_status: "verified"`, the user is fully verified and can transact. Skip the remaining steps. - If `l2` has `verification_status: "not_started"` or `"pending"`, verify that `kyc_verified`, `id_document_verified`, and `phone_verified` all have status `verified` in the `verifications` array before you create successful transactions. These are prerequisites for the EU KYC flow. ```javascript const response = await fetch( `https://api.stripe.com/v1/crypto/customers/${customerId}`, { headers: { 'Authorization': `Bearer ${process.env.STRIPE_SECRET_KEY}`, 'Stripe-OAuth-Token': oauthToken, 'Stripe-Version': '2026-03-25.dahlia;crypto_onramp_beta=v2', }, } ); const customer = await response.json(); const l2Tier = (customer.kyc_tiers ?? []).find((t) => t.tier === 'l2'); if (l2Tier?.verification_status === 'verified') { // User is fully verified — no EU KYC flow needed } if (l2Tier?.verification_status === 'not_started' || l2Tier?.verification_status === 'pending') { // Check prerequisites: kyc_verified, id_document_verified, phone_verified // must all be "verified" in the verifications array before proceeding } ``` ## Step 2: Submit basic KYC info [Client-side] For EU-based users, `submitKycInfo` requires the `nationalities`, `birth_city`, and `birth_country` fields in addition to the standard fields (name, DOB, address). The user’s nationalities and country of residence together determine which MiCA identifiers are required. The `state` field is not required for EU addresses, except for Ireland (IE). ```javascript await onramp.submitKycInfo({ given_name: 'Maria', surname: 'Papadopoulos', date_of_birth: { // Object with numeric fields, not a date string. day: 15, // Day of month (1-31). month: 3, // Month of year (1-12). year: 1990, // Full 4-digit year. }, address: { line1: '123 Example Street', city: 'Athens', // Free-text city name. postal_code: '10557', country: 'GR', // ISO 3166-1 alpha-2 country code. }, nationalities: ['GR', 'EE'], // Array of ISO 3166-1 alpha-2 country codes. birth_city: 'Athens', birth_country: 'GR', }); ``` ### Errors | Error | Message | | ----------------------- | ----------------------------------------------------------------------------------------------------- | | `MISSING_NATIONALITIES` | `nationalities` is required for EU-based users. Provide at least one ISO 3166-1 alpha-2 country code. | | `INVALID_REQUEST_ERROR` | Invalid value for parameter `{param}`. | | `UNAUTHENTICATED_USER` | User has not authenticated. | | `RATE_LIMIT_EXCEEDED` | The request was rate limited. | ## Step 3: Get missing identifiers [Client-side] After submitting basic KYC info, call `getMissingIdentifiers` to determine which identifiers the user still needs to provide. The response contains: - **`carf_tin_required`**: A boolean indicating whether the user still needs to provide at least one CARF TIN. - **`identifiers`**: MiCA national identifier requirements (derived from the user’s nationalities and residence country). - **`alternatives`**: Alternative options for MiCA identifiers (for example, passport instead of national ID for Malta). When `carf_tin_required` is `true`, collect a TIN for **every** country where the user is tax resident — users can be tax resident in multiple countries. The user provides each country of tax residency and the corresponding TIN value; use the [Identifier types reference](https://docs.stripe.com/crypto/onramp/eu-kyc-integration-guide.md#identifier-types-reference) for the country-to-type mapping (for example, DE to `de_stn`). Collect any required MiCA identifiers from the `identifiers` array at the same time — these are determined by the user’s nationalities and residence country. ```javascript const requirements = await onramp.getMissingIdentifiers(); // { // carf_tin_required: true, // identifiers: [ // { type: 'ee_ik', regulation: 'eu_mica' } // ], // alternatives: [] // } ``` In this example, `ee_ik` is required because the user has Estonian nationality, which is a MiCA country. `carf_tin_required` is `true` because the user hasn’t yet provided a TIN for their tax residency. You must collect the TIN from the user and map it to the corresponding identifier type. Each entry in `identifiers` contains: | Field | Description | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | The identifier type code (see [Identifier types reference](https://docs.stripe.com/crypto/onramp/eu-kyc-integration-guide.md#identifier-types-reference)) | | `regulation` | The regulation requiring this identifier (always `eu_mica` in the `identifiers` array) | If a CARF TIN is submitted for a MiCA country, it only needs to be submitted once. For example, if a user is both an Estonian national and an Estonian tax resident, submitting `ee_ik` satisfies both MiCA and CARF for Estonia. ### Alternative identifiers For some countries (currently Malta and Poland), MiCA accepts an alternative identifier. When alternatives exist, they appear in the `alternatives` array: ```javascript // Maltese national living in Germany await onramp.submitKycInfo({ address: { country: 'DE' }, nationalities: ['MT'] }); const requirements = await onramp.getMissingIdentifiers(); // { // carf_tin_required: true, // identifiers: [ // { type: 'mt_nic', regulation: 'eu_mica' } // ], // alternatives: [ // { // original_missing_identifiers: ['mt_nic'], // alternative_missing_identifiers: ['mt_pp'] // } // ] // } ``` In this example, `carf_tin_required` is `true` because the user needs to provide a TIN for their tax residency (Germany in this case — the developer should collect `de_stn`). For the MiCA requirement, the user can provide either `mt_nic` (Malta national identity card) **or** `mt_pp` (Malta passport). Use the `alternatives` array to present the user with a choice between the two. ### Errors | Error | Message | | ------------------------ | ---------------------------------------------------------- | | `KYC_INFO_NOT_SUBMITTED` | Basic KYC info has not been submitted via `submitKycInfo`. | | `UNAUTHENTICATED_USER` | User has not authenticated. | | `RATE_LIMIT_EXCEEDED` | The request was rate limited. | ## Step 4: Submit identifiers [Client-side] Before calling `updateKycInfo`, validate identifiers client-side using the regex patterns and structure rules documented in the [Identifier validation logic](https://docs.stripe.com/crypto/onramp/eu-kyc-integration-guide.md#appendix-identifier-validation-logic) appendix. Client-side validation lets you provide immediate feedback to users about formatting errors (for example, wrong length or invalid characters) without a round trip. Submit the collected identifiers (both MiCA and CARF) with `updateKycInfo`. When `completed` is `true`, all identifier requirements are satisfied and you can proceed to the next step (attestation). ```javascript const result = await onramp.updateKycInfo([ { type: 'gr_afm', value: '123456789' }, { type: 'ee_ik', value: '39901011234' }, ]); if (result.completed) { // All identifier requirements satisfied — proceed to attestation } else { // Prompt the user to correct invalid identifiers or provide remaining ones } ``` The response contains: | Field | Description | | --------------------- | -------------------------------------------------------------------------------------------- | | `completed` | `true` when all identifier requirements (MiCA + CARF) are satisfied — proceed to attestation | | `carf_tin_required` | `true` if the user still needs to provide at least one CARF TIN | | `identifiers` | Remaining missing MiCA identifiers (same shape as `getMissingIdentifiers`) | | `alternatives` | Remaining alternative options for missing MiCA identifiers | | `invalid_identifiers` | Identifier types that were submitted but rejected (for example, wrong format) | Example response when all requirements are met: ```javascript // { // completed: true, // carf_tin_required: false, // identifiers: [], // alternatives: [], // invalid_identifiers: [] // } ``` Example response when identifiers are invalid or missing: ```javascript // { // completed: false, // carf_tin_required: false, // identifiers: [{ type: 'ee_ik', regulation: 'eu_mica' }], // alternatives: [], // invalid_identifiers: ['ee_ik'] // } ``` ### Errors | Error | Message | | ----------------------- | -------------------------------------- | | `INVALID_REQUEST_ERROR` | Invalid value for parameter `{param}`. | | `UNAUTHENTICATED_USER` | User has not authenticated. | | `RATE_LIMIT_EXCEEDED` | The request was rate limited. | ## Step 5: Stripe Terms of Service [Client-side] After all identifiers are submitted (`completed: true`), present the Stripe Terms of Service declaration for the user to review and accept. This method returns a mountable `HTMLElement` that renders a Stripe-hosted iframe. The `onCompletion` callback fires when the user accepts or abandons the declaration. ```javascript const element = await onramp.promptUserAttestation('eu_carf', ({ result }) => { // result === 'confirmed' — user accepted, proceed to identity verification // result === 'abandoned' — user closed without confirming }); document.getElementById('attestation-container').replaceChildren(element); ``` You must call `updateKycInfo` and have `completed: true` **before** calling `promptUserAttestation`. If identifiers are incomplete, the SDK throws a `MissingEuIdentifiers` error. ### Errors | Error | Message | | ------------------------ | ----------------------------------------------------------------- | | `MISSING_EU_IDENTIFIERS` | EU identifiers have not been fully submitted via `updateKycInfo`. | | `INCOMPLETE_DECLARATION` | User abandoned the attestation without confirming. | | `INVALID_REQUEST_ERROR` | The provided regulation value is not valid. | | `UNAUTHENTICATED_USER` | User has not authenticated. | | `RATE_LIMIT_EXCEEDED` | The request was rate limited. | ## Step 6: Complete identity verification [Client-side] After identifiers and attestation are complete, call `verifyDocuments` to present a Stripe-hosted flow where the user uploads an identity document and a selfie. ```javascript await onramp.verifyDocuments(); // User is fully verified and can transact ``` ### Errors | Error | Message | | ---------------------- | ----------------------------- | | `UNAUTHENTICATED_USER` | User has not authenticated. | | `RATE_LIMIT_EXCEEDED` | The request was rate limited. | ## Complete EU flow example ```javascript import { loadCryptoOnrampAndInitialize } from '@stripe/crypto'; const onramp = await loadCryptoOnrampAndInitialize('pk_test_12345'); // ... authenticate user (see Authentication section) ... // 1. Submit basic KYC info with nationalities await onramp.submitKycInfo({ given_name: 'Maria', surname: 'Papadopoulos', date_of_birth: { year: 1990, month: 3, day: 15 }, address: { line1: '123 Example Street', city: 'Athens', postal_code: '10557', country: 'GR', }, nationalities: ['GR', 'EE'], birth_city: 'Athens', birth_country: 'GR', }); // 2. Check what identifiers are needed const requirements = await onramp.getMissingIdentifiers(); // requirements.carf_tin_required — whether the user needs to provide CARF TINs // requirements.identifiers — MiCA identifiers the user must provide // 3. Collect and submit identifiers const result = await onramp.updateKycInfo([ { type: 'ee_ik', value: '39901011234' }, // MiCA identifier for Estonian nationality { type: 'gr_afm', value: '123456789' }, // CARF TIN for Greek tax residency ]); // result.completed — true when all requirements are satisfied // 4. Present Stripe ToS const element = await onramp.promptUserAttestation('eu_carf', ({ result }) => { // result === 'confirmed' or 'abandoned' }); document.getElementById('attestation-container').replaceChildren(element); // 5. Complete identity verification (document + selfie) await onramp.verifyDocuments(); ``` ## Identifier types reference Identifier type codes follow the `{country_code}_{abbreviation}` convention, aligned with the [V2 Persons API `id_numbers.type` enum](https://docs.stripe.com/api/v2/core/persons/create.md#v2_create_persons-id_numbers-type). ### MiCA identifiers National identifiers required for users whose nationality or country of residence is one of: **EE, ES, IS, IT, MT, PL**. | Type | Country | Name | | ---------- | ------- | ----------------------------------------- | | `ee_ik` | Estonia | Isikukood (PIC) | | `es_nif` | Spain | Tax Identification Number (NIF) | | `is_kt` | Iceland | Kennitala (PIC) | | `it_cf` | Italy | Codice fiscale | | `mt_nic` | Malta | National Identity Card Number | | `mt_pp` | Malta | Passport Number (alternative to `mt_nic`) | | `pl_pesel` | Poland | PESEL number | | `pl_nip` | Poland | NIP (alternative to `pl_pesel`) | For MT and PL, an alternative identifier (passport or NIP respectively) can be used instead of the primary national identifier. Overlap with CARF: `ee_ik`, `es_nif`, `it_cf`, `mt_nic`, and `pl_pesel` also serve as TINs for their respective countries. If a user’s CARF TIN type matches a required MiCA type, submitting it once satisfies both regulations. Iceland (`is_kt`) doesn’t overlap — it is EEA (not EU), so it is MiCA-only and not a valid CARF country. ### CARF identifiers Tax Identification Numbers (TINs) for countries of tax residency. Required when `carf_tin_required` is `true`. The developer must collect TINs from the user for their self-declared countries of tax residency, then submit the corresponding identifier type. See the [Appendix](https://docs.stripe.com/crypto/onramp/eu-kyc-integration-guide.md#appendix-identifier-validation-logic) for format details and validation rules. | Type | Country | Name | | ---------- | -------------- | ----------------------------------------- | | `at_stn` | Austria | Steuernummer | | `be_nrn` | Belgium | National Registration Number (NRN) | | `bg_ucn` | Bulgaria | Unified Civil Number | | `hr_oib` | Croatia | Osobni identifikacijski broj (OIB) | | `cy_tic` | Cyprus | Tax Identification Code (TIC) | | `cz_rc` | Czech Republic | Rodne cislo | | `dk_cpr` | Denmark | Personnummer (CPR) | | `ee_ik` | Estonia | Isikukood (PIC) | | `es_nif` | Spain | Tax Identification Number (NIF) | | `fi_hetu` | Finland | Henkilotunnus (HETU) | | `fr_spi` | France | Numero fiscal de reference (SPI) | | `fr_nir` | France | NIR (Social Security Number) | | `de_stn` | Germany | Steuer-ID | | `gr_afm` | Greece | Tax Identification Number (AFM) | | `hu_ad` | Hungary | Adoazonasito | | `ie_ppsn` | Ireland | Personal Public Service Number (PPSN) | | `it_cf` | Italy | Codice fiscale | | `lv_pk` | Latvia | Personas kods | | `lt_ak` | Lithuania | Asmens kodas | | `lu_nif` | Luxembourg | Numero d’Identification Personnelle (NIF) | | `mt_nic` | Malta | National Identity Card Number | | `nl_bsn` | Netherlands | Citizen Service Number (BSN) | | `pl_pesel` | Poland | PESEL number | | `pt_nif` | Portugal | Numero de Identificacao Fiscal (NIF) | | `ro_cnp` | Romania | Codul Numeric Personal (CNP) | | `sk_rc` | Slovakia | Rodne cislo | | `si_pin` | Slovenia | Personal Identification Number (EMSO) | | `se_pin` | Sweden | Personnummer (PIN) | ### Distinguish CARF from MiCA - **CARF/CRS**: Tax Identification Numbers (TINs) for countries of **tax residency**. Stripe supports these for all 27 EU member states. The user self-declares tax residency, and the API can’t determine it automatically, so it surfaces CARF as a boolean flag (`carf_tin_required`). - **MiCA**: National or country identifiers for specific countries (**EE, ES, IS, IT, MT, PL**) based on nationality or residence. The API returns these as specific types in the `identifiers` array. Iceland (`IS`) is in the EEA, but not in the EU, so it requires a MiCA identifier but isn’t a valid CARF country. - **Overlap type** (`it_cf`, `ee_ik`, `es_nif`, `mt_nic`, `pl_pesel`): For these countries, the national identifier and CARF TIN are the same. A single submission satisfies both regulations. If the user’s CARF TIN type matches a required MiCA type, submit it only once. ## Appendix: Identifier validation logic All identifiers are first stripped of whitespace. Hyphens, slashes, and spaces are stripped before checksum computation unless noted otherwise. Validation proceeds in three stages: (1) regex pattern match, (2) structural validation (date fields, prefixes), (3) checksum verification. ### MiCA validation algorithms #### Estonia (ee_ik) — 11 digits, dual-weight mod-11 checksum When `ee_ik` is submitted as a CARF TIN (for Estonian tax residency), additional structure validation (century digit, month, day, serial range) is applied — see the [CARF validation section](https://docs.stripe.com/crypto/onramp/eu-kyc-integration-guide.md#carf-validation-algorithms). ``` Regex: /^\d{11}$/ Checksum: weights1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 1] sum = Σ(digit[i] * weights1[i]) for i=0..9 remainder = sum % 11 if remainder == 10: weights2 = [3, 4, 5, 6, 7, 8, 9, 1, 2, 3] sum = Σ(digit[i] * weights2[i]) for i=0..9 remainder = sum % 11 if remainder == 10: remainder = 0 check_digit = remainder Valid if check_digit == digit[10] ``` #### Spain (es_nif) — 9 chars, modulo-23 check letter ``` Regex: /^([KLMXYZ]?\d{7}[A-Za-z]|\d{8}[A-Za-z])$/ Checksum: check_letters = "TRWAGMYFPDXBNJZSQVHLCKE" If first char is X/Y/Z/K/L/M: replace with 0/1/2/0/0/0, prepend to digits 1-7 If first char is digit: use digits 0-7 numeric_value = integer of the 8-digit string remainder = numeric_value % 23 Valid if check_letters[remainder] == last char (position 8) ``` #### Iceland (is_kt) — 10 digits, date structure validation ``` Regex: /^\d{10}$/ Structure: - Digits 0-1: day (01-31) - Digits 2-3: month (01-12) - Digits 4-5: year (last 2 digits) - Digit 9: century indicator (9=1900s, 0=2000s) No checksum — validation is date structure only. ``` #### Italy (it_cf) — 16 alphanumeric, odd-even positional checksum ``` Regex: /^[A-Za-z]{6}\d{2}[A-Za-z]\d{2}[A-Za-z]\d{3}[A-Za-z]$/ Structure: - Position 8 (0-indexed): month letter from "ABCDEHLMPRST" - Positions 9-10: day (01-31 for males, 41-71 for females) Checksum: odd_values = {0:1, 1:0, 2:5, 3:7, 4:9, 5:13, 6:15, 7:17, 8:19, 9:21, A:1, B:0, C:5, D:7, E:9, F:13, G:15, H:17, I:19, J:21, K:2, L:4, M:18, N:20, O:11, P:3, Q:6, R:8, S:12, T:14, U:16, V:10, W:22, X:25, Y:24, Z:23} even_values = {0:0, 1:1, ..., 9:9, A:0, B:1, ..., Z:25} sum = 0 for i = 0..14: if (i+1) is odd: sum += odd_values[char[i]] else: sum += even_values[char[i]] expected = chr(65 + (sum % 26)) Valid if expected == char[15] ``` #### Malta national ID (mt_nic) — 8 chars or 9 digits ``` Regex: /^\d{7}[MGAPLHBZ]$|^\d{9}$/ Validation: If 8 chars: last char must be one of M, G, A, P, L, H, B, Z → valid If 9 digits: first 2 digits must be one of: 11, 22, 33, 44, 55, 66, 77, 88 ``` #### Malta passport (mt_pp) — 7 digits ``` Regex: /^\d{7}$/ No additional validation. ``` #### Poland PESEL (pl_pesel) — 11 digits, weighted checksum ``` Regex: /^\d{11}$/ Structure: - Digits 2-3: month (01-12, 21-32, 41-52, 61-72, or 81-92 for different centuries) - Digits 4-5: day (01-31) Checksum: weights = [1, 3, 7, 9, 1, 3, 7, 9, 1, 3] sum = Σ((digit[i] * weights[i]) % 10) for i=0..9 check_digit = (10 - (sum % 10)) % 10 Valid if check_digit == digit[10] ``` #### Poland NIP (pl_nip) — 10 digits, weighted mod-11 checksum ``` Regex: /^\d{10}$/ Checksum: weights = [6, 5, 7, 2, 3, 4, 5, 6, 7] sum = Σ(digit[i] * weights[i]) for i=0..8 remainder = sum % 11 Invalid if remainder == 10 Valid if remainder == digit[9] ``` ### CARF validation algorithms #### Austria (at_stn) — 9 digits, Luhn-variant checksum ``` Regex: /^\d{2}-?\d{3}\/?\d{4}$/ Checksum (after stripping to 9 digits): weights = [1, 2, 1, 2, 1, 2, 1, 2] sum = Σ(sum_digits(digit[i] * weights[i])) for i=0..7 where sum_digits(n) = n if n<10, else (n/10)+(n%10) check_digit = (100 - sum) % 10 Valid if check_digit == digit[8] ``` #### Belgium (be_nrn) — 11 digits, mod-97 checksum ``` Regex: /^\d{11}$/ Structure: - Digits 2-3: month (≤12) - Digits 4-5: day (≤31) Checksum: base = first 9 digits as integer check = last 2 digits as integer Try 1: (97 - (base % 97)) == check → valid Try 2 (born after 2000): prepend "2" → (97 - ("2"+base % 97)) == check → valid ``` #### Bulgaria (bg_ucn) — 10 digits, weighted checksum ``` Regex: /^\d{10}$/ Structure: - Digits 2-3: month (1-12, 21-32, or 41-52) - Digits 4-5: day (1-31) Checksum: weights = [2, 4, 8, 5, 10, 9, 7, 3, 6] sum = Σ(digit[i] * weights[i]) for i=0..8 check_digit = sum % 11 if check_digit == 10: check_digit = 0 Valid if check_digit == digit[9] ``` #### Croatia (hr_oib) — 11 digits, ISO 7064 mod-11,10 ``` Regex: /^\d{11}$/ Checksum: rest = 10 for i = 0..9: s = (digit[i] + rest) % 10 if s == 0: s = 10 rest = (s * 2) % 11 check_digit = 11 - rest if check_digit == 10: check_digit = 0 Valid if check_digit == digit[10] ``` #### Cyprus (cy_tic) — 9 chars, positional odd-even checksum ``` Regex: /^[069]\d{7}[A-Za-z]$/ Checksum: First char must be 0, 6, or 9 odd_values = {0:1, 1:0, 2:5, 3:7, 4:9, 5:13, 6:15, 7:17, 8:19, 9:21} even_sum = sum of digits at even positions (2nd, 4th, 6th, 8th) odd_sum = sum of odd_values[char] at odd positions (1st, 3rd, 5th, 7th) total = even_sum + odd_sum check_char = chr(65 + (total % 26)) Valid if check_char == char[8] ``` #### Czech Republic (cz_rc) — 9-10 digits, date structure ``` Regex: /^\d{6}\/?\d{3,4}$/ Structure (after stripping /): - Digits 2-3: month (1-12 or 51-62 for 9-digit; also 21-32 or 71-82 for 10-digit) - Digits 4-5: day (1-31) No checksum. ``` #### Denmark (dk_cpr) — 10 digits, weighted checksum ``` Regex: /^\d{6}-?\d{4}$/ Structure: - Digits 0-1: day (1-31) - Digits 2-3: month (1-12) Checksum (after stripping to 10 digits): weights = [4, 3, 2, 7, 6, 5, 4, 3, 2] sum = Σ(digit[i] * weights[i]) for i=0..8 remainder = sum % 11 Invalid if remainder == 1 check_digit = 0 if remainder == 0, else 11 - remainder Valid if check_digit == digit[9] ``` #### Estonia (ee_ik as CARF) — 11 digits, structure + dual-weight mod-11 checksum When submitted as a CARF TIN, `ee_ik` undergoes additional structure validation beyond the MiCA checksum-only validation. ``` Regex: /^\d{11}$/ Structure: - Digit 1: century/sex indicator (1-6) - Digits 4-5: month (01-12) - Digits 6-7: day (01-31) - Digits 8-10: serial (001-710) Checksum: same as MiCA (dual-weight mod-11) ``` #### Finland (fi_hetu) — 11 chars, mod-31 check char ``` Regex: /^(0[1-9]|[12]\d|3[01])(0[1-9]|1[0-2])\d{2}[+\-A-FU-Y]\d{3}[A-Za-z0-9]$/ Checksum: check_chars = "0123456789ABCDEFHJKLMNPRSTUVWXY" combined = (digits[0..5] + digits[7..9]) as integer (date + serial) remainder = combined % 31 Valid if check_chars[remainder] == char[10] ``` #### France (fr_spi, fr_nir) — 13 digits, mod-511 checksum ``` Regex: /^[0-3]\d{12}$/ Checksum: First digit must be 0-3 base = first 10 digits as integer check_digits = last 3 digits as string remainder = base % 511 expected = remainder zero-padded to 3 chars Valid if check_digits == expected ``` #### Germany (de_stn) — 11 or 13 digits, iterative mod-11 ``` Regex: /^\d{11}$|^\d{13}$/ For 13 digits: valid if digit[4] == '0' For 11 digits: Structure: - First digit must not be 0 - Among first 10 digits: at least one digit appears 2 or 3 times - No digit appears 3 times consecutively Checksum: x = 10 for i = 0..9: s = (digit[i] + x) % 10 if s == 0: s = 10 x = (s * 2) % 11 check_digit = 11 - x if check_digit == 10: check_digit = 0 Valid if check_digit == digit[10] ``` #### Greece (gr_afm) — 9 digits, regex only ``` Regex: /^\d{9}$/ No additional checksum validation. ``` #### Hungary (hu_ad) — 10 digits, weighted mod-11 ``` Regex: /^8\d{9}$/ Checksum: First digit must be 8 weights = [1, 2, 3, 4, 5, 6, 7, 8, 9] sum = Σ(digit[i] * weights[i]) for i=0..8 check_digit = sum % 11 Valid if check_digit == digit[9] ``` #### Ireland (ie_ppsn) — 8-9 chars, weighted mod-23 check letter ``` Regex: /^\d{7}[A-Wa-w][A-IWa-iw]?$/ Checksum: weights = [8, 7, 6, 5, 4, 3, 2] If 9 chars: ninth_value = (char[8].ord - 64), except W which maps to 0; else ninth_value = 0 sum = ninth_value * 9 sum += Σ(digit[i] * weights[i]) for i=0..6 remainder = sum % 23 expected = 'W' if remainder == 0, else chr(64 + remainder) Valid if expected == char[7] ``` #### Latvia (lv_pk) — 11 digits, date structure ``` Regex: /^(0[1-9]|[12]\d|3[01])(0[0-9]|1[0-2])\d{7}$|^32\d{9}$/ Structure: If starts with "32": new-format personal code → valid (no date check) Otherwise: - Digits 0-1: day (1-31) - Digits 2-3: month (≤12) - Digit 6: century indicator (0, 1, or 2) No checksum. ``` #### Lithuania (lt_ak) — 11 digits, dual-weight mod-11 ``` Regex: /^\d{11}$/ Structure: - Digit 0: century/sex (1-6) - Digits 3-4: month (1-12) - Digits 5-6: day (1-31) Checksum: weights1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 1] sum = Σ(digit[i] * weights1[i]) for i=0..9 remainder = sum % 11 if remainder == 10: weights2 = [3, 4, 5, 6, 7, 8, 9, 1, 2, 3] sum = Σ(digit[i] * weights2[i]) for i=0..9 remainder = sum % 11 if remainder == 10: remainder = 0 Valid if remainder == digit[10] ``` #### Luxembourg (lu_nif) — 13 digits, Luhn + Verhoeff ``` Regex: /^\d{13}$/ Structure: - Digits 0-3: year (1800-2100) - Digits 4-5: month (1-12) - Digits 6-7: day (1-31) Checksum (two-part): Part 1 — Luhn on first 12 digits: weights = [2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1] sum = Σ(sum_digits(digit[i] * weights[i])) for i=0..11 Valid part 1 if sum % 10 == 0 Part 2 — Verhoeff on digits 0-10 + digit 12: Uses Dihedral group D5 multiplication table and permutation table Valid part 2 if final accumulator == 0 ``` #### Malta (mt_nic as CARF) — 8 chars or 9 digits ``` Regex: /^\d{7}[MGAPLHBZ]$|^\d{9}$/ Structure: If 8 chars: last char is one of M, G, A, P, L, H, B, Z → valid If 9 digits: prefix (first 2 digits) must be 11, 22, 33, 44, 55, 66, 77, or 88 No checksum. ``` #### Netherlands (nl_bsn) — 9 digits, weighted mod-11 ``` Regex: /^\d{9}$/ Checksum: weights = [9, 8, 7, 6, 5, 4, 3, 2] sum = Σ(digit[i] * weights[i]) for i=0..7 remainder = sum % 11 Invalid if remainder == 10 Valid if remainder == digit[8] ``` #### Poland (pl_pesel or pl_nip as CARF) Same algorithms as MiCA section above. CARF accepts either format (11 digits for PESEL, 10 digits for NIP) and runs both checksums — passes if either is valid. #### Portugal (pt_nif) — 9 digits, weighted mod-11 ``` Regex: /^\d{9}$/ Checksum: weights = [9, 8, 7, 6, 5, 4, 3, 2] sum = Σ(digit[i] * weights[i]) for i=0..7 check_digit = 11 - (sum % 11) if check_digit >= 10: check_digit = 0 Valid if check_digit == digit[8] ``` #### Romania (ro_cnp) — 13 digits, weighted checksum ``` Regex: /^\d{13}$/ Structure: - Digit 0: sex/century (1-9; if 9 + digits[1..3]=="000" → foreign resident, skip date) - Digits 3-4: month (1-12) - Digits 5-6: day (1-31) - Digits 7-8: county code (1-47, 51, or 52) Checksum: weights = [2, 7, 9, 1, 4, 6, 3, 5, 8, 2, 7, 9] sum = Σ(digit[i] * weights[i]) for i=0..11 check_digit = sum % 11 if check_digit == 10: check_digit = 1 Valid if check_digit == digit[12] ``` #### Slovakia (sk_rc) — 9-10 digits, date structure ``` Regex: /^\d{6}\/?\d{3,4}$/ Structure (after stripping /): - Digits 2-3: month (1-12 or 51-62) - Digits 4-5: day (1-31) No checksum. ``` #### Slovenia (si_pin) — 8 digits, weighted mod-11 ``` Regex: /^\d{8}$/ Structure: First 7 digits must form a number between 1000000 and 9999999 Checksum: weights = [8, 7, 6, 5, 4, 3, 2] sum = Σ(digit[i] * weights[i]) for i=0..6 check_digit = 11 - (sum % 11) if check_digit == 10: check_digit = 0 Invalid if check_digit == 11 Valid if check_digit == digit[7] ``` #### Sweden (se_pin) — 10 or 12 digits, Luhn checksum ``` Regex: /^\d{6}-?\d{4}$|^\d{12}$/ Structure: If 12 digits: first 2 must be century (18, 19, or 20); use digits 2-11 If 10 digits: use all 10 - Digits 2-3 (of working 10): month (1-12) - Digits 4-5: day (1-31 or 61-91 for coordination numbers) Checksum (Luhn on first 9 of working 10 digits): weights = [2, 1, 2, 1, 2, 1, 2, 1, 2] sum = Σ(sum_digits(digit[i] * weights[i])) for i=0..8 check_digit = (10 - (sum % 10)) % 10 Valid if check_digit == digit[9] ```