Plans & Subscriptions
A Plan says what to charge and how often. A Subscription puts a customer on a plan, and PayMongo then bills them every cycle on its own: it issues an invoice, charges the customer's saved payment method, and retries when that fails.
Plans live on Paymongo::plans() and come back as a Plan. Subscriptions live on Paymongo::subscriptions() and come back as a Subscription. For every attribute PayMongo accepts, see its Subscriptions guide and its Plan and Subscription references.
Subscriptions charge cards (Visa and Mastercard) and Maya. PayMongo must enable subscriptions on your account before you can test them.
The flow:
- Create a plan once, and reuse it for every subscriber.
- Create a customer for the subscriber.
- Create a subscription for the customer on the plan. It starts
incomplete. - Take the first payment on the subscription's first invoice, which saves the customer's payment method for the cycles after it.
- Listen to webhooks to keep your app in step with each cycle.
Create a plan
create() takes the plan's attributes:
use Luigel\Paymongo\Enums\PlanInterval;
use Luigel\Paymongo\Facades\Paymongo;
$plan = Paymongo::plans()->create([
'name' => 'Pro',
'description' => 'Pro tier, billed every 3 months',
'amount' => 299700, // PHP 2,997.00 per cycle, in centavos
'currency' => 'PHP',
'interval' => PlanInterval::Monthly, // or Weekly, Yearly
'interval_count' => 3, // 1 to 10 intervals per cycle
'cycle_count' => 4, // optional: stop after 4 payments, first one included
'metadata' => ['tier' => 'pro'],
]);
$plan->id; // "plan_...", reuse it for every Pro subscriber
name,description,amount,currency,interval, andinterval_countare required.amountis integer centavos per cycle, at least100(PHP 1.00).intervalis aLuigel\Paymongo\Enums\PlanIntervalcase (Weekly,Monthly, orYearly) or its value.interval_countis how many intervals make one cycle, from 1 to 10, soMonthlywith 3 bills quarterly.cycle_countends the subscription after that many payments, the first one included. It is at least 2. Leave it out to bill until the subscription is cancelled.plan_typedefaults toscheduled, which PayMongo bills on the interval. Anon_demandplan (Maya only) bills only when you ask PayMongo to, which the package does not wrap yet. See PayMongo's Subscriptions guide.
Retrieve, update, and list plans
retrieve() returns a plan, update() changes its name, description, amount, or metadata, and list() returns a page of plans:
use Luigel\Paymongo\Facades\Paymongo;
$plan = Paymongo::plans()->retrieve('plan_9VrvpRkkYqK6twbhuvcVTtjM');
$plan->interval; // ?PlanInterval
$plan->intervalCount;
$plan->money()?->format(); // "₱2,997.00"
$plan = Paymongo::plans()->update('plan_9VrvpRkkYqK6twbhuvcVTtjM', [
'name' => 'Pro (quarterly)', // name, description, amount, and metadata can change
]);
foreach (Paymongo::plans()->list() as $plan) {
$plan->name;
}
The page is a Luigel\Paymongo\Pagination\CursorPage. Iterate it for its plans, check hasMore, and call nextPage() for the next one, or lazy() to walk every page.
Subscribe a customer
create() takes the customer's id and the plan's id:
use Luigel\Paymongo\Enums\SubscriptionStatus;
use Luigel\Paymongo\Facades\Paymongo;
$subscription = Paymongo::subscriptions()->create(
'cus_b9ENKVqcHBfQQmv26uDYDCsD',
'plan_9VrvpRkkYqK6twbhuvcVTtjM',
);
$subscription->status === SubscriptionStatus::Incomplete; // until the first payment
// The first invoice's payment intent: take the first payment on it as usual.
$intentId = $subscription->attribute('latest_invoice.payment_intent.id');
PayMongo issues the subscription's first invoice straight away, with a payment intent on it. Take the payment on that intent the way you take any other: attach a payment method, and send the customer to authorize it. Paying it saves the payment method for the next cycles and makes the subscription active.
The customer has 24 hours to pay. After that PayMongo cancels the subscription (incomplete_cancelled), and you create a new one to try again.
Retrieve and list subscriptions
retrieve() returns a subscription with its plan, its latest invoice, and when it bills next. list() returns a page of subscriptions:
use Luigel\Paymongo\Enums\SubscriptionStatus;
use Luigel\Paymongo\Facades\Paymongo;
$subscription = Paymongo::subscriptions()->retrieve('sub_iEbGuGDrxPZoTg9r6BLbdCfV');
$subscription->status === SubscriptionStatus::Active;
$subscription->plan?->name; // the plan, nested
$subscription->customerId;
$subscription->attribute('anchor_date'); // "2026-09-01", the first payment's date
$subscription->attribute('next_billing_schedule'); // "2026-12-01"
$subscription->latestInvoice['status'] ?? null; // "paid", "open", ...
foreach (Paymongo::subscriptions()->list()->lazy() as $subscription) {
$subscription->status;
}
latestInvoice and setupIntent are the raw arrays PayMongo sends, with their own id, status, and payment_intent or next_action_url.
PayMongo sends anchor_date and next_billing_schedule as YYYY-MM-DD dates, which the anchorDate and nextBillingSchedule properties do not parse yet, so they read null. Read the dates with attribute(), as above.
Change the plan or payment method
changePlan() moves the subscription to another plan from the next cycle on. The current cycle stays at the old plan's price. changePaymentMethod() charges a different payment method from the next cycle on:
use Luigel\Paymongo\Facades\Paymongo;
// From the next cycle on, bill another plan instead:
$subscription = Paymongo::subscriptions()->changePlan('sub_iEbGuGDrxPZoTg9r6BLbdCfV', 'plan_hsJNpsRFU1LxgVbxW4YJHRs6');
// Charge a new card from the next cycle on. The customer authenticates it first:
$subscription = Paymongo::subscriptions()->changePaymentMethod(
'sub_iEbGuGDrxPZoTg9r6BLbdCfV',
'pm_wr98R2gwWroVxfkcNVZBuXg2',
redirectUrl: 'https://example.com/billing', // where they land after authenticating
);
$nextActionUrl = $subscription->setupIntent['next_action_url'] ?? null; // send them here
The customer authenticates a new card before it is used: PayMongo authorizes a small amount on it and then cancels that. redirectUrl: is where PayMongo sends them back afterwards, and it applies to cards only.
Cancel a subscription
cancel() takes the subscription and a reason, a Luigel\Paymongo\Enums\CancellationReason case (TooExpensive, MissingFeatures, SwitchedService, Unused, or Other) or its value. PayMongo requires the reason:
use Luigel\Paymongo\Enums\CancellationReason;
use Luigel\Paymongo\Facades\Paymongo;
$subscription = Paymongo::subscriptions()->cancel('sub_iEbGuGDrxPZoTg9r6BLbdCfV', CancellationReason::TooExpensive);
$subscription->status; // ?SubscriptionStatus: Cancelled
$subscription->cancelledAt; // ?CarbonImmutable
$subscription->cancellationReason; // ?CancellationReason
Cancelling takes effect immediately, and no new invoices follow. An invoice that is already open can still be paid.
Test a billing cycle
In test mode, triggerTestCycle() bills the subscription's next cycle now, so you can test renewals and failed payments without waiting for the billing date. It returns nothing:
use Luigel\Paymongo\Facades\Paymongo;
// Test mode only: bill the next cycle now instead of on its date.
Paymongo::subscriptions()->triggerTestCycle('sub_iEbGuGDrxPZoTg9r6BLbdCfV');
Statuses
$subscription->status is a Luigel\Paymongo\Enums\SubscriptionStatus:
| Case | Value | Meaning |
|---|---|---|
Incomplete |
incomplete |
Waiting for the first payment. |
IncompleteCancelled |
incomplete_cancelled |
The first payment did not come within 24 hours. Create a new subscription. |
Active |
active |
Every invoice is paid. Provide the service. |
PastDue |
past_due |
The latest invoice's payment failed. PayMongo retries it once a day, up to 3 times. |
Unpaid |
unpaid |
Still unpaid after 3 retries. Consider pausing the service. |
Cancelled |
cancelled |
Cancelled. No new invoices. |
Know when each cycle is billed
Every cycle happens on PayMongo's side, so webhooks are how your app hears about it. The package dispatches each as its own event in Luigel\Paymongo\Events:
| PayMongo event | Event class | When |
|---|---|---|
subscription.updated |
SubscriptionUpdated |
The subscription changed. |
subscription.past_due |
SubscriptionPastDue |
It became past_due: a cycle's payment failed. |
subscription.unpaid |
SubscriptionUnpaid |
It became unpaid: the retries ran out. |
subscription.invoice.created |
SubscriptionInvoiceCreated |
A cycle's invoice was created as a draft, a day before the billing date. |
subscription.invoice.finalized |
SubscriptionInvoiceFinalized |
The invoice became open, and PayMongo attempts the charge. |
subscription.invoice.paid |
SubscriptionInvoicePaid |
The invoice was paid. |
subscription.invoice.payment_failed |
SubscriptionInvoicePaymentFailed |
The invoice's payment failed. |
See Webhooks.