Manage subscriptions
Creating, listing, cancelling and rescheduling subscriptions with the SUQO PHP SDK.
Four write-or-read operations on $suqo->subscriptions: list a page, iterate every page, create, cancel, and move the next billing date.
Creating one
use Suqo\Params\CreateSubscriptionParams;
use Suqo\Params\CustomerBilling;
use Suqo\Params\CustomerInput;
use Suqo\Params\CustomerShipping;
$created = $suqo->subscriptions->create(new CreateSubscriptionParams(
pbpId: 'pbp_3n9k2x',
customer: new CustomerInput(
phone: '9800000001',
fullName: 'John Doe',
email: '[email protected]',
address: 'Kathmandu, Nepal',
billing: new CustomerBilling(
billingBusinessName: 'ABC Pvt Ltd.',
billingEmail: '[email protected]',
billingAddress: 'Kathmandu, Nepal',
billingPanVat: '111111111',
),
shipping: new CustomerShipping(
phone: '9800000001',
fullName: 'John Doe',
email: '[email protected]',
),
),
returnUrl: 'https://merchant.example.com/thanks',
));
echo $created->checkoutUrl, PHP_EOL; // send the buyer here
echo $created->subscriptionId, ' ', $created->status?->value, PHP_EOL;pbpId comes from the catalog — see Browse the catalog.
The return is not proof of payment. The 201 body is its own schema, not an echo of the request: CreateSubscriptionResponse carries subscriptionId, pbpId, status, checkoutUrl, nextBillingCycle and createdAt, and neither return_url nor the customer comes back. The real outcome arrives on the checkout.succeeded / checkout.failed webhooks.
The SDK says customer; the wire says client
The rename is applied in both directions at the serialisation boundary. A raw error body, and anything read back through toArray(), always keeps the wire name:
$subscription->customer?->email; // typed accessor
$subscription->toArray()['client']['email'] ?? null; // the same value, wire namestoWire() is public, so you can see exactly what will be sent without making a request — handy when a ValidationError names a key you did not expect:
echo json_encode($params->toWire(), JSON_PRETTY_PRINT);
// {"pbp_id":"pbp_3n9k2x","client":{"phone":"…","full_name":"…","email":"…"}}Sending a field the SDK does not type yet
Every params object ends with extra, merged into the serialised object under wire names, verbatim. A declared key always wins over an extra key of the same name.
new UpdateBillingCycleParams(
subscriptionId: '3fa85f64-…',
nextBillingCycle: '2026-09-03',
extra: ['reason' => 'customer request'],
);Listing
$page = $suqo->subscriptions->list();
echo $page->totalSubscriptions, ' / ', $page->activeSubscriptions, PHP_EOL;
foreach ($page->results as $subscription) {
echo $subscription->subscriptionId, ' ', $subscription->customer?->email, PHP_EOL;
echo ' ', $subscription->product?->pbpId, ' ', $subscription->product?->price, PHP_EOL;
echo ' next billing ', $subscription->nextBillingCycle, PHP_EOL;
}SubscriptionPage is a page plus four account-level counters — totalSubscriptions, activeSubscriptions, dueSubscriptions, inactiveSubscriptions. They live on the page, so they are not reachable through autoPaging(); call list() when you want them.
Cancelling
$result = $suqo->subscriptions->cancel('3fa85f64-5717-4562-b3fc-2c963f66afa6');
echo $result->message, PHP_EOL;Cancellation is scheduled, not immediate. A 404 here also covers an id belonging to the other environment — it looks identical.
Changing the billing date
use Suqo\Params\UpdateBillingCycleParams;
$suqo->subscriptions->updateBillingCycle(new UpdateBillingCycleParams(
subscriptionId: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
nextBillingCycle: '2026-09-03',
));The subscription id travels in the body for this one, not the path. nextBillingCycle is YYYY-MM-DD and stays a string — the SDK does no date parsing, for the same reason it does no decimal parsing.
Status values
Known values arrive as a SubscriptionStatus case; a value the server adds later arrives as the raw string rather than failing the read. That is why $subscription->status is typed SubscriptionStatus|string|null, and why you narrow before matching:
use Suqo\Model\SubscriptionStatus;
$status = $subscription->status;
if ($status instanceof SubscriptionStatus) {
match ($status) {
SubscriptionStatus::Active => activate(),
SubscriptionStatus::Due => chase(),
default => null,
};
} else {
error_log("unrecognised status: {$status}");
}Cases: PendingCheckout, Active, Due, Cancelled, PendingCancellation, Inactive. The subscription statuses page in the API reference is the authority on what each one means.
What's not here yet
There is no resume() and no single-subscription retrieve(). Both exist in the API specification and neither is exposed — filter a list() page, or read the record you already hold.
Decimals and dates
price, vat, totalSubscribers and every timestamp are string end to end, never parsed into a float inside the SDK. Parse at your own boundary:
$total = bcmul($subscription->product->price, '2', 2);Retries
None of the write methods above are retried on failure — see Handle errors for why, and what changes once the backend supports idempotency keys.