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:
| 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;
}
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—trueorfalse
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
- Get a free API key — no credit card, 100 requests/hour
- Full API docs
- Stripe VAT validation example repo
- VAT validation REST API example
- VIES API alternative
Top comments (0)