Ein detaillierter Überblick für Webentwickler und interessierte Nerds mit Beispielen in React und Golang.
Paypal
Wann immer man im Internet bezahlen möchte kommt man um ein Unternehmen nicht herum: Paypal. 1998 gegründet gibt es kaum eine bekanntere Firma in diesem Bereich. Die Zahlung ist in der Regel einfach und schnell. Zudem ist Paypal weit verbreitet. In Deutschland wird online nach der Zahlung auf Rechnung am liebsten mit Paypal bezahlt. Wir hatten einige Verständnisprobleme bei der Einbindung in unser Zahlungssystem. Die Dokumentation von Paypal ist leider nicht die beste. Hier ein kleiner Überblick mit tiefer-gehenden Details an einigen Stellen.
Am einfachsten: “Buttons” für die SPA
Buttons von https://paypal.github.io/react-paypal-js/
Dabei wird ein relativ leichtgewichtiges Code-Snippet einfach in die eigene SPA (Single-Page-Application) integriert.
Hier ein gekürztes Beispiel mit React:
// https://paypal.github.io/react-paypal-js/?path=/docs/example-paypalbuttons--default
import { useEffect } from "react";
import {
PayPalScriptProvider,
PayPalButtons,
usePayPalScriptReducer
} from "@paypal/react-paypal-js";
// This values are the props in the UI
const amount = "2";
const currency = "USD";
const style = {"layout":"vertical"};
// [...]
return (<>
{ (showSpinner && isPending) && <div className="spinner" /> }
<PayPalButtons
style={style}
disabled={false}
forceReRender={[amount, currency, style]}
fundingSource={undefined}
createOrder={(data, actions) => {
return actions.order
.create({
purchase_units: [
{
amount: {
currency_code: currency,
value: amount,
},
},
],
})
.then((orderId) => {
// Your code here after create the order
return orderId;
});
}}
onApprove={function (data, actions) {
return actions.order.capture().then(function () {
// Your code here after capture the order
});
}}
/>
</>
);
// [...]
Entsprechendes Vorwissen vorausgesetzt geht die Einbindung schnell von der Hand und ist streng genommen auch sicher. Je nach Anwendungsfall lauern allerdings einige Fallstricke im Ablauf:

Ablaufdiagramm beim einfachen Paypal Button
Nachteile der Button Lösung
Nachteilig kann sich hier auswirken, dass diese Lösung client-basiert ist. Sie findet im wesentlichen im Browser des jeweiligen Nutzers statt. Dem Nutzer und seinem Browser darf man aber im Regelfall nicht vertrauen. Angreifern ist es ein leichtes beliebiges Browserverhalten zu verändern. Eingegebene Zahlungen haben durch den Browser des Users schnell eine Null mehr oder weniger. Über die Entwicklertools lassen sich client-seitige Eingaberegeln für Textfelder einfach deaktivieren.
Traue niemals dem Client.
Weitere Fehlerquelle: Unzuverlässiges Internet
Ein weiteres Problem ist die Unzuverlässigkeit dieser Methode: Selbst wenn der Nutzer den Bezahlvorgang bei Paypal korrekt abschliesst, kann es zu Problemen mit dem Callback (der Rückmeldung im Code) kommen:
- “Schluckauf” im Netzwerk
- Plötzlich ausgefallenes Internet/WLAN
- Absturz des Smartphones / Tablets / PCs
- Abschalten aufgrund von Strommangel des Endgeräts
- Plötzlicher Stromausfall / Ausfall einer Sicherung
In all diesen (und wahrscheinlich noch weiteren Fällen) wird der Callback nicht aufgerufen werden obwohl der Endnutzer die Zahlung bei Paypal veranlasst hat.
Will man trotzdem den Überblick behalten, muss man manuell von Zeit zu Zeit bei Paypal vorbeischauen und das eigene Konto auf Einzahlungen überprüfen. Das kann bei größeren Mengen natürlich schnell lästig werden.
(Rein theoretisch könnte man auch per API alle Zahlungen regelmäßig abfragen. Dies wird von Paypal soweit ersichtlich nicht empfohlen zumal es auch aufwändig werden kann. Möglich ist auch das Paypal hier schlicht ein rate limit gesetzt hat, sodass das ständige Abfragen sowieso nicht möglich ist.)
Zwischenfazit: “Button”-Lösung
Für einfach gelagerte Fälle ist die reine Button-Lösung wahrscheinlich die beste Option. Wichtig ist hierbei aber, dass die eigene Backend-Anwendung dem Browser niemals vertraut. Dass der Browser des Endanwenders IDs oder Erfolge zurückmeldet sollte man höchstens als Indiz werten. Quelle der Wahrheit sollte hier einzig und allein die Zahlungsliste bei Paypal bleiben.
Empfohlen: Webhook Lösung
Besser ist da die von Paypal empfohlene Variante: Man verwendet immer noch einen Button, richtet gleichzeitig aber Webhooks ein. Dabei senden die Paypal Server eine Anfrage direkt an den eigenen Server bzw. das eigene Backend. Je nach Typ des Hooks kann man die entscheidenden Informationen direkt aus diesem Payload entnehmen. Wichtig ist nur, dass man diesen vorher noch verifiziert.

Ablaufdiagramm beim Paypal Webhook
Entscheidender Vorteil dieser Lösung: Man kommuniziert direkt mit Paypal und bekommt vertrauenswürdige Informationen: Der Endanwender und dessen Browser spielen hier keine entscheidende Rolle mehr.
Backend: Teilimplementierung in Golang
Wichtig ist die eingehenden Webhooks zu verifizieren, bevor man sie weiterverarbeitet. Die ersten Schritte in Golang sind dabei recht übersichtlich (vorliegend gegen die Paypal Sandbox Umgebung):
(Hilfreich ist für Golang https://github.com/plutov/paypal/, wobei es für andere Sprachen ähnliche Projekte gibt.)
import (
"bytes"
"context"
"errors"
"fmt"
"log"
"net/http"
"os"
"time"
"github.com/plutov/paypal/v4"
)
func PaypalUnverifiedWebhookIncoming(ctx context.Context, request *http.Request) error {
var paypalClient *paypal.Client
var errClient error
paypalClient, errClient = paypal.NewClient(
os.Getenv("PAYPAL_CLIENT_ID"),
os.Getenv("PAYPAL_SECRET_ID"),
paypal.APIBaseSandBox,
)
if errClient != nil {
log.Printf("paypal webhook client error: %s", errClient.Error())
return errClient
}
_, errAccessToken := paypalClient.GetAccessToken(ctx)
if errAccessToken != nil {
return errAccessToken
}
_, errVerify := paypalClient.VerifyWebhookSignature(
ctx,
request,
os.Getenv("PAYPAL_WEBHOOK_ID"),
)
if errVerify != nil {
log.Printf("paypal webhook client verify error: %s", errVerify.Error())
return errVerify
}
buf := &bytes.Buffer{}
buf.ReadFrom(request.Body)
requestBody := buf.Bytes()
return PaypalVerifiedWebhook(ctx, requestBody)
}
Die Variable requestBody
enthält hierbei die verifizierten JSON-Daten die man beliebig Parsen und verwenden kann.
Voraussetzungen für das eigene Backend
Damit Paypal die Daten erfolgreich übertragen kann (Punkt 6 des Webhook Diagrams weiter oben) gibt es einige Voraussetzungen:
- Der eigene Webdienst / Backend muss über HTTP erreichbar sein und eine route (z.B.:
/paypal/webhooks
) bereitstellen. Man kann bei Paypal genau einstellen welche Webhooks man gesendet haben möchte. Hier sollte man zumindest im Development Modus großzügig erst einmal alles aktivieren. So vermeidet man Wartezeit auf Hooks, die man fälschlicherweise sowieso nicht aktiviert hat.

Ausschnitt ‘Webhooks’ aus dem Paypal Developer Dashboard (Sandbox modus)
- Das eigene Backend muss HTTP Statuscode 200 zurückgeben. Ansonsten versucht es Paypal über die nächsten Minuten, Tage und Stunden noch mehrere Male. Das ist zwar recht komfortabel für temporäre Probleme auf der eigenen Seite und man sollte tunlichst nur dann Code 200 zurückgeben, wenn man die Zahlung in der eigenen Datenbanklösung verbucht hat. Gleichzeitig hat man je nach eigenem Zahlungsmodell möglicherweise einen Kunden, der auf der eigenen Webseite auf das Ende des Zahlungsvorgangs wartet. Insbesondere in diesem Fall sollte man zusehen, dass die eigene Verbuchung ein grundsolider Prozess ist.
Eigene Erfahrung mit der Paypal Developer Sandbox
Aus unserer Sicht ist die Paypal Sandbox an sich sehr praktisch: Man bekommt sehr einfach einen Sandbox Account als Händler und einen Account als Sandbox Kunde. Mit diesen kann man grundsätzlich alles testen, was man möchte. Erfreulicherweise ist die Sicherheit der Sandboxumgebung auch reduziert: Man muss für Logins nicht ständig SMS Codes eingeben und Captchas lösen.
Leider lösen die Webhooks - zumindest unserer Erfahrung nach - in der Sandbox Umgebung nicht immer sofort aus, sondern kommen manchmal auch 10 Minuten später. Zusätzlich scheint die Webhook Übersicht, wo man den Status der Webhooks verfolgen kann, nicht immer korrekt zu arbeiten. Erfolgreiche Zustellungen (die bezeichnenderweise dann auch nicht mehr wiederholt wurden) werden teilweise nicht als solche gekennzeichnet. Zudem kam es zumindest zweimal vor, dass ein Job erst als erfolgreich zugestellt gekennzeichnet war und nach einem Neuladen der Seite war er es plötzlich nicht mehr. Das Verhalten ist unlogisch: Wenn man erfolgreich zugestellt hat, gibt es keinen Grund das nachträglich wieder zu ändern.
Die Sache mit der Zuordnung von Zahlungen
Hat man eine initiale Lösung implementiert stellt sich spätestens jetzt die Frage, wie man Zahlungen bei Paypal eigentlich den eigenen Kunden zuordnet. Dafür gibt es im wesentlichen eine Möglichkeit nämlich die Übergabe im Button Code. Hier springt einem die OrderID
ins Auge, die wird aber anscheinend bei einigen Webhooks nicht mitübertragen.
Stattdessen kann man aber sehr gut eine custom_id
verwenden, die vom Kunden nicht gesehen und - vor allem - bei allen diesbezüglichen Webhooks mitgeschickt wird.
(Alternativ gibt es noch invoice_id
aber damit haben wir keine Erfahrung.)
TLDR / Fazit
Bei Paypal bietet sich für einfach gelagerte Fälle mit niedrigem Volumen bzw. geringer Automatisierung die reine Button-Lösung an. Will man für sich und seine Kunden eine höhere Automatisierung sollte man zusätzlich noch Webhooks einrichten und den gesamten Prozess automatisieren. IDs wie die custom_id
kann man dem Kunden beim Button “mitgeben” und sie später bei den Webhooks für die Zuordnung von Zahlung und Kunde verwenden. Die Sandbox-Umgebung von Paypal ist aus unserer Sicht großartig zum Testen, man könnte hier noch etwas an der Geschwindigkeit und Zuverlässigkeit arbeiten.