I’m a developer who’s been in your shoes trying to get Stripe to bill customers exactly the right amount, only to discover those invoices keep coming through for £0. In this walkthrough, I’ll share my own Stripe PHP invoice setup for music lesson billing, explain where things went wrong, and offer ideas to level up your invoicing flow with discounts and reminders. By the end, you’ll have a solid, working function and inspiration for extra practice features.
Error Code
Here’s the cleaned-up createStripeLessonInvoice
function I ended up using. I’ve added clear logging, a single finalize step, and return the hosted invoice URL once everything’s locked in.
<?php
require 'vendor/autoload.php'; // Load the Stripe PHP library
function createStripeLessonInvoice(
string $parentName,
string $email,
string $childName,
string $instrument,
string $grouping,
float $costPerLesson,
int $lessonCount
): ?string {
\Stripe\Stripe::setApiKey('sk_test_[MYKEY]');
try {
// 1) Get or create the Stripe customer
$customer = getOrCreateStripeCustomer($email, $parentName, null, $childName);
// 2) Calculate the total and convert to pence
$totalCost = $costPerLesson * $lessonCount;
$amountPence = (int) round($totalCost * 100);
error_log("DEBUG: Total cost is £{$totalCost}");
error_log("DEBUG: Amount in pence: {$amountPence}");
// 3) Create a single invoice item
$invoiceItem = \Stripe\InvoiceItem::create([
'customer' => $customer->id,
'amount' => $amountPence,
'currency' => 'gbp',
'description' => "{$lessonCount} × £{$costPerLesson} lessons",
'metadata' => [
'student_name' => $childName,
'instrument' => $instrument,
'grouping' => $grouping,
'lesson_count' => $lessonCount,
'unit_cost' => $costPerLesson,
'total_cost' => $totalCost,
],
]);
// 4) Create the invoice, schedule to send in 7 days
$invoice = \Stripe\Invoice::create([
'customer' => $customer->id,
'collection_method' => 'send_invoice',
'days_until_due' => 7,
'auto_advance' => false, // we’ll call finalize manually
]);
// 5) Finalize the invoice so it moves from draft to open
$finalizedInvoice = $invoice->finalizeInvoice();
error_log("DEBUG: Invoice finalized: " . print_r($finalizedInvoice, true));
// 6) (Optional) Send immediately:
// \Stripe\Invoice::sendInvoice($finalizedInvoice->id);
// 7) Return the URL to share with your customer
return $finalizedInvoice->hosted_invoice_url;
} catch (\Stripe\Exception\ApiErrorException $e) {
error_log('Stripe API error: ' . $e->getMessage());
return null;
}
}
Error Definition
In my first iteration, I had two calls to finalizeInvoice()
and two early return
statements. That meant:
- The very first
return
ran before I logged anything or finalized properly. - No invoice items ever attached, so Stripe generated a blank (zero-value) invoice.
- My logs never showed the real error or amount in pence, leaving me clueless.
By consolidating to a single finalize and moving the return
to the end, I can see exactly what’s happening in my logs—and Stripe now charges the correct amount.
Explanation of the Stripe Flow
InvoiceItem::create
I add one or more line items (here, my lessons) to the customer’s upcoming invoice. Remember: Stripe expects amounts in the smallest currency unit (pence for GBP).Invoice::create
This builds a draft invoice for the customer, automatically including any pending invoice items.Invoice::finalizeInvoice
Finalizes the draft—locking in totals and making line items appear on the hosted invoice page.- (Optional)
Invoice::sendInvoice
You can call this immediately if you don’t want to wait for Stripe’s scheduled send. Otherwise, Stripe will auto-send on the date you set.
Additional Practice Functionality
To take your invoicing further, try these extras in your own project:
Multiple Line-Items & Discounts
function addLessonPackage($customerId, $description, $unitCost, $quantity) {
\Stripe\InvoiceItem::create([
'customer' => $customerId,
'amount' => (int) round($unitCost * $quantity * 100),
'currency' => 'gbp',
'description' => "{$quantity}× {$description}",
]);
}
function addDiscount($customerId, float $discountAmount) {
\Stripe\InvoiceItem::create([
'customer' => $customerId,
'amount' => -(int) round($discountAmount * 100),
'currency' => 'gbp',
'description' => "Discount: £" . number_format($discountAmount, 2),
]);
}
// Usage example:
addLessonPackage($customer->id, 'Group Piano Lessons', 25.00, 4);
addDiscount($customer->id, 10.00);
Automated Reminder Emails
Hook into Stripe’s webhooks listen for invoice.finalized
and then schedule your own email reminder a day before the due date. Rough sketch:
// webhook-handler.php
$payload = json_decode(file_get_contents('php://input'), true);
if ($payload['type'] === 'invoice.finalized') {
$invoice = $payload['data']['object'];
scheduleReminderEmail(
$invoice['customer_email'],
(new DateTime())->setTimestamp($invoice['due_date'])->modify('-1 day')
);
}
Final Thoughts
Billing students for music lessons shouldn’t feel like wrestling with Stripe’s API. By logging pence values, finalizing once, and returning the hosted URL last, you’ll see correct invoices every time. Then, when you’re comfortable, add multiple items, apply discounts, or build reminder hooks to automate follow-ups.