# Integrate EU KYC verification for the Embedded Components onramp Collect MiCA identifiers and complete L2 verification for EU users. # Web ## Before you begin Before you begin, [integrate the Embedded Components Onramp](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md?platform=web) (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. The primary EU regulation driving these requirements is: - **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). Users must also accept a Stripe Terms of Service attestation. `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-customers-current-kyc-tier) for details about tier statuses. Separately, purchases at or above 1,000 EUR are subject to the EU Travel Rule, which requires the user to verify ownership of their destination wallet before the purchase can proceed. See [Travel Rule wallet ownership verification](https://docs.stripe.com/crypto/onramp/eu-kyc-integration-guide.md#travel-rule-wallet-ownership). ## 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`, `kyc_tiers`, and `provided_fields` on the [CryptoCustomer object](https://docs.stripe.com/api/crypto/customers/object.md) 2. Collect basic KYC info including **nationalities** and submit with `submitKycInfo` 3. Call `getMissingIdentifiers` to determine which MiCA identifiers are needed 4. Collect any MiCA identifiers from the `identifiers` array and submit with `updateKycInfo` 5. Present the Stripe Terms of Service (ToS) with `promptUserAttestation` 6. Complete identity verification (document + selfie) with `verifyDocuments` ## 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. The following fields on the CryptoCustomer are relevant: | Field | Description | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `kyc_region` | Derived from the user’s country of residence. Possible values: `null` (KYC not yet submitted), `"us"`, or `"eu"`. | | `kyc_tiers` | Array of tier objects, each with a `tier` name and `verification_status`. For EU users, `l0` and `l1` are `"not_available"` — only the `l2` entry is relevant. See the L2 states table below. | | `provided_fields` | Array of strings indicating which EU-specific data the user has submitted. Key values: `"identifiers"` (identifier requirements satisfied — either MiCA identifiers were submitted or none were required) and `"attestation"` (user accepted the Stripe Terms of Service). | The `l2` tier’s `verification_status` can be one of the following for EU users: | Status | Meaning | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `not_started` | Basic KYC info has not been submitted or failed validation. | | `pending` | Basic KYC info submitted. Document verification is either not yet submitted or still processing. | | `verified` | Basic KYC info and identity document verification are both complete. | | `rejected` | Identity document verification was rejected. Inspect `verification_errors` on the `l2` tier object to determine whether the user can retry. To learn more, see [Handle a rejected L2 verification](https://docs.stripe.com/crypto/onramp/eu-kyc-integration-guide.md#handle-rejected-l2). | First, check `kyc_region` to determine whether the user is subject to EU requirements: - If `kyc_region` is `null`, the user hasn’t submitted basic KYC information yet. Display a KYC form that includes a country of residence field. Based on the selected country, show the appropriate KYC fields: - EU country: show the EU KYC form (nationalities, birth city/country)—see [Submit basic KYC info](https://docs.stripe.com/crypto/onramp/eu-kyc-integration-guide.md#step-2-submit-basic-kyc-info) below. - US: show the standard KYC form—see the [embedded components integration guide](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md?platform=web#step-collect-kyc-if-needed). - If `kyc_region` is `us`, this flow doesn’t apply. Collect KYC via the [embedded components integration guide](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md?platform=web#step-collect-kyc-if-needed). - If `kyc_region` is `eu`, continue with this guide. Then check whether the user has completed all EU requirements. **All three** conditions must be true for the user to transact: 1. `kyc_tiers` contains an `l2` entry with `verification_status: "verified"` 2. `provided_fields` includes `"identifiers"` (identifier requirements satisfied) 3. `provided_fields` includes `"attestation"` (ToS accepted) If all three are met, the user is fully verified—skip the remaining steps. ### Determine where to resume If any condition above is not met, check the `l2` tier’s `verification_status` and `provided_fields` to determine which step the user needs next: | Condition | Next step | | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `l2` is `"not_started"` | [Submit basic KYC info](https://docs.stripe.com/crypto/onramp/eu-kyc-integration-guide.md#step-2-submit-basic-kyc-info)—basic info not yet submitted | | `l2` is `"pending"` and `provided_fields` doesn’t include `"identifiers"` | [Get missing identifiers](https://docs.stripe.com/crypto/onramp/eu-kyc-integration-guide.md#step-3-get-missing-identifiers)—basic info done, identifiers still needed | | `provided_fields` includes `"identifiers"` but not `"attestation"` | [Stripe Terms of Service](https://docs.stripe.com/crypto/onramp/eu-kyc-integration-guide.md#step-5-stripe-terms-of-service)—identifiers submitted, Stripe ToS still needed | | `provided_fields` includes both but `l2` is not `"verified"` | [Complete identity verification](https://docs.stripe.com/crypto/onramp/eu-kyc-integration-guide.md#step-6-complete-identity-verification)—document and selfie still needed | | `l2` is `"rejected"` | [Handle a rejected L2 verification](https://docs.stripe.com/crypto/onramp/eu-kyc-integration-guide.md#handle-rejected-l2)—check `verification_errors` to determine whether to retry or direct to support | ```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-05-27.preview;crypto_onramp_beta=v2', }, } ); const customer = await response.json(); if (customer.kyc_region === null) { // Basic KYC not yet submitted — display KYC form with country of residence field; fields adapt based on selected country } if (customer.kyc_region === 'us') { // User is in the US — use the embedded components integration guide } // kyc_region is 'eu' — continue with this guide const l2Tier = (customer.kyc_tiers ?? []).find((t) => t.tier === 'l2'); const providedFields = customer.provided_fields ?? []; const isFullyVerified = l2Tier?.verification_status === 'verified' && providedFields.includes('identifiers') && providedFields.includes('attestation'); if (isFullyVerified) { // All EU requirements met — no further action needed } // Determine which step to resume from if (l2Tier?.verification_status === 'not_started') { // Start at Step 2: Submit basic KYC info } else if (!providedFields.includes('identifiers')) { // Start at Step 3: Get missing identifiers } else if (!providedFields.includes('attestation')) { // Start at Step 5: Stripe Terms of Service } else if (l2Tier?.verification_status === 'rejected') { // Start at: Handle rejected L2 verification — see below } else { // Start at Step 6: Complete identity verification (L2 not yet verified) } ``` ## Submit basic KYC info [Client-side] If the user is US-based, collect KYC via the [embedded components integration guide](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md?platform=web#step-collect-kyc-if-needed) instead. 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. | ## 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`**: Always `false`. This field isn’t used. You can ignore it. - **`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). Collect any required MiCA identifiers from the `identifiers` array — these are determined by the user’s nationalities and residence country. ```javascript const requirements = await onramp.getMissingIdentifiers(); // { // carf_tin_required: false, // 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. 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) | ### 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: false, // identifiers: [ // { type: 'mt_nic', regulation: 'eu_mica' } // ], // alternatives: [ // { // original_missing_identifiers: ['mt_nic'], // alternative_missing_identifiers: ['mt_pp'] // } // ] // } ``` In this example, the user can provide either `mt_nic` (Malta national identity card) **or** `mt_pp` (Malta passport) to satisfy the MiCA requirement. 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. | ## Submit identifiers [Client-side] Before calling `updateKycInfo`, validate identifiers client-side using the regex patterns and structure rules documented in [Identifier validation logic](https://docs.stripe.com/crypto/onramp/eu-kyc-integration-guide.md#appendix-identifier-validation-logic). 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 MiCA identifiers 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: '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) are satisfied — proceed to attestation | | `carf_tin_required` | This field isn’t used. You can ignore it. | | `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. | ## 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`. | | `INVALID_REQUEST_ERROR` | The provided regulation value is not valid. | | `UNAUTHENTICATED_USER` | User has not authenticated. | | `RATE_LIMIT_EXCEEDED` | The request was rate limited. | ## 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 ``` ### Handle a rejected L2 verification If `l2.verification_status` is `rejected`, inspect `l2.verification_errors` to determine the next step: | `verification_errors` value | Meaning | Next step | | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------- | | `id_document_verification_failed` | The identity document or selfie was rejected by Stripe’s verification system. | Prompt the user to retry identity verification. | | `user_has_reached_max_verification_attempts` | The user has exhausted all allowed verification attempts. | No retry is possible. Ask the user to contact Stripe support. | | `id_document_verification_failed` and `user_has_reached_max_verification_attempts` | The last attempt was rejected and no retries remain. | No retry is possible. Ask the user to contact Stripe support. | ```javascript const l2Tier = customer.kyc_tiers.find((t) => t.tier === 'l2'); if (l2Tier?.verification_status === 'rejected') { const errors = l2Tier.verification_errors ?? []; const canRetry = !errors.includes('user_has_reached_max_verification_attempts'); if (canRetry) { // Prompt the user to retry identity verification await onramp.verifyDocuments(); } else { // User has exhausted all attempts — direct them to contact Stripe support } } ``` ### Errors | Error | Message | | ---------------------- | ----------------------------- | | `UNAUTHENTICATED_USER` | User has not authenticated. | | `RATE_LIMIT_EXCEEDED` | The request was rate limited. | ## Travel Rule wallet ownership verification In the EU, purchases at or above 1,000 EUR are subject to the [EU Travel Rule](https://www.eba.europa.eu/regulation-and-policy/anti-money-laundering-and-countering-financing-terrorism/guidelines-preventing-abuse-funds-and-certain-crypto-assets-transfers-money-laundering-and-terrorist). Before a user can complete an onramp purchase at or above this threshold, they must prove that they control the destination wallet by signing a Stripe-issued challenge with that wallet. This flow is separate from the identity verification steps above. It applies at purchase time and is scoped to a specific registered wallet address and network, and not to the user’s identity. > EVM-compatible networks (for example, Ethereum, Base, and Polygon) and Solana support wallet ownership verification at launch. Requesting a challenge for an unsupported network returns an error (see [Wallet ownership verification errors](https://docs.stripe.com/crypto/onramp/embedded-components-error-handling-guide.md?platform=web#wallet-ownership-verification-errors)). ### Flow overview 1. Check whether the destination wallet is already verified by inspecting `verified_ownership` on the [crypto.consumer_wallet](https://docs.stripe.com/api/crypto/consumer_wallets/object.md) resource. 2. If it isn’t verified, request a short-lived wallet ownership challenge with `getWalletOwnershipChallenge`. 3. Ask the user to sign the exact challenge `message` with their wallet. 4. Submit the signature with `submitWalletOwnershipSignature`. 5. After `verified_ownership` is `true`, the user can complete the purchase. If you triggered verification in response to a checkout that failed with `wallet_ownership_verification_required`, retry that checkout. Verification persists on the wallet, so a verified wallet address and network don’t need to be re-verified for future purchases. Decide when to verify based on your checkout flow, either preemptively after wallet registration for wallets likely to be used above the threshold, or on demand when a purchase indicates verification is required. ### Check whether a wallet is already verified The `verified_ownership` field on each registered wallet indicates whether it has already completed ownership verification. Retrieve the user’s registered wallets from your server and only run the challenge flow when `verified_ownership` is `false`. `verified_ownership` defaults to `false` for existing and newly registered wallets until verification succeeds. ```javascript // Your server: list the user's registered wallets and check verified_ownership. const response = await fetch( `https://api.stripe.com/v1/crypto/customers/${customerId}/crypto_consumer_wallets`, { headers: { 'Authorization': `Bearer ${process.env.STRIPE_SECRET_KEY}`, 'Stripe-OAuth-Token': oauthToken, 'Stripe-Version': '2026-05-27.preview;crypto_onramp_beta=v2', }, } ); const { data: wallets } = await response.json(); const wallet = wallets.find( (consumerWallet) => consumerWallet.wallet_address === walletAddress && consumerWallet.network === network ); if (wallet?.verified_ownership) { // Already verified — proceed with the purchase. } else { // Not verified — run the challenge and signature flow below. } ``` ### Request a wallet ownership challenge Request a challenge for a wallet the user has already registered with `registerWalletAddress`. The challenge is short-lived and single-use, and it’s bound to the authenticated user, the wallet address, and the network. ```javascript const challenge = await onramp.getWalletOwnershipChallenge({ walletAddress: '0x1234567890abcdef1234567890abcdef12345678', network: 'ethereum', }); // { // challengeId: 'woc_123', // walletAddress: '0x1234567890abcdef1234567890abcdef12345678', // network: 'ethereum', // message: '...', // Exact opaque string the wallet must sign. // expiresAt: '2026-06-15T12:00:00Z', // } ``` Treat `message` as opaque and sign it exactly as returned. Stripe owns message construction and verification. Use the standard message-signing method for the wallet’s network: [`personal_sign`](https://docs.metamask.io/wallet/reference/json-rpc-methods/personal_sign/) for EVM-compatible networks, and [`signMessage`](https://github.com/anza-xyz/wallet-adapter) for Solana. For the errors these methods can return and how to recover from them, see [Wallet ownership verification errors](https://docs.stripe.com/crypto/onramp/embedded-components-error-handling-guide.md?platform=web#wallet-ownership-verification-errors). ### Collect a signature and submit it Ask the user to sign the exact `message` from the challenge with their wallet, then submit the resulting signature. `submitWalletOwnershipSignature` returns the updated wallet on success, with `verified_ownership` set to `true`. The `challengeId` is authoritative and already identifies the wallet address and network, so you don’t pass them again. ```javascript // Ask the user to sign the exact challenge message with their wallet. const signature = await userWallet.signMessage(challenge.message); try { const consumerWallet = await onramp.submitWalletOwnershipSignature({ challengeId: challenge.challengeId, signature, }); // consumerWallet.verified_ownership === true — retry the purchase. } catch (error) { if (error.code === OnrampErrorCode.WALLET_OWNERSHIP_CHALLENGE_EXPIRED) { // Request a fresh challenge and ask the wallet to sign again. } else if (error.code === OnrampErrorCode.INVALID_WALLET_OWNERSHIP_SIGNATURE) { // The signature does not prove ownership. Restart the full flow to retry. } else { // Handle unexpected errors. } } ``` If the user cancels signing, stop the flow before calling `submitWalletOwnershipSignature`. Signing failures that happen in the signing UI aren’t Stripe API errors. ### Handle wallet ownership verification during checkout If you attempt an onramp purchase at or above the Travel Rule threshold with an unverified destination wallet, checkout fails and Stripe sets `transaction_details.last_error` to `wallet_ownership_verification_required` on the onramp session. This is a recoverable error: run the wallet ownership challenge and signature flow above for the destination wallet, then retry checkout. For how to detect and handle this error alongside the other checkout errors, see [Error handling](https://docs.stripe.com/crypto/onramp/embedded-components-error-handling-guide.md?platform=web#checkout-errors). ## 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: { // 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', }); // 2. Check what identifiers are needed const requirements = await onramp.getMissingIdentifiers(); // requirements.identifiers — MiCA identifiers the user must provide // 3. Collect and submit MiCA identifiers const result = await onramp.updateKycInfo([ { type: 'ee_ik', value: '39901011234' }, // MiCA identifier for Estonian nationality ]); // 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. ## 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 ``` 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] ```