vatverify home
All guides

The EU VAT reverse-charge API: stop hand-coding B2B tax logic

The /v1/decide endpoint decides whether to charge VAT, at what rate, under which mechanism. Full walkthrough with Stripe + invoice integration.

TL;DR

  • POST /v1/decide takes seller_vat and buyer_vat (full VAT numbers with country prefix).
  • Returns mechanism (standard | reverse_charge | zero_rated | out_of_scope), rate, and invoice_note.
  • Eliminates the need to hand-code EU VAT Directive Article 196 logic in your app.
SDK status

@vatverify/node (used in the Stripe and invoice examples below) ships on npm at the API launch. Reading this before launch? The /v1/decide REST endpoint is live today. Replace any vat.decide({ seller_vat, buyer_vat }) call with a fetch('https://api.vatverify.dev/v1/decide', { method: 'POST', ... }) using the same Authorization: Bearer header. The SDK is a typed wrapper around the same JSON.

The EU reverse-charge rule in plain English

Concrete example: a German SaaS sells a €1,000/year subscription to a French company. Under normal VAT rules, the seller would charge the buyer's local VAT, which would force the German SaaS to register for VAT in France, file French VAT returns, and remit collected VAT to the French tax authority. Multiply that by 26 other member states and the compliance overhead kills cross-border B2B entirely.

The reverse-charge mechanism fixes this. When a VAT-registered business in one EU member state sells services (or certain goods) to a VAT-registered business in another EU member state, the seller charges zero VAT. Instead, the buyer "self-accounts," declaring both the output VAT and the (usually offsetting) input VAT on their own domestic return. Liability shifts from seller to buyer, and the seller never has to register in the buyer's country.

Three conditions must all be true for reverse-charge to apply:

  1. Both parties are VAT-registered businesses. A valid, currently-registered VAT number on each side. Reverse-charge does not apply to B2C sales.
  2. They are in different EU member states. Same-country B2B uses the standard domestic rate; non-EU sales follow export rules, not reverse-charge.
  3. The supply is covered by the general rule (Article 44, covering most services including SaaS). Exceptions exist for real estate, passenger transport, and a few other categories.

On the invoice itself, reverse-charge means zero VAT charged plus a mandatory legal note citing Article 196 of the EU VAT Directive. Missing that note is a common audit finding. The /v1/decide endpoint returns the note ready to print, so you can paste it verbatim.

Why regex + VIES isn't enough

Validating the VAT number is step one. The actual question your billing system needs to answer is: "do I charge VAT on this invoice, and if so, at what rate?" A raw VIES check returns valid: true | false, useful but not the complete answer.

The real decision chain is:

  1. Is the buyer's VAT number currently registered? (VIES for EU, HMRC for UK, BFS for CH.)
  2. What country is the seller established in?
  3. What country is the buyer established in? (Not always the same as the country prefix on the VAT number.)
  4. Is this a supply of services or goods? (Different place-of-supply rules apply.)
  5. Are the seller and buyer in the same country? (If yes, standard domestic rate; reverse-charge doesn't apply.)
  6. Does Article 44 / Article 196 cover this supply? (SaaS yes; real estate, passenger transport, and restaurant services follow different rules.)
  7. If reverse-charge applies: rate is 0, but an Article 196 invoice note is legally required.
  8. If standard domestic rate applies: look up the current standard rate for the seller's country.
  9. If the seller is non-EU selling into the EU: OSS / import-VAT rules kick in (out of scope for /v1/decide today).

Hand-coding that is a few hundred lines of conditional logic, a rate table you have to keep current, and a set of legal-text templates you have to get exactly right. /v1/decide collapses it into one HTTP call and returns the full answer including the invoice note.

Introducing /v1/decide: one call, complete answer

curl -X POST https://api.vatverify.dev/v1/decide \
  -H "Authorization: Bearer $VATVERIFY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "seller_vat": "DE811569869",
    "buyer_vat": "FR40303265045"
  }'

Request / response walkthrough

Full request and response for an EU cross-border B2B sale:

Request:

{
  "seller_vat": "DE811569869",
  "buyer_vat": "FR40303265045"
}

Response:

{
  "data": {
    "charge_vat": false,
    "rate": 0,
    "mechanism": "reverse_charge",
    "legal_basis": "EU VAT Directive Article 196",
    "explanation": "Your customer provided a valid French VAT number, so this is an EU cross-border B2B supply. The reverse-charge mechanism applies, so you do not charge VAT, and the buyer self-accounts.",
    "invoice_note": "Reverse charge — VAT to be accounted for by the recipient",
    "disclaimer": "This is guidance, not legal advice. Confirm with a qualified tax adviser for your specific situation.",
    "buyer_vat": {
      "valid": true,
      "country": { "code": "FR", "name": "France" }
    },
    "decided_at": "2026-04-27T12:00:00Z"
  },
  "meta": {
    "request_id": "0190f8ea-a5b2-7000-a123-000000000000",
    "latency_ms": 422,
    "source_status": "live",
    "sources": [
      { "role": "seller", "source": "vies", "source_status": "live", "cached": false, "cached_at": null, "stale_seconds": null },
      { "role": "buyer", "source": "vies", "source_status": "live", "cached": false, "cached_at": null, "stale_seconds": null }
    ]
  }
}

All decision cases

EU seller → EU buyer with valid VAT (different countries)

{
  "seller_vat": "DE811569869",
  "buyer_vat": "FR40303265045"
}
{ "mechanism": "reverse_charge", "rate": 0, "charge_vat": false }

EU seller → same-country B2B

{
  "seller_vat": "DE811569869",
  "buyer_vat": "DE123456789"
}
{ "mechanism": "standard", "rate": 19, "charge_vat": true }

EU seller → B2C (no VAT number provided)

The /v1/decide endpoint requires both seller_vat and buyer_vat. B2C transactions (where the buyer has no VAT number) are not supported in v1.

{
  "error": {
    "code": "b2c_not_supported",
    "message": "This endpoint is for B2B transactions only. B2C VAT rules (OSS/IOSS) are not currently supported."
  }
}

HTTP status: 400

EU seller → unregistered buyer VAT number

{
  "seller_vat": "DE811569869",
  "buyer_vat": "FR00000000000"
}
{
  "error": {
    "code": "buyer_vat_not_registered",
    "message": "The buyer VAT number FR00000000000 is not registered in VIES."
  }
}

HTTP status: 400

Non-EU seller

Non-EU seller VAT numbers (US, UK, CH, NO) are not supported in v1. The API returns seller_country_unsupported (400).

{
  "error": {
    "code": "seller_country_unsupported",
    "message": "Seller country US is not supported. Currently only EU-27 seller countries are supported."
  }
}

HTTP status: 400

Integration with Stripe checkout

Use /v1/decide before creating the Stripe customer to set up correct tax handling:

lib/checkout.ts
import Stripe from 'stripe';
import { Vatverify } from '@vatverify/node';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const vat = new Vatverify(process.env.VATVERIFY_API_KEY!);

export async function createCheckoutSession({
  sellerVat,
  buyerVat,
  priceId,
}: {
  sellerVat: string;
  buyerVat: string;
  priceId: string;
}) {
  // Step 1: get the tax decision
  const decision = await vat.decide({
    seller_vat: sellerVat,
    buyer_vat: buyerVat,
  });

  // Step 2: create a Stripe customer with the validated tax ID
  const customer = await stripe.customers.create({
    tax_id_data: [
      {
        type: `eu_vat`,
        value: buyerVat,
      },
    ],
    metadata: {
      vatverify_mechanism: decision.data.mechanism,
      vatverify_rate: String(decision.data.rate),
    },
  });

  // Step 3: create the checkout session
  const session = await stripe.checkout.sessions.create({
    customer: customer.id,
    mode: 'subscription',
    line_items: [{ price: priceId, quantity: 1 }],
    // Stripe applies zero-rate automatically when a valid EU VAT ID is on the customer
    automatic_tax: { enabled: true },
    success_url: `${process.env.NEXT_PUBLIC_APP_URL}/checkout/success`,
    cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/checkout`,
  });

  return session;
}

Integration with invoice generation

Inject decision.data.invoice_note directly into the invoice PDF to satisfy Article 226 of the EU VAT Directive:

lib/invoice.ts
import { Vatverify } from '@vatverify/node';

const vat = new Vatverify(process.env.VATVERIFY_API_KEY!);

export async function buildInvoiceData({
  sellerVat,
  buyerVat,
  subtotal,
}: {
  sellerVat: string;
  buyerVat: string;
  subtotal: number;
}) {
  const decision = await vat.decide({
    seller_vat: sellerVat,
    buyer_vat: buyerVat,
  });

  const vatAmount = subtotal * (decision.data.rate / 100);
  const total = subtotal + vatAmount;

  return {
    subtotal,
    vatRate: decision.data.rate,
    vatAmount,
    total,
    // Paste this string directly onto the invoice PDF
    legalNote: decision.data.invoice_note,
    mechanism: decision.data.mechanism,
  };
}

Legal basis: EU VAT Directive Article 196

Article 196 of Council Directive 2006/112/EC (the EU VAT Directive) is the legal foundation for the cross-border B2B reverse-charge mechanism. The operative text: "VAT shall be payable by any taxable person to whom the services referred to in Article 44 are supplied, where those services are supplied by a taxable person not established within the territory of the Member State." In plain terms: when a taxable person (business) in one member state buys general-rule services from a supplier established elsewhere in the EU, the buyer (not the seller) is liable to account for VAT. That shift of liability is what makes zero-rating on the seller's invoice compliant instead of criminal.

Article 226 of the same directive lists the mandatory particulars on a VAT invoice. For reverse-charge supplies, it specifically requires a reference to the provision under which the reverse-charge applies. The /v1/decide response returns invoice_note: "Reverse charge — VAT to be accounted for by the recipient" alongside legal_basis: "EU VAT Directive Article 196". Print both verbatim on the invoice and the combination satisfies the Article 226 requirement without legal copy-editing.

Article 196 covers the general rule for services under Article 44. Special rules exist for real estate (Article 47), passenger transport (Article 48), restaurant and catering services (Article 55), admission to cultural events (Article 53), and short-term hire of means of transport (Article 56). These are out of scope for /v1/decide today. For edge cases or non-standard supplies, consult a VAT advisor. The /decide response is intentionally conservative and will surface known limitations rather than guess.

FAQ

Does this endpoint cover UK VAT?

Not in v1. UK coverage (including UK→EU and UK domestic scenarios under HMRC rules) is planned for v2. See the HMRC VAT check guide for UK-only validation in the meantime.

Does it handle OSS or IOSS?

No. The /v1/decide endpoint is B2B only. OSS (One Stop Shop) and IOSS apply to B2C cross-border sales and are not currently in scope.

What about digital services with special place-of-supply rules?

For most B2B digital services, the general rule (Article 44 of the EU VAT Directive) applies: place of supply is where the buyer is established, which triggers the reverse charge. Edge cases like pre-recorded online events may have different rules. Consult a tax advisor.

Do distance-selling thresholds affect this?

Distance-selling thresholds (€10,000 OSS threshold) apply to B2C sales, not B2B. If you have a valid buyer VAT number, the reverse charge applies regardless of transaction volume.

What should I do if the API is down?

If /v1/decide returns a 5xx, do not silently charge zero VAT. That could be incorrect. The safest fallback is to block checkout and show an error message. See handle-vies-downtime for retry and fallback patterns.

Validate VAT in three lines.

Free up to 500 requests per month. No credit card.

Start free