# 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
```

## Node.js Implementation

```bash
npm install @eurovalidate/sdk stripe
```

```javascript
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

```bash
pip install eurovalidate stripe
```

```python
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)

```bash
curl -H "X-API-Key: YOUR_KEY" \
  https://api.eurovalidate.com/v1/vat/FR40303265045
```

```json
{
  "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)

```json
{
  "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:

| 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.

```javascript
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_status` — `valid`, `invalid`, `unavailable`
- `vat_company` — registered company name from VIES
- `vat_confidence` — `high`, `medium`, `low`
- `vat_validated_at` — ISO timestamp of validation
- `reverse_charge` — `true` 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](https://api.eurovalidate.com/docs) (`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

- [Get a free API key](https://eurovalidate.com) — no credit card, 100 requests/hour
- [Full API docs](https://api.eurovalidate.com/docs)
- [Stripe VAT validation example repo](https://github.com/eurovalidate/stripe-vat-validation)
- [VAT validation REST API example](https://blog.eurovalidate.com/vat-validation-rest-api-example)
- [VIES API alternative](https://blog.eurovalidate.com/vies-api-alternative)
