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'); // InvoiceSupported events
| Stripe event | Result | Status | Amount |
|---|---|---|---|
payment_intent.succeeded | Purchase (audited only when it pays an invoice) | The intent’s own status | amount |
payment_intent.payment_failed | Purchase (audited only when it pays an invoice) | Failed | amount |
checkout.session.completed | Purchase keyed on its PaymentIntent (payment mode; subscription and setup modes are audited only) | Completed when payment_status is paid or no_payment_required, else Pending | amount_total |
invoice.paid | Purchase for a one-off invoice (subscription invoices are audited only) | Completed | amount_paid |
invoice.payment_failed | Purchase for a one-off invoice (subscription invoices are audited only) | Failed | amount_due |
customer.subscription.created / .updated / .deleted | Subscription | Mapped from the subscription status | — |
charge.refunded | Refund | Refunded, or Completed for a partial refund | amount_refunded |
charge.dispute.created | Refund (chargeback) | Refunded | amount |
charge.dispute.closed | Won: the same chargeback row is reversed and a recorded purchase reinstated; lost: the chargeback stands | Completed when won | amount |
| charge.dispute.updated, inquiries, unmapped events | Informational — audited, never applied | — | — |
Subscription statuses
| Stripe subscription status | Status |
|---|---|
| trialing, active | Completed |
| past_due | InGracePeriod |
| incomplete | Pending |
| paused, unpaid | OnHold |
| canceled, incomplete_expired | Canceled |
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 checkoutsSpecial 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 cryptoBy 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.