SUQODocs
User GuideAPI ReferenceSDKs

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 names

toWire() 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.

On this page