NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

The Stripe provider (id stripe) verifies webhook signatures natively — HMAC-SHA256 over t.payload from the Stripe-Signature header, a constant-time comparison and a configurable timestamp tolerance — and reads REST objects with the pinned API version:

use RoundlyConsulting\Purchases\Providers\Stripe\Stripe;

$stripe = app(Stripe::class);

$event = $stripe->notification($request);   // verifies Stripe-Signature, returns StripeEvent
$event->type;                               // EventType, e.g. EventType::InvoicePaid
$event->object;                             // array — the event's data.object

$stripe->paymentIntent('pi_123');           // PaymentIntent
$stripe->subscription('sub_123');           // Subscription
$stripe->session('cs_123');                 // CheckoutSession
$stripe->invoice('in_123');                 // Invoice

Supported events

Stripe eventResultStatusAmount
payment_intent.succeededPurchase (audited only when it pays an invoice)The intent’s own statusamount
payment_intent.payment_failedPurchase (audited only when it pays an invoice)Failedamount
checkout.session.completedPurchase keyed on its PaymentIntent (payment mode; subscription and setup modes are audited only)Completed when payment_status is paid or no_payment_required, else Pendingamount_total
invoice.paidPurchase for a one-off invoice (subscription invoices are audited only)Completedamount_paid
invoice.payment_failedPurchase for a one-off invoice (subscription invoices are audited only)Failedamount_due
customer.subscription.created / .updated / .deletedSubscriptionMapped from the subscription status—
charge.refundedRefundRefunded, or Completed for a partial refundamount_refunded
charge.dispute.createdRefund (chargeback)Refundedamount
charge.dispute.closedWon: the same chargeback row is reversed and a recorded purchase reinstated; lost: the chargeback standsCompleted when wonamount
charge.dispute.updated, inquiries, unmapped eventsInformational — audited, never applied——

Subscription statuses

Stripe subscription statusStatus
trialing, activeCompleted
past_dueInGracePeriod
incompletePending
paused, unpaidOnHold
canceled, incomplete_expiredCanceled

Subscription billing is not recorded as purchases: a subscription- or setup-mode Checkout and a subscription invoice are audited only, and the subscription’s own customer.subscription.* events keep its state. Those events carry the current period start and end and the trial end; they carry no price. A PaymentIntent that pays any invoice is audited only too — a renewal belongs to its subscription, and a one-off invoice is already recorded once, from invoice.paid.

Which invoice a PaymentIntent pays

Since API version 2025-03-31 (the pinned 2026-05-27.dahlia included) a PaymentIntent no longer says which invoice it pays, so for a payment_intent.* event without an invoice field the package asks Stripe’s Invoice Payments API (GET /v1/invoice_payments). That needs PURCHASES_STRIPE_SECRET: without it such an event is refused (VerificationException, 400 on the route, so Stripe retries) rather than guessed at — a guess would record every renewal as a new one-off purchase.

Disputes

A dispute is a chargeback only while the funds are gone: an inquiry (warning_*) and charge.dispute.updated change nothing, and a lost dispute stays the single chargeback recorded at charge.dispute.created (keyed on the dispute id). A dispute closed as won updates that same row and reinstates the refunded purchase it was linked to (PurchaseCompleted) — its own payment data is kept, and only a purchase the package recorded is reinstated: a won dispute of a payment that never became a Purchase (a subscription renewal’s) creates none and fires nothing.

Client callbacks

Stripe::callback() retrieves a Checkout Session from a session_id input, or a PaymentIntent from a payment_intent input — handy on a success-redirect page:

// GET /checkout/success?session_id=cs_123
$session = app(Stripe::class)->callback($request);   // CheckoutSession (PaymentIntent for ?payment_intent=pi_123)

$session->paymentStatus;   // 'paid'
$session->amountTotal;     // ?Money
$session->subscription;    // ?string — the subscription id, for subscription checkouts

Special currencies

Amounts are read in Stripe’s smallest currency unit and re-scaled where Stripe differs from ISO 4217 — ISK and UGX are sent with two decimals, MGA with none (StripeAmount::SCALE_EXCEPTIONS). A float or fractional-string amount is refused, never truncated.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.