Zuwendungsbestätigungen per API
Aktualisiert am
Diese Anleitung ist für Vereine, die Spenden auf der eigenen Website über Stripe annehmen und dem Spender danach seine Zuwendungsbestätigung zeigen wollen. Sie beschreibt beide Seiten: wie die Zahlung bei Stripe als Spende markiert wird und wie die Website die Bestätigung bei Rechnungskit abholt. Was die Bestätigung enthält und wie der Verein sie einrichtet, steht in der Hilfe zu Spendenquittungen (siehe spendenquittungen).
Voraussetzungen
- Stripe ist in Rechnungskit verbunden (siehe stripe). Rechnungskit erfährt von der Zahlung über den Stripe-Webhook, nicht über deine Website.
- Spendenquittungen sind für das Konto freigeschaltet und unter Einstellungen → Spendenquittungen vollständig eingerichtet. Sonst antwortet die API mit
403 donations_not_enabled, oder die Spende bleibt aufreceipt_pending. - Ein API-Schlüssel mit der Berechtigung
documents:read. Du legst ihn unter Verbindungen → API an. Für diese Integration braucht er keine weitere Berechtigung.

| Punkt | So ist es |
|---|---|
| Basis-URL | https://api.rechnungskit.de/v1 |
| Authentifizierung | Authorization: Bearer rk_key_… |
| Berechtigung | documents:read |
| Format | JSON, Beträge in ganzzahligen Cent |
| Rate-Limit | 300 Anfragen pro Minute je Schlüssel, kurzfristig bis 600 am Stück. Jede Antwort trägt RateLimit-Limit und RateLimit-Remaining, bei Überschreitung kommt 429 rate_limited mit Retry-After |
| Maschinenlesbar | https://api.rechnungskit.de/v1/openapi.json |
Der volle Schlüssel wird beim Anlegen genau einmal angezeigt. Er gehört in die Umgebungsvariablen deines Servers, nie in JavaScript, das im Browser läuft.
Spende im Stripe Checkout markieren
Rechnungskit macht eine Zahlung nur dann zur Zuwendung, wenn sie markiert ist. Bei einer einzelnen Spende stehen die Metadaten am PaymentIntent. Im Checkout setzt du sie über payment_intent_data.metadata:
| Schlüssel | Wert | Bedeutung |
|---|---|---|
rk_kind |
donation oder membership_fee |
Pflicht. Spende oder Mitgliedsbeitrag. Die deutschen Werte spende und mitgliedsbeitrag gehen auch, Groß- und Kleinschreibung ist egal. Andere Werte macht Rechnungskit nicht zur Zuwendung |
rk_purpose |
freier Text | optional, z. B. eine Kampagne. Steht unter Spenden als Zweck |
rk_recurring |
true |
optional, für Dauerspenden und Patenschaften, die du ohne Stripe-Abo abbuchst. Bei Stripe-Abos setzt Rechnungskit es selbst |
Dazu kommt billing_address_collection: 'required'. Ohne Anschrift gibt es keine Zuwendungsbestätigung.
Beispiel mit stripe-node auf dem Server der Website:
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [
{
price_data: {
currency: 'eur',
unit_amount: 5000, // 50,00 € aus deinem Spendenformular, in Cent
product_data: { name: 'Spende Naturschutz' }
},
quantity: 1
}
],
billing_address_collection: 'required',
customer_email: 'jonas.becker@example.org', // optional, aus dem Formular
payment_intent_data: {
metadata: { rk_kind: 'donation', rk_purpose: 'Streuobstwiese' }
},
success_url: 'https://verein.example/danke?session_id={CHECKOUT_SESSION_ID}',
cancel_url: 'https://verein.example/spenden'
});
// Den Spender zu session.url weiterleiten.
Dasselbe mit curl:
curl https://api.stripe.com/v1/checkout/sessions \
-u "$STRIPE_SECRET_KEY:" \
-d mode=payment \
-d "line_items[0][price_data][currency]=eur" \
-d "line_items[0][price_data][unit_amount]=5000" \
-d "line_items[0][price_data][product_data][name]=Spende Naturschutz" \
-d "line_items[0][quantity]=1" \
-d billing_address_collection=required \
-d "payment_intent_data[metadata][rk_kind]=donation" \
-d "payment_intent_data[metadata][rk_purpose]=Streuobstwiese" \
--data-urlencode "success_url=https://verein.example/danke?session_id={CHECKOUT_SESSION_ID}" \
-d cancel_url=https://verein.example/spenden
Auf der Danke-Seite liest dein Server die Session und nimmt die ID des PaymentIntent als paymentRef:
const session = await stripe.checkout.sessions.retrieve(req.query.session_id);
const paymentRef = session.payment_intent; // "pi_…"
Andere Wege zur Zahlung
- Stripe-Abo (Dauerspende, Patenschaft, Mitgliedsbeitrag):
mode: 'subscription'und die Markierung insubscription_data.metadata, z. B.{ rk_kind: 'membership_fee' }. Alternativ steht sie am Preis. Rechnungskit liest sie aus jeder bezahlten Stripe-Rechnung des Abos und bucht jede Abbuchung als wiederkehrende Zuwendung. - Payment Links: per API mit
payment_intent_data.metadatabzw.subscription_data.metadata. Metadaten, die nur am Payment Link oder an der Checkout-Session stehen, liest Rechnungskit nicht. - Eigene PaymentIntents (z. B. off-session abgebuchte Patenschaften ohne Stripe-Abo):
metadatadirekt am PaymentIntent setzen, für wiederkehrende Beträge zusätzlichrk_recurring: 'true'. - Markierung am Preis wirkt nur bei Abos. Bei Einzelzahlungen zählt allein die Markierung am PaymentIntent.
Spende und Verkauf nie mischen
Eine markierte Zahlung ist als Ganzes eine Zuwendung. Leg Spende und Verkauf (Kalender, T-Shirt, Ticket) deshalb nie in dieselbe Checkout-Session. Bei Abos kommt es darauf an, wo die Markierung steht: Steht sie am Abo, gilt jede bezahlte Rechnung des Abos als Ganzes als Zuwendung, auch eine Verkaufsposition darauf. Steht sie am Preis, zählt eine Rechnung nur dann als Zuwendung, wenn jede Position dieselbe Markierung trägt, sonst läuft die Zahlung als Verkauf weiter. Trenn Spende und Verkauf deshalb immer in zwei Abos.
Testmodus
Solange das Rechnungskit-Projekt im Testmodus ist, verbindest du Stripe mit Test-Schlüsseln und zahlst mit Stripe-Testkarten. Es entstehen Testbelege (TEST-ZB-…), und derselbe API-Schlüssel sieht nur diese. Nach dem Go-Live sieht er nur echte Bestätigungen, du musst ihn nicht tauschen (siehe Testmodus (Sandbox)).
Bestätigung abrufen
GET https://api.rechnungskit.de/v1/donation-receipts?paymentRef=pi_3QdemoA1LindentalSp01
Authorization: Bearer rk_key_…
| Parameter | Ort | Pflicht | Bedeutung |
|---|---|---|---|
paymentRef |
Query | ja | ID der Zahlung beim Zahlungsanbieter, bei Stripe der PaymentIntent (pi_…). Die Charge-ID (ch_…) funktioniert ebenfalls. Höchstens 200 Zeichen |
Antwort 200 (echte Ausgabe mit Demo-Daten):
{
"paymentRef": "pi_3QdemoA1LindentalSp01",
"donation": {
"amountCents": 5000,
"refundedCents": 0,
"receivedOn": "2026-10-28"
},
"data": [
{
"number": "ZB-2026-0001",
"kind": "single",
"issueDate": "2026-11-09",
"totalCents": 5000,
"currency": "EUR",
"donorName": "Jonas Becker",
"withdrawsNumber": null,
"withdrawn": false,
"sha256": "a88c22710c51cacd381cadb93eda05960ceb051c66a86602a376c75b0c27643e",
"test": false,
"pdfPath": "/v1/donation-receipts/ZB-2026-0001/pdf"
}
]
}
| Feld | Bedeutung |
|---|---|
paymentRef |
die abgefragte Zahlungs-ID |
donation.amountCents |
Betrag der Zuwendung in Cent |
donation.refundedCents |
bisher erstatteter Betrag in Cent |
donation.receivedOn |
Tag der Zuwendung (YYYY-MM-DD) |
data[] |
alle Bestätigungen zu dieser Zahlung samt Widerrufen, sortiert nach Ausstellungsdatum und Nummer |
data[].number |
Nummer der Bestätigung, ZB-JJJJ-NNNN, im Testmodus TEST-ZB-JJJJ-NNNN |
data[].kind |
single (Einzelbestätigung), collective (Sammelbestätigung) oder withdrawal (Widerruf) |
data[].issueDate |
Ausstellungsdatum (YYYY-MM-DD) |
data[].totalCents |
bestätigter Betrag. Bei einer Sammelbestätigung die Summe aller Zuwendungen auf ihr, bei einem Widerruf der erstattete Betrag |
data[].currency |
Währung, EUR |
data[].donorName |
Name des Spenders, wie er auf der Bestätigung steht |
data[].withdrawsNumber |
nur bei withdrawal: die Nummer der widerrufenen Bestätigung, sonst null |
data[].withdrawn |
true, wenn ein späterer Widerruf diese Bestätigung ungültig gemacht hat |
data[].sha256 |
SHA-256 des archivierten Originals |
data[].test |
true bei Testbelegen |
data[].pdfPath |
Pfad zum PDF ab dem Host: https://api.rechnungskit.de + pdfPath |
Die gültige Bestätigung ist der Eintrag mit kind ungleich withdrawal und withdrawn: false. Es gibt höchstens einen solchen Eintrag je Zahlung.
Status- und Fehlercodes
Fehler kommen immer im selben Format, mit Request-ID im Header X-Request-Id:
{
"error": {
"code": "receipt_pending",
"message": "The donation is booked, its receipt is not issued yet (missing address, yearly mode or pending issue). Retry later.",
"details": {
"donation": { "amountCents": 3000, "refundedCents": 0, "receivedOn": "2026-11-08" }
}
},
"requestId": "110a0117-1dc5-4fdc-a082-b601f988ac88"
}
| Status | error.code |
Bedeutung | Was deine Website tut |
|---|---|---|---|
| 200 | Zuwendung und Bestätigungen | gültige Bestätigung anzeigen | |
| 400 | invalid_request |
paymentRef fehlt |
Aufruf korrigieren, nicht wiederholen |
| 401 | unauthorized |
Schlüssel fehlt, ist ungültig, abgelaufen oder widerrufen | Schlüssel prüfen |
| 403 | insufficient_scope |
Schlüssel hat kein documents:read. details nennt erforderliche und vorhandene Berechtigungen |
neuen Schlüssel mit documents:read anlegen |
| 403 | donations_not_enabled |
Spendenquittungen sind für das Konto nicht freigeschaltet | an support@rechnungskit.de wenden |
| 404 | not_found |
Rechnungskit kennt zu dieser Zahlung (noch) keine Zuwendung | kurz mit Backoff wiederholen, siehe unten |
| 404 | receipt_pending |
Zuwendung gebucht, Bestätigung noch nicht ausgestellt. details.donation zeigt Betrag und Tag |
Hinweis zeigen, später erneut fragen |
| 429 | rate_limited |
Rate-Limit überschritten | Retry-After abwarten |
| 5xx | vorübergehender Serverfehler | mit Backoff wiederholen |
not_found bleibt dauerhaft, wenn die Zahlung nicht markiert war, im falschen Modus (Test oder echt) lief oder zu einem anderen Projekt gehört.
Wann die Bestätigung da ist
Webhooks für Zuwendungsbestätigungen gibt es nicht. Deine Website fragt nach (Polling). Wann es etwas zu holen gibt, hängt vom Fall ab:
- Einzelspende im Modus „Sofort, je Zahlung“ mit Anschrift: Rechnungskit bucht die Zuwendung und stellt die Bestätigung noch während der Verarbeitung des Stripe-Webhooks
payment_intent.succeededaus. Die Bestätigung geht gleichzeitig per E-Mail an den Spender. - Anschrift fehlt: Rechnungskit fragt sie bei Stripe nach. Klappt das nicht, bleibt die Spende auf
receipt_pending, bis der Verein sie unter Spenden ergänzt. Ein täglicher Lauf um 06:15 Uhr stellt dann aus, was fällig ist. - Modus „Jährlich als Sammelbestätigung“: Die Spende bleibt bis nach Jahresende auf
receipt_pending. Erst wenn der Verein das Jahr unter Spenden abschließt, liefert die API die Sammelbestätigung.
Empfohlenes Verhalten auf der Danke-Seite:
- Erste Abfrage nach etwa 2 Sekunden, dann mit wachsendem Abstand (2, 4, 8, 16, 30 Sekunden), insgesamt höchstens etwa 2 Minuten. Solange kommt
not_found, wenn der Stripe-Webhook noch unterwegs ist. - Bei
200die gültige Bestätigung zeigen und das PDF anbieten. - Bei
receipt_pendingnicht weiter in kurzen Abständen fragen. Zeig einen Hinweis wie „Deine Zuwendungsbestätigung bekommst du per E-Mail“ und frag, falls nötig, höchstens einmal am Tag erneut. - Kommt nach 2 Minuten immer noch
not_found, denselben Hinweis zeigen. Die Bestätigung kommt in jedem Fall per E-Mail, wenn sie ausgestellt ist.
const BASE = 'https://api.rechnungskit.de/v1';
async function fetchReceipt(paymentRef) {
const delays = [2000, 2000, 4000, 8000, 16000, 30000, 30000, 30000];
for (const ms of delays) {
await new Promise((r) => setTimeout(r, ms));
const res = await fetch(
`${BASE}/donation-receipts?paymentRef=${encodeURIComponent(paymentRef)}`,
{ headers: { Authorization: `Bearer ${process.env.RECHNUNGSKIT_API_KEY}` } }
);
if (res.status === 200) {
const body = await res.json();
return body.data.find((r) => r.kind !== 'withdrawal' && !r.withdrawn) ?? null;
}
const err = await res.json().catch(() => null);
if (res.status === 404 && err?.error?.code === 'receipt_pending') return 'pending';
if (res.status === 429) {
await new Promise((r) => setTimeout(r, Number(res.headers.get('retry-after') ?? 5) * 1000));
continue;
}
if (res.status === 404 || res.status >= 500) continue; // Webhook noch unterwegs
throw new Error(`Rechnungskit ${res.status} ${err?.error?.code}`);
}
return 'pending';
}
Erstattung: Widerruf und Ersatzbestätigung
Erstattet der Verein eine bestätigte Spende ganz oder teilweise bei Stripe, widerruft Rechnungskit die Bestätigung mit einem eigenen, nummerierten Schreiben und stellt über den verbleibenden Betrag eine neue Bestätigung aus. Die API liefert dann alle drei Einträge. Beispiel: 150,00 € gespendet, 50,00 € erstattet:
{
"paymentRef": "pi_3QdemoA1LindentalSp02",
"donation": { "amountCents": 15000, "refundedCents": 5000, "receivedOn": "2026-11-02" },
"data": [
{ "number": "ZB-2026-0002", "kind": "single", "totalCents": 15000, "withdrawsNumber": null, "withdrawn": true, "pdfPath": "/v1/donation-receipts/ZB-2026-0002/pdf", "…": "…" },
{ "number": "ZB-2026-0006", "kind": "withdrawal", "totalCents": 5000, "withdrawsNumber": "ZB-2026-0002", "withdrawn": false, "pdfPath": "/v1/donation-receipts/ZB-2026-0006/pdf", "…": "…" },
{ "number": "ZB-2026-0007", "kind": "single", "totalCents": 10000, "withdrawsNumber": null, "withdrawn": false, "pdfPath": "/v1/donation-receipts/ZB-2026-0007/pdf", "…": "…" }
]
}
(Gekürzt: Jeder Eintrag hat dieselben Felder wie oben.) Gültig ist nur ZB-2026-0007. Der Widerruf trägt als totalCents den erstatteten Betrag. Wird alles erstattet, gibt es nur Bestätigung und Widerruf, keine gültige Bestätigung mehr. Widerruf und Ersatzbestätigung verschickt Rechnungskit nicht selbst per E-Mail, der Verein schickt sie unter Spenden mit „Erneut senden“. Wer Spendern ein Konto auf der Website anbietet, kann beide dort auch selbst zeigen.
Sammelbestätigung
Im Modus „Jährlich als Sammelbestätigung“ stehen alle Zuwendungen eines Spenders aus einem Kalenderjahr auf einer Bestätigung mit Anlage. Für jede dieser Zahlungen liefert die API dieselbe Bestätigung mit kind: "collective" und der Jahressumme in totalCents:
{
"paymentRef": "pi_3QdemoA1LindentalPa01",
"donation": { "amountCents": 1500, "refundedCents": 0, "receivedOn": "2025-10-15" },
"data": [
{ "number": "ZB-2026-0005", "kind": "collective", "issueDate": "2026-11-09", "totalCents": 6000, "donorName": "Miriam Schulte", "withdrawn": false, "pdfPath": "/v1/donation-receipts/ZB-2026-0005/pdf", "…": "…" }
]
}
Die Nummer trägt das Jahr der Ausstellung, nicht das Jahr der Spenden. Wer nur jährlich bestätigt, braucht auf der Danke-Seite meist kein Polling: Dort steht dann „Deine Sammelbestätigung kommt nach Jahresende“.
PDF laden
curl -o ZB-2026-0001.pdf \
-H "Authorization: Bearer $RECHNUNGSKIT_API_KEY" \
https://api.rechnungskit.de/v1/donation-receipts/ZB-2026-0001/pdf
pdfPath aus der Antwort ist der Pfad ab dem Host: https://api.rechnungskit.de + pdfPath. Die Nummer im Pfad ist URL-kodiert.
| Header | Wert |
|---|---|
Content-Type |
application/pdf |
Content-Disposition |
attachment; filename="ZB-2026-0001.pdf" |
Cache-Control |
private, no-store |
X-SHA256 |
SHA-256 des archivierten Originals, gleich dem Feld sha256 |
X-Request-Id |
Request-ID |
Das PDF entsteht bei jedem Abruf neu aus dem archivierten Original, nachdem Rechnungskit dessen Prüfsumme kontrolliert hat. X-SHA256 gehört deshalb zum Original, nicht zu den Bytes des PDF: Vergleich die Prüfsumme nicht mit dem heruntergeladenen PDF. Weil Cache-Control: no-store gilt, lädt dein Server das PDF bei Bedarf neu oder speichert es selbst. Eine unbekannte Nummer, eine Nummer aus dem anderen Modus oder aus einem anderen Projekt ergibt 404 not_found. Der Endpunkt liefert auch Sammelbestätigungen und Widerrufe.
Beispiel für eine Route auf der Website, die das PDF an den Spender weiterreicht:
app.get('/spende/bestaetigung/:number', async (req, res) => {
// Vorher prüfen, dass diese Nummer zur Zahlung des Spenders gehört
// (z. B. über die gespeicherte paymentRef seiner Checkout-Session).
const r = await fetch(
`https://api.rechnungskit.de/v1/donation-receipts/${encodeURIComponent(req.params.number)}/pdf`,
{ headers: { Authorization: `Bearer ${process.env.RECHNUNGSKIT_API_KEY}` } }
);
if (!r.ok) return res.status(r.status === 404 ? 404 : 502).end();
res.set('Content-Type', 'application/pdf');
res.set('Content-Disposition', r.headers.get('content-disposition'));
res.set('Cache-Control', 'private, no-store');
res.send(Buffer.from(await r.arrayBuffer()));
});
Sicherheit
- Der API-Schlüssel steht nur auf dem Server. Er kann jede Bestätigung des Projekts lesen.
- Eine Bestätigung enthält Name und Anschrift des Spenders. Gib das PDF nur an die Person heraus, die gespendet hat, etwa indem du die
paymentRefan ihre Checkout-Session bindest. Verwende keine erratbaren, öffentlichen Links. - Mandant und Projekt ergeben sich immer aus dem Schlüssel, nie aus der Anfrage.
So findest du es in der App
Rechnungskit ist keine Steuerberatung und keine Rechtsberatung. Dieser Beitrag erklärt allgemeine Zusammenhänge und ersetzt weder die Beratung durch deine Steuerkanzlei (§ 5 StBerG) noch eine Rechtsberatung (§ 3 RDG). Rechnungskit richtet sich an in Deutschland ansässige Unternehmen und bereitet Belege, Steuersätze und Buchungen automatisch auf. Wie dein Einzelfall zu bewerten ist, entscheidest du, im Zweifel gemeinsam mit deiner Steuerkanzlei oder einer Anwaltskanzlei.