About us Guides Projects Contacts
Админка
please wait

Introduction

Payment processing is critical infrastructure that must be implemented correctly—errors mean lost revenue or compliance violations. Stripe provides robust APIs for accepting payments, managing subscriptions, and handling complex marketplace scenarios with Stripe Connect. This guide covers practical Stripe integration patterns for common payment scenarios.

Initial Setup

Account Configuration

  1. Create a Stripe account at stripe.com
  2. Configure your business profile in Dashboard → Settings → Profile
  3. Set supported currencies in Dashboard → Settings → Payouts
  4. Enable payment methods in Dashboard → Settings → Payment Methods
  5. Generate API keys in Dashboard → Developers → API Keys

PHP Integration

composer require stripe/stripe-php
// config/services.php or .env
STRIPE_KEY=pk_test_xxx
STRIPE_SECRET=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
// Initialize Stripe
\Stripe\Stripe::setApiKey(env('STRIPE_SECRET'));

JavaScript Integration

npm install @stripe/stripe-js
import { loadStripe } from '@stripe/stripe-js';
const stripePromise = loadStripe('pk_test_xxx');

Checkout Sessions

Server-Side (PHP)

// Create checkout session
public function createCheckoutSession(Request $request)
{
$session = \Stripe\Checkout\Session::create([
'payment_method_types' => ['card'],
'line_items' => [[
'price_data' => [
'currency' => 'usd',
'product_data' => [
'name' => $request->product_name,
'description' => $request->description,
'images' => [$request->image_url],
],
'unit_amount' => $request->price * 100, // Amount in cents
],
'quantity' => $request->quantity,
]],
'mode' => 'payment',
'success_url' => route('checkout.success') . '?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => route('checkout.cancel'),
'customer_email' => auth()->user()->email,
'metadata' => [
'user_id' => auth()->id(),
'order_id' => $request->order_id,
],
]);
return response()->json(['id' => $session->id]);
}

Client-Side Redirect

async function handleCheckout() {
const response = await fetch('/create-checkout-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ product_id: 123, quantity: 1 }),
});
const { id } = await response.json();
const stripe = await stripePromise;
const { error } = await stripe.redirectToCheckout({ sessionId: id });
if (error) {
console.error(error);
}
}

Payment Intents (Custom UI)

Create Payment Intent

public function createPaymentIntent(Request $request)
{
$intent = \Stripe\PaymentIntent::create([
'amount' => $request->amount * 100,
'currency' => 'usd',
'payment_method_types' => ['card'],
'metadata' => [
'order_id' => $request->order_id,
],
]);
return response()->json([
'clientSecret' => $intent->client_secret,
]);
}

Client-Side Payment Form

import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
function PaymentForm({ amount, orderId }) {
const stripe = useStripe();
const elements = useElements();
const [error, setError] = useState(null);
const [processing, setProcessing] = useState(false);
async function handleSubmit(event) {
event.preventDefault();
setProcessing(true);
// Get client secret from server
const response = await fetch('/create-payment-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount, order_id: orderId }),
});
const { clientSecret } = await response.json();
// Confirm payment
const { error, paymentIntent } = await stripe.confirmCardPayment(
clientSecret,
{
payment_method: {
card: elements.getElement(CardElement),
billing_details: {
name: 'Customer Name',
},
},
}
);
setProcessing(false);
if (error) {
setError(error.message);
} else if (paymentIntent.status === 'succeeded') {
// Payment successful
window.location.href = '/success';
}
}
return (
<form onSubmit={handleSubmit}>
<CardElement />
{error && <div className="error">{error}</div>}
<button disabled={!stripe || processing}>
{processing ? 'Processing...' : `Pay $${amount}`}
</button>
</form>
);
}

Webhooks

Set Up Webhook Endpoint

// routes/web.php (exclude CSRF for webhooks)
Route::post('/webhook/stripe', [WebhookController::class, 'handle'])
->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);

Handle Webhook Events

// app/Http/Controllers/WebhookController.php
public function handle(Request $request)
{
$payload = $request->getContent();
$sigHeader = $request->header('Stripe-Signature');
try {
$event = \Stripe\Webhook::constructEvent(
$payload,
$sigHeader,
config('services.stripe.webhook_secret')
);
} catch (\Exception $e) {
return response('Webhook Error', 400);
}
switch ($event->type) {
case 'checkout.session.completed':
$session = $event->data->object;
$this->handleSuccessfulPayment($session);
break;
case 'payment_intent.succeeded':
$paymentIntent = $event->data->object;
$this->handlePaymentIntentSucceeded($paymentIntent);
break;
case 'payment_intent.payment_failed':
$paymentIntent = $event->data->object;
$this->handlePaymentFailed($paymentIntent);
break;
case 'customer.subscription.created':
case 'customer.subscription.updated':
case 'customer.subscription.deleted':
$subscription = $event->data->object;
$this->handleSubscriptionChange($subscription);
break;
default:
Log::info('Unhandled webhook event: ' . $event->type);
}
return response('OK', 200);
}
protected function handleSuccessfulPayment($session)
{
$orderId = $session->metadata->order_id;
$order = Order::find($orderId);
$order->update([
'status' => 'paid',
'stripe_session_id' => $session->id,
'paid_at' => now(),
]);
// Send a confirmation email
Mail::to($order->user)->send(new OrderConfirmation($order));
}

Subscriptions

Create Subscription

public function subscribe(Request $request)
{
$user = auth()->user();
// Create or get customer
if (!$user->stripe_customer_id) {
$customer = \Stripe\Customer::create([
'email' => $user->email,
'name' => $user->name,
]);
$user->update(['stripe_customer_id' => $customer->id]);
}
// Create subscription
$subscription = \Stripe\Subscription::create([
'customer' => $user->stripe_customer_id,
'items' => [
['price' => $request->price_id], // price_xxx from the Stripe Dashboard
],
'payment_behavior' => 'default_incomplete',
'expand' => ['latest_invoice.payment_intent'],
]);
return response()->json([
'subscriptionId' => $subscription->id,
'clientSecret' => $subscription->latest_invoice->payment_intent->client_secret,
]);
}

Cancel Subscription

public function cancel(Request $request)
{
$subscription = \Stripe\Subscription::retrieve($request->subscription_id);
// Cancel at period end (recommended)
$subscription->cancel_at_period_end = true;
$subscription->save();
// Or cancel immediately
// $subscription->cancel();
return response()->json(['status' => 'cancelled']);
}

Stripe Connect (Marketplaces)

Onboard Connected Accounts

public function createConnectAccount(Request $request)
{
$account = \Stripe\Account::create([
'type' => 'express',
'country' => $request->country,
'email' => $request->email,
'capabilities' => [
'card_payments' => ['requested' => true],
'transfers' => ['requested' => true],
],
]);
// Create an account link for onboarding
$accountLink = \Stripe\AccountLink::create([
'account' => $account->id,
'refresh_url' => route('connect.refresh'),
'return_url' => route('connect.return'),
'type' => 'account_onboarding',
]);
// Save account ID
$user = auth()->user();
$user->update(['stripe_connect_id' => $account->id]);
return redirect($accountLink->url);
}

Process Marketplace Payment

public function processMarketplacePayment(Request $request)
{
$seller = User::find($request->seller_id);
$platformFee = $request->amount * 0.10; // 10% platform fee
$paymentIntent = \Stripe\PaymentIntent::create([
'amount' => $request->amount * 100,
'currency' => 'usd',
'payment_method_types' => ['card'],
'transfer_data' => [
'destination' => $seller->stripe_connect_id,
],
'application_fee_amount' => $platformFee * 100,
]);
return response()->json([
'clientSecret' => $paymentIntent->client_secret,
]);
}

Testing

Test Cards

Card NumberScenario
4242424242424242Successful payment
4000000000000077Automatic approval
4000000000009995Insufficient funds
4000000000000069Expired card
4000002500003155Requires authentication

Local Webhook Testing

# Install Stripe CLI
# Download from https://stripe.com/docs/stripe-cli
# Log in
stripe login
# Forward webhooks to local server
stripe listen --forward-to localhost:8000/webhook/stripe
# Trigger test events
stripe trigger payment_intent.succeeded

Security Best Practices

  1. Never expose secret keys in client-side code
  2. Verify webhooks using signing secrets
  3. Use HTTPS for all Stripe communication
  4. Store minimal card data—let Stripe handle PCI compliance
  5. Validate amounts server-side before creating charges
  6. Use idempotency keys for mutation requests
  7. Log all payment events for debugging and auditing

Idempotency: The Critical Pattern

Network retries are common. Without idempotency, retries can double-charge customers:

// Always include an idempotency key for payment writes
$paymentIntent = \Stripe\PaymentIntent::create([
'amount' => $amount * 100,
'currency' => 'usd',
'payment_method_types' => ['card'],
], [
'idempotency_key' => 'order_' . $orderId . '_' . $attemptNumber,
]);
// Store the key with the payment record
$order->update([
'idempotency_key' => $idempotencyKey,
'stripe_payment_intent_id' => $paymentIntent->id,
]);

This applies to refunds and payouts as well. If a refund request is retried, it must not create multiple refunds.

Reconciliation Process

Even with webhooks, data can drift. Build reconciliation that compares your records against Stripe:

// Daily reconciliation job
class ReconcilePayments implements ShouldQueue
{
public function handle()
{
$yesterday = now()->subDay();
// Get charges from Stripe
$stripeCharges = \Stripe\Charge::all([
'created' => [
'gte' => $yesterday->startOfDay()->timestamp,
'lte' => $yesterday->endOfDay()->timestamp,
],
'limit' => 100,
]);
foreach ($stripeCharges->autoPagingIterator() as $charge) {
$localPayment = Payment::where('stripe_charge_id', $charge->id)->first();
if (!$localPayment) {
Log::warning('Missing local payment record', ['charge_id' => $charge->id]);
// Create missing record or alert
} elseif ($localPayment->amount != $charge->amount) {
Log::warning('Amount mismatch', [
'charge_id' => $charge->id,
'local' => $localPayment->amount,
'stripe' => $charge->amount,
]);
}
}
}
}

Check:

  • Total charges vs. total orders
  • Refund totals vs. refund records
  • Payouts vs. platform balances

Refunds and Disputes

Model refunds and disputes as explicit states:

// Refund model states
const REFUND_STATES = [
'initiated',
'submitted',
'succeeded',
'failed',
];
public function processRefund(Order $order, string $reason, float $amount = null)
{
$refund = Refund::create([
'order_id' => $order->id,
'amount' => $amount ?? $order->amount,
'reason' => $reason,
'initiated_by' => auth()->id(),
'status' => 'initiated',
]);
try {
$stripeRefund = \Stripe\Refund::create([
'charge' => $order->stripe_charge_id,
'amount' => ($amount ?? $order->amount) * 100,
], [
'idempotency_key' => 'refund_' . $refund->id,
]);
$refund->update([
'status' => 'submitted',
'stripe_refund_id' => $stripeRefund->id,
]);
} catch (\Stripe\Exception\ApiErrorException $e) {
$refund->update(['status' => 'failed', 'error' => $e->getMessage()]);
throw $e;
}
}

Monitor Key Payment Metrics

Track operational visibility metrics:

// Payment metrics for monitoring dashboards
class PaymentMetrics
{
public function getMetrics(): array
{
return [
'success_rate' => $this->calculateSuccessRate(),
'failure_rate' => $this->calculateFailureRate(),
'webhook_processing_delay' => $this->getWebhookDelay(),
'refund_volume' => $this->getRefundVolume(),
'dispute_volume' => $this->getDisputeVolume(),
'payout_failures' => $this->getPayoutFailures(),
];
}
private function calculateSuccessRate(): float
{
$total = Payment::whereDate('created_at', today())->count();
$succeeded = Payment::whereDate('created_at', today())
->where('status', 'succeeded')->count();
return $total > 0 ? round(($succeeded / $total) * 100, 2) : 0;
}
}

A sudden drop in payment success rates should page the team immediately.

Common Pitfalls

  • Treating a client response as payment success without webhook confirmation
  • Missing idempotency in payment creation
  • Logging sensitive data in plain text
  • Ignoring provider outages until customers report problems
  • Leaving payout schedules ambiguous

Avoid these by designing payment flows as distributed systems, not simple request-response calls.

The Senior Payments Mindset

Senior engineers treat payments as a risk-heavy system that demands discipline. They design for safety, auditability, and recovery. When payments are reliable, customers trust the platform and revenue stays predictable.

Conclusion

Stripe provides comprehensive payment infrastructure, but proper implementation is crucial for security and reliability. Use Checkout Sessions for simple flows, Payment Intents for custom UIs, and webhooks for reliable payment confirmation. Always test thoroughly with test cards before going live, and implement proper error handling and logging. Remember: payments are financial events—treat them with the respect they deserve.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive