DEV Community

Alexander Nitrovich
Alexander Nitrovich

Posted on • Originally published at blog.eurovalidate.com

Validate VAT for Stripe Subscriptions

Where VAT Validation Fits in Stripe Billing

If you charge EU customers through Stripe, you need to validate their VAT number before creating a subscription. A valid VAT number from another EU country means you apply reverse charge (0% VAT) instead of your local rate. Get it wrong, and you either overcharge the customer or owe the tax authority the difference.

EuroValidate checks the VAT against VIES in real time and returns the company name, address, and a confidence score. You call it once before stripe.subscriptions.create, store the result in subscription metadata, and your invoicing is audit-ready.

The Flow

Customer enters VAT → Your server validates via EuroValidate →
  Valid B2B?  → Create subscription with reverse charge (0% tax)
  Invalid?    → Create subscription with standard VAT rate
  VIES down?  → Check confidence score, decide fallback
Enter fullscreen mode Exit fullscreen mode

Node.js Implementation

npm install @eurovalidate/sdk stripe
Enter fullscreen mode Exit fullscreen mode
import { EuroValidate } from '@eurovalidate/sdk';
import Stripe from 'stripe';

const ev = new EuroValidate(process.env.EUROVALIDATE_API_KEY);
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

async function createSubscription(email, vatNumber, priceId) {
  // Step 1: Validate VAT
  let vatResult = null;
  let applyReverseCharge = false;

  if (vatNumber) {
    vatResult = await ev.validateVat(vatNumber);

    if (vatResult.status === 'valid') {
      // Valid EU B2B customer — reverse charge applies
      applyReverseCharge = true;
    }
  }

  // Step 2: Create Stripe customer
  const customer = await stripe.customers.create({
    email,
    tax_id_data: vatNumber ? [{ type: 'eu_vat', value: vatNumber }] : [],
    metadata: {
      vat_validated: vatResult ? 'true' : 'false',
      vat_status: vatResult?.status || 'not_provided',
      vat_company: vatResult?.company_name || '',
      vat_confidence: vatResult?.meta?.confidence || '',
      vat_validated_at: new Date().toISOString(),
    },
  });

  // Step 3: Create subscription
  const subscription = await stripe.subscriptions.create({
    customer: customer.id,
    items: [{ price: priceId }],
    // Reverse charge: set tax behavior via Stripe Tax or manual exemption
    ...(applyReverseCharge && {
      automatic_tax: { enabled: true },
    }),
    metadata: {
      vat_number: vatNumber || '',
      vat_valid: vatResult?.status === 'valid' ? 'true' : 'false',
      reverse_charge: applyReverseCharge ? 'true' : 'false',
    },
  });

  return { subscription, vatResult, reverseCharge: applyReverseCharge };
}
Enter fullscreen mode Exit fullscreen mode

Python Implementation

pip install eurovalidate stripe
Enter fullscreen mode Exit fullscreen mode
import stripe
from eurovalidate import Client

ev = Client(api_key="YOUR_EUROVALIDATE_KEY")
stripe.api_key = "YOUR_STRIPE_SECRET_KEY"

def create_subscription(email: str, vat_number: str | None, price_id: str):
    vat_result = None
    reverse_charge = False

    if vat_number:
        vat_result = ev.validate_vat(vat_number)
        if vat_result.status == "valid":
            reverse_charge = True

    # Create customer with VAT proof in metadata
    customer = stripe.Customer.create(
        email=email,
        metadata={
            "vat_status": vat_result.status if vat_result else "not_provided",
            "vat_company": vat_result.company_name or "" if vat_result else "",
            "vat_confidence": vat_result.meta.confidence if vat_result else "",
        },
    )

    subscription = stripe.Subscription.create(
        customer=customer.id,
        items=[{"price": price_id}],
        metadata={
            "vat_number": vat_number or "",
            "reverse_charge": str(reverse_charge).lower(),
        },
    )

    return subscription
Enter fullscreen mode Exit fullscreen mode

What EuroValidate Returns

Valid VAT (reverse charge applies)

curl -H "X-API-Key: YOUR_KEY" \
  https://api.eurovalidate.com/v1/vat/FR40303265045
Enter fullscreen mode Exit fullscreen mode
{
  "vat_number": "FR40303265045",
  "country_code": "FR",
  "status": "valid",
  "company_name": "SA SODIMAS",
  "company_address": "RUE DE LA PAIX 75002 PARIS",
  "meta": {
    "confidence": "high",
    "source": "vies_live",
    "cached": false,
    "response_time_ms": 203
  }
}
Enter fullscreen mode Exit fullscreen mode

Invalid VAT (charge standard VAT)

{
  "vat_number": "DE000000000",
  "status": "invalid",
  "company_name": null,
  "meta": {
    "confidence": "high",
    "source": "vies_live"
  }
}
Enter fullscreen mode Exit fullscreen mode

Handling VIES Downtime

VIES has roughly 70% uptime. When it is down, EuroValidate returns cached results with a confidence score:

Confidence Meaning Recommended action
high Fresh data from VIES Trust the result
medium Cached within 24 hours Trust for billing
low Stale cache (1-7 days) Trust with warning
unknown No data available Charge VAT, refund later if needed

For subscriptions, medium and high confidence are safe to use for reverse charge. For unknown, the safest approach is to charge standard VAT and refund the difference once VIES confirms the VAT is valid.

if (vatResult.status === 'valid' && 
    ['high', 'medium'].includes(vatResult.meta.confidence)) {
  applyReverseCharge = true;
}
Enter fullscreen mode Exit fullscreen mode

Storing Proof for Tax Audits

Tax authorities may ask you to prove that you verified the VAT number before applying reverse charge. Store these fields in Stripe metadata:

  • vat_number — the number as submitted
  • vat_statusvalid, invalid, unavailable
  • vat_company — registered company name from VIES
  • vat_confidencehigh, medium, low
  • vat_validated_at — ISO timestamp of validation
  • reverse_chargetrue or false

Stripe metadata is included in invoice exports and API responses, making it audit-ready without a separate database.

Common Mistakes

Validating only at signup. VAT numbers can be revoked. Re-validate periodically using the monitoring endpoint (POST /v1/monitor) which sends a webhook when status changes.

Applying reverse charge to domestic customers. Reverse charge only applies to B2B transactions where the customer is in a different EU country than the seller. If both are in Portugal, charge Portuguese VAT.

Ignoring Germany and Spain. These countries never return company names from VIES due to data protection laws. Your code must handle company_name: null without treating it as invalid.

Not handling Greece correctly. VIES uses EL for Greece, but ISO 3166 uses GR. EuroValidate accepts both, but if you validate the prefix yourself, account for this mapping.

Latency

Scenario Time
Cached VAT (second check) 1-5 ms
Live VIES lookup 150-300 ms
Stripe customer.create ~200 ms
Total (validate + create) ~400-500 ms

The VAT validation adds minimal latency to the subscription flow. Cached responses are near-instant.

Next Steps

Top comments (0)