shapin/stripe
A lightweight Laravel integration for Stripe that wraps the official Stripe PHP SDK with a simple, expressive API. It helps streamline common billing tasks like customers, payments, subscriptions, and webhook handling in PHP applications.
Install the package and confirm what it actually depends on before writing a line of billing code:
composer require shapin/stripe
composer show shapin/stripe
That second command matters more than usual. With 4 stars and 0 dependents, the first question to answer is whether this is a thin wrapper around stripe/stripe-php or an independent reimplementation of Stripe's REST API. If stripe/stripe-php appears in the dependency tree, most of the risk collapses to "which version is pinned" and you can lean on Stripe's official docs for API shape. If it does not, read the source before it touches a real key.
Where to look first, in order:
README.md — expect it to be short. Treat any undocumented method as source-read-only.Shapin\Stripe\StripeServiceProvider or similar) — this tells you whether Laravel auto-discovery is wired, how the client is resolved from the container, and which config keys it reads.config/stripe.php (if present) — publish it and read every key:php artisan vendor:publish --provider="Shapin\Stripe\StripeServiceProvider"
Configure credentials in .env:
STRIPE_KEY=pk_test_...
STRIPE_SECRET=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
First use case — smallest possible round-trip. Open a Tinker session against test keys and create a customer plus a PaymentIntent. Nothing else. Do not build a checkout flow until these two calls return sane objects:
use Shapin\Stripe\Stripe;
$stripe = app(Stripe::class);
$customer = $stripe->customers()->create(['email' => '[email protected]']);
$intent = $stripe->paymentIntents()->create([
'amount' => 1000,
'currency' => 'usd',
'customer' => $customer->id,
'automatic_payment_methods' => ['enabled' => true],
]);
dump($customer->id, $intent->client_secret);
If those succeed, you have a working baseline and a clear signal that the method signatures mirror the official SDK closely enough to transfer knowledge. If the method names differ ($stripe->paymentIntent() vs ->paymentIntents()->create()), find that out now, not mid-sprint.
Put it behind your own interface from day one. The package is a swappable implementation detail, not a contract. One interface, one adapter class, zero direct references to Shapin\Stripe\Stripe in controllers or jobs:
interface PaymentGateway {
public function createIntent(int $amountCents, string $currency, string $customerId): string;
public function refund(string $intentId, ?int $amountCents = null): void;
}
final class StripeGateway implements PaymentGateway {
public function __construct(private \Shapin\Stripe\Stripe $stripe) {}
public function createIntent(int $amountCents, string $currency, string $customerId): string {
$intent = $this->stripe->paymentIntents()->create([
'amount' => $amountCents,
'currency' => $currency,
'customer' => $customerId,
'automatic_payment_methods' => ['enabled' => true],
]);
return $intent->client_secret;
}
public function refund(string $intentId, ?int $amountCents = null): void {
$this->stripe->refunds()->create(array_filter([
'payment_intent' => $intentId,
'amount' => $amountCents,
]));
}
}
Bind it in a service provider ($this->app->bind(PaymentGateway::class, StripeGateway::class);). This is your escape hatch: if the package is abandoned, you swap one class, not every call site.
Queue every outbound Stripe call. Stripe's API latency is real and occasionally awful. Charge capture, refunds, subscription changes, customer sync — put them behind jobs so a slow response doesn't stall a web request:
ProcessRefund::dispatch($order->stripe_payment_intent_id, $amountCents)->onQueue('billing');
Use a dedicated billing queue so payment work can't be starved by report generation or email blasts.
Webhooks are the source of truth. Never let the client-side confirmation drive order state. Register a single endpoint, verify signatures, dispatch jobs, return 2xx fast:
Route::post('/stripe/webhook', function (Request $request) {
$event = $this->stripe->webhooks()->constructEvent(
$request->getContent(),
$request->header('Stripe-Signature'),
config('services.stripe.webhook_secret')
);
match ($event->type) {
'payment_intent.succeeded' => MarkOrderPaid::dispatch($event->data->object->id),
'payment_intent.payment_failed' => MarkOrderFailed::dispatch($event->data->object->id),
'charge.refunded' => RecordRefund::dispatch($event->data->object->id),
default => null,
};
return response()->noContent();
});
Put this in routes/api.php (or add it to VerifyCsrfToken::$except) so CSRF doesn't 419 Stripe's server. Use $request->getContent() — the raw body — not $request->all(), or signature verification will fail.
Idempotency keys on everything that moves money. Derive them from your own IDs so a retried job can't double-charge:
$this->stripe->paymentIntents()->create($payload, [
'idempotency_key' => "order-{$order->id}-charge",
]);
If the package doesn't support the second-argument options array, that's a red flag — check the source, and if it's missing, this is where you consider wrapping stripe/stripe-php directly instead.
Model mapping. Keep stripe_customer_id on User and stripe_payment_intent_id on Order. Update them from webhook handlers, not from synchronous API calls in controllers. Your read path stays fast and your state converges even if a request dies mid-flight.
The package is a fork risk, not a library. Verify the upstream dependency and pinned version:
composer show shapin/stripe | grep -i stripe
If it pins old stripe/stripe-php, watch for composer update conflicts as your app's PHP and framework versions move forward. Lock it in composer.json and treat upgrades as deliberate PRs, not routine maintenance.
Method signature drift is the top surprise. Small wrappers rename things — customers() vs customer(), object vs array responses, snake_case vs camelCase in returned properties. Confirm both create and fetch paths in a sandbox before building on them. Don't assume $intent->client_secret is a string — dump() one.
Webhook secrets live in Stripe, not in the package. Use the whsec_... from the Stripe dashboard's webhook endpoint config, not your API secret. Signature failures almost always trace back to mismatched secrets or a body that's been parsed through a middleware that rewrote it.
Logging is a compliance risk. Stripe request/response bodies and exception traces can leak card metadata and API keys. Wrap the client in a service that scrubs Authorization, Stripe-Signature, and any card object from logs before they hit your log driver. If the package logs requests internally, find where and disable or filter it.
No community to debug against. There's no Stack Overflow footprint, no issue traffic, and likely a single maintainer. When something breaks at 2am, you are reading source. Onboard at least two engineers to the payments path and document every non-obvious behavior in a docs/payments.md. Treat this as institutional knowledge the package won't provide.
Extension points to lean on. Most Laravel Stripe wrappers let you pass a custom HTTP client or base URL into the service provider. If you need a proxy, retry logic, or request tracing, this is where it goes — check config/stripe.php and the provider for constructor injection. If neither offers it, you're stuck at the package's granularity, which reinforces the wrap-it-yourself guidance above.
Test keys and CI. Use pk_test_/sk_test_ in .env.testing, and mock the PaymentGateway interface in tests rather than hitting Stripe. One test suite run against live test keys is enough to validate the wiring; every other test should be hermetic.
Escape hatch rehearsal. Once a quarter, prove the migration path: implement one method of PaymentGateway directly against stripe/stripe-php in a branch and confirm it drops in without touching call sites. If it doesn't, your abstraction is leaking and the swap cost is growing — fix it while it's cheap.
Don't build subscriptions on this. Stripe's subscription and billing-portal surface is where the official SDK plus laravel/cashier earns its keep. Wrapping subscriptions here means re-implementing webhook state machines, proration handling, and dunning flows that Cashier already ships and maintains. Use this package for one-time payments if you use it at all, and only because an internal decision has already justified the dependency.
How can I help you explore Laravel packages today?