Skip to main content

Command Palette

Search for a command to run...

Validate VAT for Stripe Subscriptions

Updated
5 min read

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

Node.js Implementation

npm install @eurovalidate/sdk stripe
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 };
}

Python Implementation

pip install eurovalidate stripe
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

What EuroValidate Returns

Valid VAT (reverse charge applies)

curl -H "X-API-Key: YOUR_KEY" \
  https://api.eurovalidate.com/v1/vat/FR40303265045
{
  "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
  }
}

Invalid VAT (charge standard VAT)

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

Handling VIES Downtime

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

ConfidenceMeaningRecommended action
highFresh data from VIESTrust the result
mediumCached within 24 hoursTrust for billing
lowStale cache (1-7 days)Trust with warning
unknownNo data availableCharge 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;
}

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

ScenarioTime
Cached VAT (second check)1-5 ms
Live VIES lookup150-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