Dashboard Configuration and API Keys
Begin by logging into the Stripe Dashboard. Navigate to the Product Catalog to define the items you intend to sell. Once a product is created, generate a Price object associated with it. The identifier for this Price object (prefixed with price_) is required for the backend integration.
Retrieve the API keys from the Developers section under API Keys. Ensure consistency between environments: test mode products must be paired with test mode secret keys, and live mode products require live mode secret keys. Mixing these will result in authentication errors.
Creating a Checkout Session
Install the Stripe PHP library via Composer. In your backend script, initialize the Stripe client with your secret key. The following example demonstrates how to initialize a checkout session using a predefined Price ID.
<?php
require 'vendor/autoload.php';
\Stripe\Stripe::setApiKey(getenv('STRIPE_SECRET_KEY'));
$baseUrl = 'http://localhost:4242';
$priceId = 'price_123456789'; // Replace with actual Price ID from dashboard
try {
$session = \Stripe\Checkout\Session::create([
'line_items' => [
[
'price' => $priceId,
'quantity' => 1,
],
],
'mode' => 'payment',
'success_url' => $baseUrl . '/success.html?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => $baseUrl . '/cancel.html',
'automatic_tax' => [
'enabled' => true,
],
]);
header("HTTP/1.1 303 See Other");
header("Location: " . $session->url);
} catch (Exception $e) {
http_response_code(500);
echo $e->getMessage();
}
Passing Custom Metadata
To track internal order references or product details, include a metadata array within the session creation parameters. This data will be returned in the webhook payload upon successful payment.
<?php
// ... previous setup code ...
$session = \Stripe\Checkout\Session::create([
'line_items' => [[
'price' => $priceId,
'quantity' => 1,
]],
'mode' => 'payment',
'success_url' => $baseUrl . '/success.html',
'cancel_url' => $baseUrl . '/cancel.html',
'metadata' => [
'internal_order_ref' => 'ORD-98765',
'sku_name' => 'Premium Subscription',
],
]);
Webhook Endpoint Setup
Navigate to the Webhooks section in the Stripe Developers dashboard. Add an endpoint URL pointing to your server's listener script. Select the events to listen for; for standard Checkuot flows, checkout.session.completed is the primary event. Once created, copy the Signing Secret provided for this specific endpoint.
Verifying Webhook Signatures
Security best practices require verifying the signature of incoming webhook requests to ensure they originate from Stripe. The following handler validates the signature and processes the completed session event.
<?php
require 'vendor/autoload.php';
$whSecret = getenv('STRIPE_WEBHOOK_SECRET');
$input = file_get_contents('php://input');
$sigHeader = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';
$event = null;
try {
$event = \Stripe\Webhook::constructEvent(
$input,
$sigHeader,
$whSecret
);
} catch (\UnexpectedValueException $e) {
// Invalid payload
http_response_code(400);
exit();
} catch (\Stripe\Error\SignatureVerification $e) {
// Invalid signature
http_response_code(400);
exit();
}
// Handle the event
if ($event->type === 'checkout.session.completed') {
$session = $event->data->object;
// Retrieve metadata
$orderId = $session->metadata->internal_order_ref ?? null;
// TODO: Implement business logic such as fulfilling the order
http_response_code(200);
} else {
// Unhandled event type
http_response_code(400);
exit();
}
Understanding Webhook Payloads
The JSON payload received during a checkout.session.completed event contains detailed transaction information. Note that monetary amounts are represented in the smallest currency unit (e.g., cents for USD). When validating order totals, divide the amount_total field by 100.
{
"id": "evt_123456789",
"object": "event",
"type": "checkout.session.completed",
"data": {
"object": {
"id": "cs_test_123456789",
"amount_total": 1000,
"currency": "usd",
"payment_status": "paid",
"metadata": {
"internal_order_ref": "ORD-98765",
"sku_name": "Premium Subscription"
},
"customer_details": {
"email": "customer@example.com",
"address": {
"country": "US"
}
}
}
}
}
If implementing a Custom Payment Flow using Payment Intents directly rather than Checkout Sessions, the webhook listener must subscribe to payment_intent.succeeded instead. Additionally, Custom Flows often allow defining prices dynamically via price_data without pre-creating products in the dashboard.