SUQODocs
User GuideAPI ReferenceSDKs
PHP SDK v1.0.0. This version is frozen and no longer updated. Read the latest (1.1.0).

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