Zum Inhalt springen
Rechnungskit
Live-Demo

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).

API · Zuwendungsbestätigungen
Von der Spende auf der Website zur Bestätigung beim Spender
Website des Vereins
Stripe Checkoutpayment_intent_data.metadata.rk_kind = donation
Rechnungskit
Zuwendung gebuchtaus dem Stripe-Webhook, Bestätigung archiviert und per E-Mail verschickt
Backend der Website
GET /v1/donation-receipts?paymentRef=pi_…fragt mit Backoff, bis die Bestätigung da ist
GET …/{number}/pdfreicht das PDF an den Spender weiter
Der API-Schlüssel bleibt im Backend. Der Browser des Spenders spricht nie direkt mit Rechnungskit.

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 auf receipt_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.

Rechnungskit, Verbindungen → API, Formular „Schlüssel erstellen“: Name „Website Spendenformular“, Gültigkeit „Unbefristet (empfohlen für Produktion)“, unter Berechtigungen ist nur documents:read („Belege abrufen“) angehakt, orders:write, invoices:read und subscriptions:write sind leer. Unten der Hinweis „Nur serverseitig verwenden“ und der Knopf „Schlüssel erstellen“

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 in subscription_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.metadata bzw. 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): metadata direkt am PaymentIntent setzen, für wiederkehrende Beträge zusätzlich rk_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.succeeded aus. 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:

  1. 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.
  2. Bei 200 die gültige Bestätigung zeigen und das PDF anbieten.
  3. Bei receipt_pending nicht 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.
  4. 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 paymentRef an 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.

de en