Stripe
Jede Rechnung, die Stripe finalisiert, kann eine ZUGFeRD- / Factur-X-E-Rechnung werden: dieselbe Nummer, dieselben Positionen, dieselbe Umsatzsteuer und dieselben Summen, als PDF/A-3 mit der Rechnung als XML darin, geprüft, bevor Sie sie erhalten. In Stripe wird nichts installiert. Ein Webhook-Handler und die Funktion auf dieser Seite sind die ganze Integration: Die Funktion macht aus Stripes Rechnung den _invoice-Block, und ein Rendering erzeugt die Datei.
Vorbereitung
- Eine Vorlage, die den
_invoice-Block druckt. Vorlagen → Beispiele → E-Rechnung in der App legt eine Kopie einer solchen Vorlage in Ihrem Workspace an, mit dem Sluge-rechnung, den der Handler unten rendert, sofern Sie ihr dort keinen anderen geben. Eine Stripe-Rechnung braucht ein paar Änderungen daran; Die Vorlage zeigt sie. - Ein API-Schlüssel von Formfeed. Die Standard-Scopes genügen. Beginnen Sie mit einem
ff_test_-Schlüssel: Seine E-Rechnungen sind gültige Dateien mit der Testmarkierung auf jeder Seite, bis zu einer Anzahl pro Tag, die vom Plan abhängt. Live-E-Rechnungen setzen den Starter-Plan oder höher voraus. - Stripes API-Version 2025-03-31 oder später, die jedes
stripe-Paket ab Version 18 verwendet: Die Zuordnung liest dietaxeseiner Rechnungsposition, die mit dieser Version kamen. Ein eingeschränkter Schlüssel braucht Schreibzugriff auf Rechnungen (der Handler liest sie, listet ihre Positionen und vermerkt das Rendering in ihren Metadaten) und Lesezugriff auf Steuersätze.
Die Zuordnung
stripeToInvoice nimmt die Rechnung, wie Stripes API sie liefert, und alle ihre Positionen, gelistet mit aufgelösten Steuersätzen, und gibt den _invoice-Block zurück. Sie übernimmt Stripes Beträge und berechnet nichts, was sie falsch berechnen könnte:
- Eine Position je Rechnungsposition. Der Name ist die Beschreibung der Position, Menge und Preis sind die von Stripe, der Nettobetrag ist der Betrag, auf den die Umsatzsteuer berechnet wird. Ein Rabatt oder ein Guthaben bleibt als Nachlass an seiner Position sichtbar. Eine Position, deren Preis die Umsatzsteuer enthält, wird netto geschrieben, mit dem Rabatt im Preis. Eine Gutschrift, etwa die ungenutzte Zeit eines Abonnements nach einem Upgrade, ist eine negative Menge zu einem positiven Preis, wie die EN 16931 es verlangt.
- Die Umsatzsteuer aus Stripes eigenem Grund. Eine Position zum Regelsatz oder zu einem ermäßigten Satz ist Kategorie
Smit dem Satz, den Stripe angewendet hat. Jeder andere Grund (Reverse Charge, steuerfrei, nicht steuerbar, keine Registrierung) und eine Position ganz ohne Steuer sind Kategorien, die Sie in der Konfiguration festlegen. Ein unbekannter Grund löst einen Fehler aus, statt zu raten. - Summen aus den Positionen, an Stripes Gesamtbetrag geprüft. Ergeben die Positionen nicht Stripes Gesamtbetrag, löst die Funktion einen Fehler aus. Was schon bezahlt oder mit dem Guthaben des Kunden verrechnet ist, ist
prepaid, und Stripes offener Betrag istdue. - Der Käufer, wie Stripe ihn festgehalten hat. Name, Adresse, EU-USt-IdNr. und E-Mail sind die
customer_*-Felder der Rechnung, die sich nach dem Finalisieren nicht mehr ändern. Die E-Mail wird die elektronische Adresse des Käufers. - Daten in Ihrer Zeitzone. Das Rechnungsdatum ist das Ausstellungsdatum der Rechnung (
effective_at), die Zeiträume der Positionen sind ihre Leistungszeiträume. Einesend_invoice-Rechnung ist zu ihrem Fälligkeitsdatum fällig; eine, die Stripe automatisch einzieht, am Tag ihrer Ausstellung, denn dann zieht Stripe sie ein.
// Stripes Steuergründe (taxability reasons), die unabhängig vom Umsatz eine
// Umsatzsteuerkategorie der EN 16931 sind. Jeder andere Grund hängt vom Umsatz
// ab: Sie legen ihn in config.categories fest.
const STANDARD_RATED = ['standard_rated', 'reduced_rated'];
// Währungen, deren Beträge Stripe in ganzen Einheiten angibt, nicht in Cent.
const ZERO_DECIMAL =
'bif clp djf gnf jpy kmf krw mga pyg rwf ugx vnd vuv xaf xof xpf'.split(' ');
/**
* Eine finalisierte Stripe-Rechnung als `_invoice`-Block einer E-Rechnung.
*
* `invoice` ist die Rechnung, wie Stripes API sie liefert; `lines` sind alle
* ihre Positionen, gelistet mit `expand[]=data.taxes.tax_rate_details.tax_rate`.
* Jeder Betrag wird aus Stripe übernommen, nie aus einem Preis errechnet: Eine
* Summe, die von Stripes abweicht, ist ein Fehler, keine Rundung.
*/
export function stripeToInvoice(invoice, lines, config) {
const where = `Rechnung ${invoice.number ?? invoice.id}`;
const fail = (message) => {
throw new Error(`${where}: ${message}`);
};
if (!invoice.number) fail('ein Entwurf hat keine Nummer; erst finalisieren');
if (invoice.amount_shipping) fail('Versandkosten werden nicht abgebildet');
if (ZERO_DECIMAL.includes(invoice.currency)) fail('eine Währung ohne Cent');
const money = (cents) => (cents / 100).toFixed(2);
const sum = (list) =>
(list ?? []).reduce((all, item) => all + item.amount, 0);
const day = (seconds) => {
const parts = new Intl.DateTimeFormat('en', {
timeZone: config.timeZone ?? 'UTC',
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).formatToParts(new Date(seconds * 1000));
const [y, m, d] = ['year', 'month', 'day'].map(
(type) => parts.find((part) => part.type === type).value,
);
return `${y}-${m}-${d}`;
};
// Stripe schreibt null, wo ein Wert fehlt; der Block lässt das Feld weg
const present = (object) =>
Object.fromEntries(
Object.entries(object).filter(
([, value]) => value != null && value !== '',
),
);
const breakdown = new Map(); // ein Eintrag je Umsatzsteuerkategorie und Satz
let lineNet = 0;
let taxTotal = 0;
const invoiceLines = lines.map((line, index) => {
const at = `Position ${index + 1}`;
const taxes = line.taxes ?? [];
if (taxes.length > 1) fail(`${at} trägt ${taxes.length} Steuern`);
const tax = taxes[0];
const vat = vatOf(tax, config) ?? fail(`${at}: ${unknownVat(tax)}`);
const inclusive = tax?.tax_behavior === 'inclusive';
// Rabatte und Guthaben (credit grants); die erste Liste enthält beides
const credits =
sum(line.pretax_credit_amounts) || sum(line.discount_amounts);
const taxAmount = tax?.amount ?? 0;
// worauf die Umsatzsteuer berechnet wird: der Betrag nach Rabatt, netto
const whole = line.amount - credits - (inclusive ? taxAmount : 0);
const net = tax?.taxable_amount ?? whole;
if (net !== whole) fail(`${at} ist nur zum Teil besteuert`);
// ein Bruttopreis wird netto geschrieben, der Rabatt im Preis; jede andere
// Position behält Stripes Betrag und zeigt den Rabatt
const amount = inclusive ? net : line.amount;
const quantity = Number(line.quantity_decimal ?? line.quantity ?? 1);
const price = quantity ? Math.abs(amount) / 100 / quantity : 0;
lineNet += net;
taxTotal += taxAmount;
const key = `${vat.category} ${vat.rate}`;
const entry = breakdown.get(key) ?? { vat, basis: 0, amount: 0 };
entry.basis += net;
entry.amount += taxAmount;
breakdown.set(key, entry);
const reason = config.discountReason ?? 'Rabatt';
return present({
id: String(index + 1),
name: line.description || 'Position',
// eine Gutschrift (ungenutzte Zeit nach Upgrade) ist eine negative Menge
quantity: amount < 0 ? -quantity : quantity,
unit_code: 'C62',
net_price: String(Number(price.toFixed(6))),
net_amount: money(net),
allowances:
amount === net ? undefined : [{ amount: money(amount - net), reason }],
tax: present({ category: vat.category, rate: vat.rate }),
period: { start: day(line.period.start), end: day(line.period.end) },
});
});
const grand = lineNet + taxTotal;
if (grand !== invoice.total)
fail(
`die Positionen ergeben ${money(grand)}, nicht ${money(invoice.total)}`,
);
// schon bezahlt oder mit dem Guthaben des Kunden verrechnet
const prepaid = invoice.total - invoice.amount_remaining;
if (prepaid < 0)
fail('sie zieht auch einen Betrag aus früheren Rechnungen ein');
// eine nicht steuerbare Rechnung nennt keine USt-IdNr. (BR-O-02)
const outOfScope = [...breakdown.values()].some(
({ vat }) => vat.category === 'O',
);
const vatId = (id) => (outOfScope ? undefined : id);
const address = invoice.customer_address ?? {};
const issued =
invoice.effective_at ?? invoice.status_transitions.finalized_at;
const transfer =
invoice.collection_method === 'send_invoice' ? config.transfer : null;
const notes = [invoice.description, invoice.footer]
.filter(Boolean)
.map((text) => ({ text }));
return present({
number: invoice.number,
issue_date: day(issued),
currency: invoice.currency.toUpperCase(),
notes: notes.length > 0 ? notes : undefined,
seller: present({ ...config.seller, vat_id: vatId(config.seller.vat_id) }),
buyer: present({
name: invoice.customer_name,
vat_id: vatId(
invoice.customer_tax_ids?.find((id) => id.type === 'eu_vat')?.value,
),
address: present({
street: address.line1,
street2: address.line2,
postcode: address.postal_code,
city: address.city,
subdivision: address.state,
country: address.country,
}),
electronic_address: invoice.customer_email,
}),
lines: invoiceLines,
tax: {
breakdown: [...breakdown.values()].map(({ vat, basis, amount }) =>
present({ ...vat, basis: money(basis), amount: money(amount) }),
),
},
totals: present({
line_net: money(lineNet),
tax_basis: money(lineNet),
tax_total: money(taxTotal),
grand: money(grand),
prepaid: prepaid > 0 ? money(prepaid) : undefined,
due: money(invoice.amount_remaining),
}),
payment: present({
// Stripe bucht automatische Rechnungen sofort ab: fällig bei Ausstellung
due_date: day(invoice.due_date ?? issued),
means_code: transfer ? '58' : undefined,
iban: transfer?.iban,
bic: transfer?.bic,
reference: transfer ? invoice.number : undefined,
terms: config.terms?.[invoice.collection_method],
}),
});
}
/** Kategorie und Satz einer Position, aus Stripes Grund für ihre Steuer. */
function vatOf(tax, config) {
const rateOf = (category, percent) =>
category === 'S' ? percent : category === 'O' ? undefined : 0;
if (!tax)
return (
config.untaxed && {
...config.untaxed,
rate: rateOf(config.untaxed.category, 0),
}
);
const rate = tax.tax_rate_details?.tax_rate;
if (typeof rate !== 'object' || rate === null) return null;
const reason = tax.taxability_reason ?? 'not_available';
const percent = rate.effective_percentage ?? rate.percentage;
const stated = config.categories?.[rate.id] ?? config.categories?.[reason];
if (stated) return { ...stated, rate: rateOf(stated.category, percent) };
const standard =
STANDARD_RATED.includes(reason) ||
(reason === 'not_available' && percent > 0);
return standard ? { category: 'S', rate: percent } : null;
}
/** Warum `vatOf` keine Kategorie fand, wie der Fehler es sagt. */
function unknownVat(tax) {
const rate = tax?.tax_rate_details?.tax_rate;
if (!tax) return 'keine Steuer; ihre Kategorie gehört in config.untaxed';
if (!rate || typeof rate !== 'object')
return 'expand[]=data.taxes.tax_rate_details.tax_rate fehlt';
const reason = tax.taxability_reason ?? 'not_available';
return `die Kategorie von ${reason} (Steuersatz ${rate.id}) gehört in config.categories`;
}
Die Konfiguration
Was Stripe über eine Rechnung nicht weiß, kommt aus Ihrer Konfiguration: wer Sie sind und welche Umsatzsteuerkategorie die Steuern haben, die Stripe anwendet.
export const config = {
// die Zeitzone, in der Ihre Rechnungen datiert sind
timeZone: 'Europe/Berlin',
// Sie selbst, mit den Feldern von `seller` im _invoice-Block
seller: {
name: 'Fennlor Studio GmbH',
vat_id: 'DE000000000',
legal_registration: 'HRB 00000',
// Registergericht und Ähnliches, das die Fußzeile des Beispiels druckt
legal_info: 'Amtsgericht Musterstadt, HRB 00000',
address: {
street: 'Musterstraße 1',
postcode: '12345',
city: 'Musterstadt',
country: 'DE',
},
contact: { email: 'rechnung@fennlor.example' },
electronic_address: 'rechnung@fennlor.example',
},
// Stripes Steuergründe oder die IDs Ihrer Steuersätze als Kategorien. Welche
// Kategorie ein Umsatz hat, ist eine Steuerfrage: Klären Sie sie mit Ihrer
// Steuerberatung, bevor Sie eine Zeile ergänzen.
categories: {
// reverse_charge: { category: 'AE', exemption_reason_code: 'VATEX-EU-AE' },
},
// das Konto, auf das Kunden eine send_invoice-Rechnung überweisen
transfer: { iban: 'DE36 0000 0000 0000 0000 00' },
};
sellernimmt jedes Feld des Verkäufers im Block. Eine nicht steuerbare Rechnung (KategorieO) darf keine USt-IdNr. nennen, deshalb lässt die Funktionvat_idbei einer solchen Rechnung weg;legal_registrationidentifiziert Sie dann weiterhin, wie die EN 16931 es verlangt.legal_infosind die weiteren rechtlichen Angaben, die eine Rechnung nennt und die das Beispiel in seiner Fußzeile druckt.categoriesordnet einen Steuergrund von Stripe (taxability_reason) oder die ID eines Steuersatzes (txr_…, die vor dem Grund gilt) einer Kategorie aus der Tabelle der Umsatzsteuerkategorien zu und, für jede Kategorie außerSundZ, ihrenexemption_reasonoderexemption_reason_code. Stripesreverse_chargeumfasst, was die EN 16931 alsAE(Reverse Charge) undK(innergemeinschaftliche Lieferung) unterscheidet; welches von beiden ein Umsatz ist und welchen Grund die Rechnung angibt, entscheiden Sie mit Ihrer Steuerberatung. Ein manuell angelegter Steuersatz ohne Grund und über 0 % gilt als KategorieS.untaxedist die Kategorie einer Position, die gar keine Steuer trägt, etwa wenn Stripe für Ihr Konto keine Steuer berechnet:{ category: 'E', exemption_reason: '…' }mit dem Grund, den Ihre Rechnungen angeben. Ohne diese Angabe löst eine solche Position einen Fehler aus.transferist das Konto einersend_invoice-Rechnung: Die Rechnung nennt dann SEPA-Überweisung (58) auf diese IBAN mit der Rechnungsnummer als Verwendungszweck. Lassen Sie es weg, wenn Ihre Kunden auf Stripes gehosteter Seite bezahlen.terms, optional, ist eine Zahlungsbedingung je Einzugsart, etwa{ charge_automatically: 'Wird von der hinterlegten Karte abgebucht.' }.discountReasonbenennt den Nachlass, zu dem ein Rabatt wird (Rabatt, wenn nichts angegeben ist).
Der Webhook-Handler
Stripe sendet invoice.finalized, sobald eine Rechnung ihre Nummer hat. Der Handler antwortet sofort, liest die Rechnung und ihre Positionen noch einmal und rendert die E-Rechnung:
import { writeFile } from 'node:fs/promises';
import express from 'express';
import Stripe from 'stripe';
import { Formfeed } from '@formfeed/sdk';
import { stripeToInvoice } from './stripe-to-invoice.js';
import { config } from './config.js';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const formfeed = new Formfeed({ apiKey: process.env.FORMFEED_API_KEY });
const app = express();
app.post(
'/stripe/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
req.headers['stripe-signature'],
process.env.STRIPE_WEBHOOK_SECRET,
);
} catch {
return res.sendStatus(400);
}
// Stripe will eine schnelle Antwort; die E-Rechnung entsteht danach
res.sendStatus(200);
if (event.type !== 'invoice.finalized') return;
const id = event.data.object.id;
makeEinvoice(id).catch((error) => {
// eine abgelehnte Rechnung nennt jedes Feld und jede Regel, die es verletzt
console.error(
`Keine E-Rechnung für ${id}:`,
error.problem?.errors ?? error,
);
});
},
);
async function makeEinvoice(id) {
// die Rechnung neu lesen: Die Kopie im Ereignis ist so alt wie das Ereignis,
// und Stripe stellt ein Ereignis mehr als einmal zu
const invoice = await stripe.invoices.retrieve(id);
if (invoice.metadata.formfeed_render) return;
const lines = [];
for await (const line of stripe.invoices.listLineItems(id, {
limit: 100,
expand: ['data.taxes.tax_rate_details.tax_rate'],
}))
lines.push(line);
let render = await formfeed.renders.create(
{
template: 'e-rechnung',
data: { _invoice: stripeToInvoice(invoice, lines, config) },
post: { einvoice: {} },
filename: `${invoice.number}.pdf`,
meta: { stripe_invoice: id },
},
{ idempotencyKey: `stripe-${id}` },
);
// ein Rendering, das warten musste, läuft im Hintergrund weiter: Die API
// antwortet 202, und waitFor verfolgt es, bis es fertig ist
if (render.status !== 'succeeded')
render = await formfeed.renders.waitFor(render.id);
if (render.status === 'failed')
throw new Error(`${render.id} fehlgeschlagen: ${render.error?.detail}`);
await stripe.invoices.update(id, {
metadata: { formfeed_render: render.id },
});
// ab hier gehört die Datei Ihnen: archivieren, an den Kunden senden
await writeFile(
`${invoice.number}.pdf`,
await formfeed.renders.download(render),
);
}
app.listen(3000);
- Erst die Antwort. Stripe verlangt ein schnelles
2xxund wiederholt ein Ereignis bis zu drei Tage lang, wenn keines kommt. Eine E-Rechnung dauert ein paar Sekunden, weil die Datei umgewandelt und geprüft wird, bevor sie zurückkommt; deshalb antwortet der Handler vor dem Rendering. Legen Sie die ID im Betrieb in eine Warteschlange, stattmakeEinvoicedirekt aufzurufen: Einen Fehler nach der Antwort wiederholt Stripe nicht. - Zweimal ist einmal. Der Idempotency-Key liefert 24 Stunden lang die erste Antwort erneut, und die Rendering-ID in den Metadaten der Rechnung hält eine spätere Zustellung auf. Ohne beides macht ein doppelt zugestelltes Ereignis zwei E-Rechnungen mit einer Nummer. Keines von beiden fängt zwei Zustellungen ab, die ankommen, während das erste Rendering noch läuft, denn der Key hält eine Antwort erst, wenn es eine gibt; eine Warteschlange, die eine Rechnung nach der anderen bearbeitet, schließt diese Lücke. Der Key liefert auch eine Ablehnung erneut: Rendern Sie nach dem Korrigieren dessen, was die API abgelehnt hat, mit einem neuen Key, etwa
stripe-${id}-2. - Ein Rendering kann unfertig zurückkommen. Wenn es auf einen freien Render-Slot warten muss, antwortet die API mit
202und stellt es im Hintergrund fertig (der automatische Fallback).waitForfragt das Rendering dann ab, bis es fertig ist, standardmäßig bis zu zwei Minuten lang, und der Handler wirft einen Fehler, wenn es fehlgeschlagen ist, sodass die Metadaten der Rechnung nur eine fertige E-Rechnung nennen. templateist der Slug Ihrer Kopie des Beispiels;post.einvoicemacht jede Vorlage, die den Block druckt, zur E-Rechnung, auch eine, die das nicht selbst festlegt. Die Optionen wählen das Profil und ob das XML auch als eigene Datei gespeichert wird.- Die Region.
new Formfeed({ apiKey })spricht mit der Region EU, der einzigen, die heute rendert (Datenresidenz). - Behalten Sie die Datei. Formfeed löscht Ausgaben, wenn die Aufbewahrungsfrist des Workspace endet, lange bevor Rechnungen vernichtet werden dürfen. Archivieren Sie die ausgelieferte Datei und daneben den
einvoice-Bericht des Renderings: siehe Archivierung.
Verletzt der Block eine Regel der EN 16931, wird das Rendering abgelehnt, bevor etwas gerendert oder berechnet wird, und einvoice_data_invalid nennt jedes Feld mit seiner Regel. Mit der Zuordnung oben heißt das: In Stripe fehlen Daten, meistens die Adresse oder das Land eines Kunden.
Die Vorlage
Das Beispiel druckt die Positionen, die Umsatzsteuer je Satz, den Gesamtbetrag und einen Absatz zur Zahlung mit dem Konto, auf das überwiesen wird. Eine Stripe-Rechnung unterscheidet sich in einigen Punkten von den Daten des Beispiels, und die Vorlage muss folgen:
- Kein Lieferdatum. Stripe nennt stattdessen für jede Position einen Leistungszeitraum, den die Zuordnung als
periodder Position schreibt. Die Zeile Leistungsdatum des Beispiels druckt_invoice.delivery.date, und der Helferdatedruckt für einen fehlenden Wert das aktuelle Datum: Nehmen Sie die Zeile heraus oder drucken Sie sie nur{% if _invoice.delivery %}. - Leistungszeiträume, Rabatte und ein schon bezahlter Betrag, die das Beispiel nicht druckt.
- Kein Konto bei einer Rechnung, die Stripe automatisch einzieht: Zeigen Sie den Absatz zur Zahlung nur, wenn die Rechnung eines nennt.
- Mehr als ein Steuersatz. Das Beispiel druckt die Umsatzsteuer je Satz, aber nicht die Summe der Umsatzsteuer, nach der die Sichtprüfung sucht.
Drucken Sie sie aus demselben Block, in der Schleife des Beispiels über die Positionen und unter seinen Summen. Das Beispiel ist auch eine PDF/UA-1-Datei (pdf.ua in seinen Einstellungen), deshalb behalten die Zeilen ihre Kopfzellen (th scope="row"), wie seine eigenen Zeilen, und keine Zelle einer Tabelle mit Kopfzellen darf leer bleiben: Eine strenge Prüfung lässt das Rendering mit pdfua_validation_failed scheitern. Siehe Barrierefreie PDFs.
<td>
{{ line.name }} {% if line.period %}
<div class="muted">
{{ line.period.start | date('PP') }} – {{ line.period.end | date('PP') }}
</div>
{% endif %} {% for allowance in line.allowances %}
<div class="muted">
{{ allowance.reason }} −{{ allowance.amount | money }}
</div>
{% endfor %}
</td>
{% if _invoice.tax.breakdown | length > 1 %}
<tr>
<th scope="row">USt. gesamt</th>
<td class="num">{{ _invoice.totals.tax_total | money }}</td>
</tr>
{% endif %} {% if _invoice.totals.prepaid %}
<tr>
<th scope="row">Bezahlt</th>
<td class="num">{{ _invoice.totals.prepaid | money }}</td>
</tr>
<tr>
<th scope="row">Offener Betrag</th>
<td class="num">{{ _invoice.totals.due | money }}</td>
</tr>
{% endif %} {% if _invoice.payment.iban %}
<p class="pay">…</p>
{% endif %}
Die Sichtprüfung sucht im Text des PDFs die Rechnungsnummer, die Nettosumme, die Summe der Umsatzsteuer, den Gesamtbetrag, den offenen Betrag, wo er vom Gesamtbetrag abweicht, und die Umsatzsteuer je Satz, wo es mehr als einen gibt. Mit diesen Zeilen zeigt eine Kopie des Beispiels alle davon. Einen Wert, den sie nicht findet, meldet sie in einvoice.display.missing des Renderings, und das Rendering gelingt trotzdem, sofern die Vorlage nicht display_check: "strict" festlegt.
In n8n
Die Funktion braucht nichts als JavaScript, deshalb läuft sie auch in n8n:
-
Ein Stripe Trigger-Node für das Ereignis
invoice.finalized. -
Ein HTTP Request-Node mit Ihrem Stripe-Credential,
GET https://api.stripe.com/v1/invoices/{{ $json.data.object.id }}/lines?limit=100&expand[]=data.taxes.tax_rate_details.tax_rate, benannt Stripe lines. -
Ein Code-Node mit diesem Code unter der Funktion und der Konfiguration, beide ohne das Wort
exporteingefügt:Code-Nodeconst invoice = $('Stripe Trigger').first().json.data.object;const lines = $('Stripe lines').first().json.data;return [{ json: { _invoice: stripeToInvoice(invoice, lines, config) } }]; -
Der Formfeed-Node: Render → Create mit Ihrer Kopie des Beispiels, die die E-Rechnung selbst festlegt, und
{{ JSON.stringify($json) }}als Data (JSON).
Eine Rechnung mit mehr als 100 Positionen braucht die Paginierung des HTTP Request-Nodes. Der Workflow prüft keine Metadaten; ob eine Rechnung schon erzeugt wurde, zeigt die Ausführungshistorie von n8n.
Was die Zuordnung nicht abdeckt
- Gutschriften. Stripe führt sie als eigene Objekte. Eine Gutschrift als E-Rechnung ist Dokumentart
381mit der Rechnung, die sie korrigiert, inpreceding_invoices; die Funktion bildet nur Rechnungen ab. - Versandkosten, ein Saldo aus früheren Rechnungen, der mit dieser eingezogen wird, eine Position mit mehr als einer Steuer oder nur zum Teil besteuert und Währungen ohne Cent lösen einen Fehler aus, der die Rechnung nennt, statt eine E-Rechnung zu erzeugen, die von Stripes abweicht.
- Stripes eigene Rechnungs-E-Mail. Wenn Stripe auch sein PDF der Rechnung versendet, erhält Ihr Kunde zwei Dokumente mit einer Nummer. Welches davon die Rechnung ist, die Sie stellen, entscheiden Sie; ob Stripe sein eigenes versendet, legen die E-Mail-Einstellungen für Kunden in Stripe fest.
- XRechnung wird nicht angeboten; Formfeed erzeugt ZUGFeRD / Factur-X in den Profilen
en16931undbasic. Siehe Was Formfeed nicht tut. - Die umsatzsteuerliche Behandlung. Die Funktion übernimmt Stripes Steuern und schreibt die Kategorien, die Ihre Konfiguration nennt. Sie stellen die Rechnung aus und stehen für ihren Inhalt ein; Formfeed steht für das Format ein.