Compliance10 min readAlberto García

Reverse Charge VAT for SaaS: A Developer's Implementation Guide

Reverse charge means the buyer accounts for VAT instead of you — but you must validate their VAT number, invoice correctly, and re-validate monthly. Here's how to implement each step.

reverse-chargevatsaascompliancebillingtypescript

Reverse charge VAT sounds simple: instead of you charging and remitting VAT, the buyer does. But implementing it in a SaaS billing system has four discrete steps, each with specific code requirements and compliance implications. Miss any one of them and you either create VAT liability or generate invoices that fail an audit. This guide covers all four steps with working TypeScript examples.

When reverse charge applies: the 3 conditions

Reverse charge for cross-border digital services applies when all three of these conditions are met simultaneously. If any condition fails, you are in B2C territory and must apply the customer's country VAT rate via OSS.

  • 1.Both your business and the customer's business are VAT-registered in EU member states
  • 2.The customer is registered in a different member state from your seller entity
  • 3.The customer's VAT number is confirmed active in VIES at the time of supply

Step 1: Detecting a reverse charge customer

Detection happens at two points: during account creation (when the customer first provides a VAT number) and before each invoice is generated (to catch deregistrations). The classifier below handles both entry points.

typescriptlib/reverse-charge.ts
export async function detectReverseCharge(
  sellerCountry: string, // Your VAT registration country (e.g. 'ES')
  customerCountry: string,
  vatNumber: string
): Promise<{ eligible: boolean; status: string; companyName?: string; requestId?: string }> {
  // Same country: no reverse charge even with a valid VAT number
  if (customerCountry === sellerCountry) {
    return { eligible: false, status: 'domestic_supply' };
  }

  const res = await fetch(
    `https://www.taxid.dev/api/v1/validate/${customerCountry}/${vatNumber}`,
    {
      headers: { Authorization: `Bearer ${process.env.TAXID_API_KEY}` },
      signal: AbortSignal.timeout(8000),
    }
  );

  const data = await res.json();

  return {
    eligible: data.status === 'active' || data.status === 'service_unavailable',
    status: data.status,
    companyName: data.company_name,
    requestId: data.request_id,
  };
}

Step 2: Validating before applying zero-rate

At checkout, validate in real time and store the full result — not just the boolean. You need the request_id for audit purposes and the company_name for invoice generation. If VIES is unavailable, allow the customer through but flag for re-validation.

typescriptapi/checkout/validate-vat.ts
async function processVatAtCheckout(
  customerId: string,
  countryCode: string,
  vatNumber: string
) {
  const result = await detectReverseCharge(
    process.env.SELLER_COUNTRY!, countryCode, vatNumber
  );

  // Persist validation record — required for compliance audit
  await db.vatValidations.create({
    data: {
      customerId,
      vatNumber,
      countryCode,
      status: result.status,
      companyName: result.companyName ?? null,
      requestId: result.requestId ?? null,
      validatedAt: new Date(),
      source: 'checkout',
    },
  });

  if (result.status === 'service_unavailable') {
    // VIES is temporarily down — allow through, schedule re-validation
    await scheduleRevalidation(customerId, 'vies_unavailable');
    return { proceed: true, reverseCharge: true, requiresRevalidation: true };
  }

  return {
    proceed: result.eligible || result.status === 'format_invalid',
    reverseCharge: result.eligible,
    companyName: result.companyName,
  };
}

Warning

Never treat service_unavailable as invalid. VIES is down approximately 2% of the time. Rejecting a valid customer because VIES was temporarily unreachable creates churn and is legally unnecessary — you are not required to block the sale, only to validate when possible.

Step 3: Generating the compliant invoice

A reverse-charge invoice must satisfy Article 226 of the EU VAT Directive. The most commonly omitted elements are the explicit 'reverse charge' text and the Article 196 reference. Without these, the invoice is non-compliant even if the underlying VAT number was correctly validated.

Invoice fieldRequired?Example value
Your VAT numberYesESB12345678
Customer's VAT numberYesDE123456789 (the VIES-validated number)
'Reverse charge' notationYes"VAT: Reverse charge (Article 196, EU VAT Directive)"
Net amountYes€1,200.00
VAT rateYes0%
VAT amountYes€0.00
Invoice dateYes2026-06-05
Sequential invoice numberYesINV-2026-00142

Step 4: The monthly re-validation job

Businesses deregister. A VAT number that was valid at onboarding can become inactive at any time. Applying reverse charge based on a stale validation exposes you to the same liability as never validating — the obligation is to verify the status 'at the time of supply', which for subscriptions means each billing cycle.

typescriptjobs/monthly-vat-revalidation.ts
// Run as a cron job on the 1st of each month, before invoice generation
export async function monthlyVatRevalidationJob() {
  const customers = await db.customer.findMany({
    where: {
      vatStatus: 'active',
      vatCountryCode: { not: process.env.SELLER_COUNTRY },
    },
    select: { id: true, vatNumber: true, vatCountryCode: true, email: true },
  });

  let statusChanges = 0;

  for (const customer of customers) {
    // Rate-limit: 1 request per 200ms to stay within API limits
    await new Promise((r) => setTimeout(r, 200));

    const result = await detectReverseCharge(
      process.env.SELLER_COUNTRY!,
      customer.vatCountryCode,
      customer.vatNumber
    );

    if (result.status !== 'active' && result.status !== 'service_unavailable') {
      await db.customer.update({
        where: { id: customer.id },
        data: { vatStatus: result.status, vatStatusChangedAt: new Date() },
      });
      await notifyFinanceTeam(customer, result.status);
      statusChanges++;
    }
  }

  console.log(
    `Re-validation complete. ${customers.length} checked, ${statusChanges} status changes.`
  );
}

Stripe-specific: setting tax_exempt correctly

If you use Stripe for billing, set the customer's tax_exempt property to 'reverse' when reverse charge applies. This signals to Stripe Tax not to apply VAT and ensures the invoice exported from Stripe is annotated correctly. You still need to store your own validation record — Stripe does not perform VIES validation on your behalf.

typescriptlib/stripe-vat.ts
const result = await detectReverseCharge(SELLER_COUNTRY, countryCode, vatNumber);

if (result.eligible) {
  await stripe.customers.update(stripeCustomerId, {
    tax_exempt: 'reverse',
    metadata: {
      vat_number: vatNumber,
      vat_company: result.companyName ?? '',
      vat_validated_at: new Date().toISOString(),
      vat_request_id: result.requestId ?? '',
    },
  });
} else {
  // Not eligible for reverse charge
  await stripe.customers.update(stripeCustomerId, { tax_exempt: 'none' });
}

Start validating EU VAT numbers

Free plan — 100 validations/month. No credit card required.

AG
Alberto García

Founder, TaxID

Building EU VAT validation tools for developers. Obsessed with compliance automation and developer experience.