# Zuwendungsbestätigungen per API

> Für die Website eines Vereins: Spenden im Stripe-Checkout markieren und die Zuwendungsbestätigung per API abrufen. Schlüssel, Endpunkte, Antwortfelder, Fehlercodes, Polling, Widerruf und PDF.

Aktualisiert am 09.11.2026

Quelle: https://rechnungskit.de/docs/api/zuwendungsbestaetigungen

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 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“](https://rechnungskit.de/docs/spendenquittungen/api-schluessel-documents-read.webp "1876x642")

| 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:

```js
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:

```bash
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`:

```js
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):

```json
{
  "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`:

```json
{
  "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.

```js
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:

```json
{
  "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`:

```json
{
  "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

```bash
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:

```js
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.

## Wo in der App

- /settings/api, Schlüssel mit `documents:read` anlegen, API-Aktivität mit Request-IDs
- /settings/donations, Angaben des Vereins und Zeitpunkt der Bestätigung
- /donations, Zuwendungen, Bestätigungen, Anschrift ergänzen, Sammelbestätigung erstellen, erneut senden
