How to Send Specific Invoice Values with Stripe PHP Invoice

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

  1. 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).
  2. Invoice::create
    This builds a draft invoice for the customer, automatically including any pending invoice items.
  3. Invoice::finalizeInvoice
    Finalizes the draft—locking in totals and making line items appear on the hosted invoice page.
  4. (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.

Related blog posts