Your Stripe Checkout charges aren't legal invoices in the EU

Stripe moves the money. It does not produce a legally compliant B2B e-invoice on its own — and in Belgium, France, and Germany, that gap is now a compliance problem, not a nice-to-have.

The gap

A Stripe Checkout receipt or emailed PDF invoice is a payment confirmation, not a structured e-invoice: it has no EN 16931 arithmetic validation (the BR-CO-* business rules), no explicit VAT category/exemption reasoning per line, and it isn't transmitted over any network an EU mandate recognizes. For a B2C sale this is often fine. For a domestic B2B sale in a mandate jurisdiction, it isn't — the buyer's accounting system may be unable to legally book it, and depending on the jurisdiction and transaction, the seller may be out of compliance for not issuing correctly.

The pipeline pattern

The fix is a webhook: when Stripe tells you a Checkout session completed, map the session/customer/line-item data into canonical invoice JSON and hand it to Vantane. This is illustrative — the actual invoice fields depend on what you capture from the buyer at checkout (crucially: their VAT ID and legal address, which Stripe does not collect by default) — but every Vantane call in it is exactly what the runnable block below proves works.

import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
  let event;
  try {
    event = stripe.webhooks.constructEvent(
      req.body,
      req.headers['stripe-signature'],
      process.env.STRIPE_WEBHOOK_SECRET,
    );
  } catch (err) {
    return res.status(400).send(`Webhook signature verification failed: ${err.message}`);
  }

  if (event.type === 'checkout.session.completed') {
    const session = event.data.object;
    const customer = await stripe.customers.retrieve(session.customer);
    const lineItems = await stripe.checkout.sessions.listLineItems(session.id);

    const invoice = {
      id: `STRIPE-${session.id}`,
      invoiceTypeCode: '380',
      issueDate: new Date().toISOString().slice(0, 10),
      currencyCode: session.currency.toUpperCase(),
      seller: YOUR_REGISTERED_SELLER, // your own fixed business details
      buyer: {
        name: customer.name,
        vatId: customer.metadata.vat_id,        // collect at checkout — Stripe doesn't require it
        address: {
          line1: customer.address.line1,
          city: customer.address.city,
          postalCode: customer.address.postal_code,
          countryCode: customer.address.country,
        },
      },
      lines: lineItems.data.map((item, i) => ({
        id: String(i + 1),
        description: item.description,
        quantity: item.quantity,
        unitCode: 'EA',
        unitPrice: item.price.unit_amount, // Stripe amounts are already integer minor units — same convention Vantane uses
        vat: { categoryCode: 'S', rate: 21 }, // derive the real rate for the buyer's country/status
      })),
    };

    const created = await fetch('https://vantane.com/v1/invoices', {
      method: 'POST',
      headers: { authorization: `Bearer ${process.env.VANTANE_KEY}`, 'content-type': 'application/json' },
      body: JSON.stringify({ invoice, options: { deliver: true } }),
    }).then((r) => r.json());

    // Store created.id / created.status / created.preview_url against your order record.
  }

  res.json({ received: true });
});

The Vantane half, proven

The webhook handler above is illustrative (it depends on your Stripe integration), but the Vantane calls inside it are not — this is the exact same validate → create → deliver sequence, runnable right now:

curl -sS https://vantane.com/v1/validate \
  -H 'content-type: application/json' \
  -d '{"id":"STRIPE-DEMO-01","invoiceTypeCode":"380","issueDate":"2026-07-01","dueDate":"2026-07-31","currencyCode":"EUR","buyerReference":"PO-9001","seller":{"name":"Antwerpen Software BV","legalForm":"BV","vatId":"BE0876543270","registrationNumber":"0876543270","address":{"line1":"Meir 12","city":"Antwerpen","postalCode":"2000","countryCode":"BE"},"electronicAddress":{"scheme":"0208","value":"0876543270"}},"buyer":{"name":"Gent Logistics NV","vatId":"BE0123456749","registrationNumber":"0123456749","address":{"line1":"Korenmarkt 5","city":"Gent","postalCode":"9000","countryCode":"BE"},"electronicAddress":{"scheme":"0208","value":"0123456749"}},"lines":[{"id":"1","description":"Annual software subscription","quantity":1,"unitCode":"EA","unitPrice":4800,"vat":{"categoryCode":"S","rate":21}}],"paymentMeans":{"typeCode":"30","iban":"BE68539007547034","bic":"GKCCBEBB","accountName":"Antwerpen Software BV"},"paymentTerms":"Payment within 30 days"}'

curl -sS https://vantane.com/v1/invoices \
  -H 'authorization: Bearer vantane_sandbox_public' \
  -H 'content-type: application/json' \
  -d '{"invoice":{"id":"STRIPE-DEMO-01","invoiceTypeCode":"380","issueDate":"2026-07-01","dueDate":"2026-07-31","currencyCode":"EUR","buyerReference":"PO-9001","seller":{"name":"Antwerpen Software BV","legalForm":"BV","vatId":"BE0876543270","registrationNumber":"0876543270","address":{"line1":"Meir 12","city":"Antwerpen","postalCode":"2000","countryCode":"BE"},"electronicAddress":{"scheme":"0208","value":"0876543270"}},"buyer":{"name":"Gent Logistics NV","vatId":"BE0123456749","registrationNumber":"0123456749","address":{"line1":"Korenmarkt 5","city":"Gent","postalCode":"9000","countryCode":"BE"},"electronicAddress":{"scheme":"0208","value":"0123456749"}},"lines":[{"id":"1","description":"Annual software subscription","quantity":1,"unitCode":"EA","unitPrice":4800,"vat":{"categoryCode":"S","rate":21}}],"paymentMeans":{"typeCode":"30","iban":"BE68539007547034","bic":"GKCCBEBB","accountName":"Antwerpen Software BV"},"paymentTerms":"Payment within 30 days"},"options":{"deliver":true}}'

See also: Belgium · France · Germany