phpguzzle.org
enrudees
Stripe – Doku

Stripe PHP SDK: API-Referenz mit praktischen Beispielen

Das Stripe PHP SDK (stripe/stripe-php) wrapped jeden REST-Endpoint in typisierte Klassen, regelt die Authentifizierung, wiederholt bei transienten Fehlern und deserialisiert Responses in Objekte, durch die man per Property-Zugriff navigiert. Die offizielle Dokumentation zeigt jede Methode isoliert, ohne den Kontext: wann ein verschachteltes Objekt expandieren statt separat abrufen, wie Pagination bei großen Datasets tatsächlich funktioniert, was das Response-Objekt unter der Haube ist. Dieser Artikel füllt die Lücke.

Wenn das SDK noch nicht installiert ist, deckt der Payment-Integration-Guide das Composer-Setup, API-Keys und den ersten PaymentIntent ab.

StripeClient vs Stripe::setApiKey

Das SDK hat zwei Aufruf-Konventionen, und das Mischen der beiden ist die Ursache überraschend vieler Bugs.

Global statisch (Legacy):

\Stripe\Stripe::setApiKey('sk_test_...');

$customer = \Stripe\Customer::create(['email' => '[email protected]']);
$intent   = \Stripe\PaymentIntent::retrieve('pi_abc123');

Instanz-basiert (empfohlen seit v7.33):

$stripe = new \Stripe\StripeClient('sk_test_...');

$customer = $stripe->customers->create(['email' => '[email protected]']);
$intent   = $stripe->paymentIntents->retrieve('pi_abc123');

Der Unterschied ist nicht nur kosmetisch. setApiKey speichert den Key in einer statischen Property. Jeder Code im selben Prozess teilt denselben Key. Das betrifft Test-Suites, Queue-Worker, die Jobs für verschiedene Stripe-Konten verarbeiten, und alles andere im selben Speicher. Ein Job überschreibt den Key, der nächste Job sendet einen Request mit den falschen Credentials, und man bekommt “No such payment_intent” ohne Hinweis, dass der Key das Problem war.

StripeClient hält den Key auf der Instanz. Zwei Instanzen mit verschiedenen Keys können im selben Prozess koexistieren. In einem Laravel-Queue-Worker für Connect-Accounts ist das der einzige Schutz vor Cross-Account-Datenlecks.

// Connect: im Auftrag eines Connected Account
$stripe = new \Stripe\StripeClient([
    'api_key'        => 'sk_test_platform_key',
    'stripe_account' => 'acct_connected_123',
]);

Für neuen Code immer StripeClient verwenden. Das statische Pattern funktioniert noch und Stripe hat kein Entfernungsdatum angekündigt, aber neue API-Endpoints werden nur mit Service-basierten Methoden auf dem Client ausgeliefert. Migration aus einer Legacy-Codebasis dauert Minuten: Client instanziieren, dann \Stripe\Customer::create( durch $stripe->customers->create( ersetzen. Die Argument-Arrays bleiben gleich.

Authentifizierung und API-Schlüssel

Stripe gibt zwei Schlüsselpaare aus, eines für den Testmodus und eines für Live:

PräfixZweckGehört wohin
sk_test_Secret Key, TestmodusNur Server, Umgebungsvariable
sk_live_Secret Key, Live-ModusNur Server, Umgebungsvariable
pk_test_Publishable Key, TestmodusBrowser (Stripe.js)
pk_live_Publishable Key, Live-ModusBrowser (Stripe.js)

Secret Keys authentifizieren jeden Server-seitigen API-Aufruf. Publishable Keys dürfen im Client-JavaScript stehen; sie können nur Zahlungen bestätigen und Tokens erstellen, nicht Kontodaten lesen.

Laufen Stripe API Keys ab? Nein. Keys leben, bis man sie rotiert. Das Rotieren erzeugt einen neuen Key mit einem Übergangsfenster (24 Stunden für Secret Keys), in dem beide Keys funktionieren. Danach hört der alte auf zu authentifizieren. Rotieren im Dashboard unter Developers > API keys.

Restricted Keys sind eine dritte Art: im Dashboard mit spezifischen Berechtigungen erstellt (Charges lesen, Customers schreiben, sonst nichts). Für Microservices und Drittanbieter-Integrationen, bei denen ein voller Secret Key zu breit ist.

Keys aus dem Source Control halten

Die eine Regel: Keys in Umgebungsvariablen, nie in PHP-Dateien. Ein geleakter sk_live_ gibt vollen Zugriff auf das Stripe-Konto.

// Gut: aus env lesen
$stripe = new \Stripe\StripeClient(getenv('STRIPE_SECRET_KEY'));

// Schlecht: hardcoded, landet in Git
$stripe = new \Stripe\StripeClient('sk_live_actualKeyHere');

In Laravel in .env speichern und über config/services.php abrufen. Der Laravel-Guide deckt das vollständige Service-Provider-Setup ab.

API-Aufrufe: das Methodenmuster

Jede Ressource folgt dem gleichen Schema. Methoden bilden HTTP-Verben ab:

SDK-MethodeHTTPBeispiel
->create([...])POSTCustomer erstellen
->retrieve('id')GETEinen PaymentIntent abrufen
->update('id', [...])POSTSubscription aktualisieren
->delete('id')DELETECoupon löschen
->all([...])GETCharges auflisten

Das erste Argument von retrieve, update und delete ist immer die Objekt-ID als String. create und all nehmen ein assoziatives Array:

$stripe = new \Stripe\StripeClient('sk_test_...');

// Create
$customer = $stripe->customers->create([
    'email' => '[email protected]',
    'name'  => 'Max Mustermann',
    'metadata' => ['internal_id' => '42'],
]);

// Retrieve
$customer = $stripe->customers->retrieve('cus_abc123');

// Update
$stripe->customers->update('cus_abc123', [
    'name' => 'Max Schmidt',
]);

// Delete
$stripe->customers->delete('cus_abc123');

// List
$customers = $stripe->customers->all(['limit' => 10]);

Expand und Optionen als zusätzliche Parameter

retrieve nimmt die ID und ein optionales Parameter-Array. API-Parameter wie expand gehören in dieses Array:

$intent = $stripe->paymentIntents->retrieve(
    'pi_abc123',
    ['expand' => ['customer', 'payment_method']]
);

update nimmt ID, Parameter-Array für die Änderungen und ein optionales drittes Array für Request-Level-Optionen (Idempotency Key, Stripe-Account Header):

$stripe->paymentIntents->update(
    'pi_abc123',
    ['metadata' => ['order_id' => '99']],
    ['idempotency_key' => 'update_pi_abc123_v2']
);

Verschachtelte Objekte expandieren

Standardmäßig liefert die API verwandte Objekte als reine IDs. Das customer-Feld eines PaymentIntent kommt als "cus_abc123" zurück, nicht als vollständiges Customer-Objekt. Expand weist Stripe an, das Objekt inline in die Response einzubetten:

$intent = $stripe->paymentIntents->retrieve('pi_abc123', [
    'expand' => ['customer', 'payment_method'],
]);

echo $intent->customer->email;
echo $intent->payment_method->card->last4;

Verschachtelungstiefe

Vier Ebenen und maximal 10 Expansions pro Request. Punkt-getrennte Pfade navigieren durch die Kette:

$charge = $stripe->charges->retrieve('ch_abc', [
    'expand' => ['payment_intent.customer.default_source'],
]);

Tiefer als vier Ebenen oder mehr als 10 Expansions pro Aufruf gibt einen Fehler.

Expand bei List-Aufrufen

Beim Auflisten von Objekten den expandierten Feldern data. voranstellen:

$charges = $stripe->charges->all([
    'limit' => 50,
    'expand' => ['data.customer', 'data.balance_transaction'],
]);

foreach ($charges->data as $charge) {
    echo $charge->customer->email;
    echo $charge->balance_transaction->fee;
}

Performance-Hinweis: mehrere Felder bei einem List-Aufruf mit hohem Limit zu expandieren, macht die Response deutlich größer und langsamer.

Pagination

List-Endpoints liefern maximal 100 Objekte pro Aufruf (Standard 10). Stripe nutzt cursor-basierte Pagination: die ID des letzten empfangenen Objekts übergeben, und die API gibt die nächste Seite zurück.

Manuelle Pagination

$hasMore = true;
$startingAfter = null;

while ($hasMore) {
    $params = ['limit' => 100];
    if (null !== $startingAfter) {
        $params['starting_after'] = $startingAfter;
    }

    $charges = $stripe->charges->all($params);

    foreach ($charges->data as $charge) {
        processCharge($charge);
    }

    $hasMore = $charges->has_more;
    if ($charges->data) {
        $startingAfter = end($charges->data)->id;
    }
}

Auto-Pagination

Das SDK bietet autoPagingIterator(), der die Cursor-Logik intern übernimmt:

$charges = $stripe->charges->all(['limit' => 100]);

foreach ($charges->autoPagingIterator() as $charge) {
    processCharge($charge);
}

autoPagingIterator macht bei jedem Seitenende einen neuen API-Aufruf. Der limit-Parameter steuert die Seitengröße, nicht die Gesamtanzahl. Mit limit => 100 holt jeder HTTP-Request 100 Objekte; der Iterator läuft weiter bis has_more false ist.

Zum Stoppen nach N Objekten einen Zähler ergänzen:

$count = 0;
foreach ($charges->autoPagingIterator() as $charge) {
    processCharge($charge);
    if (++$count >= 500) break;
}

Listen filtern

Die meisten List-Endpoints akzeptieren Filter, die Ergebnisse serverseitig einschränken:

// Charges der letzten 7 Tage
$charges = $stripe->charges->all([
    'limit'   => 100,
    'created' => ['gte' => strtotime('-7 days')],
]);

// Subscriptions eines bestimmten Kunden
$subs = $stripe->subscriptions->all([
    'customer' => 'cus_abc123',
    'status'   => 'active',
    'limit'    => 100,
]);

Der created-Filter akzeptiert gt, gte, lt, lte als Keys mit Unix-Timestamps.

Response-Objekte

Jede API-Response kommt als StripeObject (oder Subklasse wie Customer, PaymentIntent) zurück. Diese Objekte verhalten sich hybrid: Zugriff als Property ($customer->email) oder als Array-Key ($customer['email']), aber sie sind weder reine Objekte noch reine Arrays.

In JSON konvertieren

Der häufigste Stolperstein: json_encode($stripeObject) funktioniert, serialisiert aber nur die öffentlichen Datenfelder. Für die rohe API-Response als JSON-String:

$customer = $stripe->customers->retrieve('cus_abc123');

// Funktioniert
$json = json_encode($customer);

// Sicherer: erst toArray(), dann encode
$array = $customer->toArray();
$json  = json_encode($array, JSON_PRETTY_PRINT);

toArray() gibt ein normales assoziatives Array ohne SDK-Interna zurück. Für die Speicherung in einer Datenbank-Spalte oder Cache toArray() verwenden und das Ergebnis JSON-encodieren.

Null-Felder prüfen

Stripe gibt null für nicht gesetzte Felder zurück. Das SDK behält das bei:

$phone = $customer->phone ?? 'keine Nummer hinterlegt';

Die lastResponse-Property

Jedes StripeObject trägt lastResponse mit der rohen HTTP-Response:

$customer = $stripe->customers->retrieve('cus_abc123');

$httpStatus = $customer->lastResponse->code;
$requestId  = $customer->lastResponse->headers['Request-Id'];
$rawBody    = $customer->lastResponse->body;

Request-Id ist bei Stripe-Support-Tickets relevant. Bei jedem API-Aufruf in Produktion loggen.

Testkarten und Test-Tokens

Der Testmodus ist eine vollständige Sandbox. Kein Geld fließt, kein Kartennetzwerk wird kontaktiert. Test-Objekte sind aber echte API-Objekte mit echten IDs, und alle Features funktionieren wie im Live-Modus, einschließlich Webhooks, 3DS, Disputes und Refunds.

Kartennummern für Stripe.js / Elements

NummerMarkeVerhalten
4242 4242 4242 4242VisaErfolg
5555 5555 5555 4444MastercardErfolg
3782 822463 10005AmexErfolg
4000 0025 0000 3155Visa3DS-Authentifizierung erforderlich
4000 0000 0000 9995VisaAbgelehnt: insufficient_funds
4000 0000 0000 0002VisaAbgelehnt: generic_decline
4000 0000 0000 0069VisaAbgelehnt: expired_card
4000 0000 0000 0127VisaAbgelehnt: incorrect_cvc

Beliebiges zukünftiges Datum als Ablauf, beliebige 3 Ziffern als CVC (4 bei Amex).

PaymentMethod-Tokens für serverseitige Tests

Beim serverseitigen Testen ohne Frontend vorgefertigte Test-PaymentMethods direkt anhängen:

$intent = $stripe->paymentIntents->create([
    'amount'         => 2000,
    'currency'       => 'eur',
    'payment_method' => 'pm_card_visa',
    'confirm'        => true,
    'automatic_payment_methods' => [
        'enabled'         => true,
        'allow_redirects' => 'never',
    ],
]);

echo $intent->status; // "succeeded"

Die pm_card_*-Tokens überspringen den Stripe.js-Schritt. Nützlich für Integrationstests, Webhook-Pipeline-Tests und CI.

TokenSimuliert
pm_card_visaErfolgreiche Visa-Zahlung
pm_card_mastercardErfolgreiche Mastercard
pm_card_chargeDeclinedgeneric_decline
pm_card_chargeDeclinedInsufficientFundsinsufficient_funds
pm_card_authenticationRequired3DS erforderlich

Das Legacy-Format tok_visa / tok_chargeDeclined funktioniert über interne Konvertierung, aber pm_card_* ist die aktuelle Empfehlung. Für den vollständigen Decline-Code-Katalog siehe die Fehler-Referenz.

API-Versionierung

Stripes API entwickelt sich über datierte Versionen wie 2026-03-25.dahlia. Jede Version kann Response-Strukturen ändern, Felder umbenennen oder Standardverhalten verändern.

Wie das SDK Versionen pinnt

Das SDK pinnt seine API-Version auf die zum Zeitpunkt des Releases aktuelle Version. Das Dashboard-Konto hat eine separate Standardversion. Requests vom SDK nutzen die gepinnte SDK-Version, nicht die Dashboard-Version.

echo \Stripe\Stripe::getApiVersion();

Version überschreiben

Per Client oder per Request:

// Per Client
$stripe = new \Stripe\StripeClient([
    'api_key'        => 'sk_test_...',
    'stripe_version' => '2024-12-18.acacia',
]);

// Per Request (statisches Pattern)
\Stripe\Stripe::setApiKey('sk_test_...');
$customer = \Stripe\Customer::create(
    ['email' => '[email protected]'],
    ['stripe_version' => '2024-12-18.acacia']
);

Sparsam einsetzen. Einen API-Aufruf mit einer anderen Version als den Rest des Codes auszuführen, führt zu subtilen Bugs durch unterschiedliche Response-Strukturen.

Webhooks und Versionen

Webhook-Events werden in der API-Version des Endpoints im Dashboard zugestellt, nicht in der SDK-Version. Bei unterschiedlichen Versionen stimmt die Event-Struktur möglicherweise nicht mit dem überein, was der Code erwartet. Beide synchron halten. Der Webhook-Guide behandelt das im Kontext der Signatur-Verifizierung.

Idempotenz

Stripe unterstützt Idempotency Keys auf allen POST-Requests. Einen eindeutigen Key übergeben, und wenn derselbe Request zweimal feuert, gibt Stripe das gleiche Ergebnis zurück ohne ein Duplikat zu erstellen.

$intent = $stripe->paymentIntents->create([
    'amount'   => 5000,
    'currency' => 'eur',
], [
    'idempotency_key' => 'order_42_payment_attempt_1',
]);

Der Key ist 24 Stunden gültig. Ein zweiter Request mit gleichem Key und gleichen Parametern gibt die gecachte Response zurück. Gleicher Key mit anderen Parametern gibt einen 400-Fehler.

Gute Keys: Domain-Identifier plus Intent. order_{id}_payment_{attempt} funktioniert. UUIDs auch, sind aber schwerer zu debuggen. Timestamps allein vermeiden: zwei Requests in derselben Millisekunde erzeugen genau das Problem, das Idempotenz verhindern soll.

Exception-Hierarchie

Das SDK wirft typisierte Exceptions für jeden API-Fehler:

\Stripe\Exception\ApiErrorException (abstrakte Basis)
├── CardException              // 402 – Karte abgelehnt
├── InvalidRequestException    // 400 – fehlende Parameter, falsche ID
├── AuthenticationException    // 401 – ungültiger API Key
├── ApiConnectionException     // Netzwerkfehler, Timeout
├── IdempotencyException       // widersprüchlicher Idempotency Key
├── PermissionException        // 403 – Restricted Key ohne Berechtigung
└── RateLimitException         // 429 – zu viele Requests

\Stripe\Exception\SignatureVerificationException   // Webhook-Signatur, separate Hierarchie

SignatureVerificationException erweitert nicht ApiErrorException, weil sie aus lokaler Signaturprüfung stammt, nicht aus einem API-Aufruf. Ein Blanket-Handler auf ApiErrorException fängt sie nicht.

Reihenfolge ist relevant. CardException erweitert ApiErrorException – wenn ApiErrorException zuerst in der Catch-Kette steht, läuft der CardException-Block nie:

try {
    $intent = $stripe->paymentIntents->create([...]);
} catch (\Stripe\Exception\CardException $e) {
    // MUSS VOR ApiErrorException stehen
    $decline = $e->getError()->decline_code;
    error_log("Card declined: {$decline}");
} catch (\Stripe\Exception\RateLimitException $e) {
    sleep(2);
} catch (\Stripe\Exception\ApiErrorException $e) {
    error_log("Stripe error: " . $e->getMessage());
}

Die vollständige Fehlerbehandlung mit Recovery-Strategien steht in der Fehler-Referenz.

Metadata

Jedes größere Stripe-Objekt akzeptiert ein metadata-Hash: bis zu 50 Keys, jeder Key bis 40 Zeichen, jeder Wert bis 500 Zeichen. Metadata ist die Brücke zwischen Stripes Welt und der eigenen.

$intent = $stripe->paymentIntents->create([
    'amount'   => 7500,
    'currency' => 'eur',
    'metadata' => [
        'order_id'    => '1042',
        'campaign'    => 'summer_sale',
        'internal_id' => 'usr_887',
    ],
]);

Metadata reist mit dem Objekt durch seinen Lifecycle. Wenn der PaymentIntent durchgeht und ein Webhook-Event feuert, liefert $event->data->object->metadata->order_id den Wert "1042". So weiß der Webhook-Handler, welche Bestellung ausgeliefert werden muss.

Metadata propagiert nicht zwischen Objekten

Metadata auf einer Checkout Session kopiert sich nicht automatisch auf den PaymentIntent. Wenn der Webhook-Handler auf payment_intent.succeeded lauscht und dort Metadata liest, ist sie leer, sofern nicht explizit payment_intent_data.metadata beim Erstellen der Session gesetzt wurde. Der einfachere Weg: auf checkout.session.completed hören.

Logging und Debugging

Request-Id

Jede API-Response enthält einen Request-Id-Header. Loggen:

$customer = $stripe->customers->create(['email' => '[email protected]']);
$requestId = $customer->lastResponse->headers['Request-Id'];
error_log("Stripe request: {$requestId}");

Beim Stripe-Support-Ticket ist die Request-Id die erste Frage. Ohne sie ist Debugging Ratespiel auf beiden Seiten.

SDK-Level Logging

Das SDK kann jeden HTTP-Request und jede Response loggen. Es erwartet einen PSR-3 Logger:

\Stripe\Stripe::setLogger(new class extends \Psr\Log\AbstractLogger {
    public function log($level, \Stringable|string $message, array $context = []): void
    {
        error_log("[Stripe {$level}] {$message}");
    }
});

In Produktion auf error setzen oder Logging deaktivieren. Volles Request/Response-Logging enthält Card-Fingerprints und Kunden-E-Mails.

FAQ

Unterstützt Stripe PHP?

Ja. stripe/stripe-php ist ein offizielles, von Stripe gepflegtes SDK auf Packagist. Installation per Composer (composer require stripe/stripe-php), vendor/autoload.php einbinden, und jede Klasse im \Stripe-Namespace ist verfügbar. Ohne Composer liefert das SDK einen init.php-Loader als Alternative. Der Payment-Integration-Guide deckt beide Wege ab.

Ist die Stripe API Open Source?

Die API selbst ist proprietär, aber das PHP SDK ist Open Source unter der MIT-Lizenz. Der Quellcode liegt auf github.com/stripe/stripe-php. Man kann jede Zeile lesen, Issues melden und Pull Requests einreichen. Das SDK ist der HTTP-Client; die API dahinter ist ein geschlossener Service.

Was ist ein Stripe SDK?

Ein SDK (Software Development Kit) ist eine PHP-Bibliothek, die das HTTP-Plumbing zum Ansprechen von Stripes REST-API übernimmt. Statt curl-Requests manuell zu bauen, Header zu setzen und JSON-Responses zu parsen, gibt das SDK $stripe->customers->create([...]) und liefert ein typisiertes Objekt. Unter der Haube macht es weiterhin HTTPS-Aufrufe, aber Authentifizierung, Serialisierung, Retries und Fehlerbehandlung werden vom SDK verwaltet.

Nächste Schritte

Etwas Ungenaues auf dieser Seite entdeckt?

Fehler melden