Developer documentation

Suqo Python SDK

A thin, class-based client for the Suqo external API at /api/v1/. Three resources — products, subscriptions, customers — reached as attributes of one client object, with API-key auth, automatic pagination and typed exceptions handled for you.

from suqo import SuqoClient

client = SuqoClient(api_key="su_key_...")

for subscription in client.subscriptions.iter():
    print(subscription["subscription_id"], subscription["status"])

Responses come back as plain dictionaries matching the API payloads — no model layer to fall out of sync with the server.

Installation

Python 3.9 or newer. The only dependency is requests.

pip install -e /path/to/suqo-python-sdk

Getting access

Two things must be true before the API will answer you:

RequirementWhy it matters
KYC verified Every endpoint except products/ requires a verified seller account. Waived on sandbox, where nobody clears KYC by hand.
An API key Minted from the dashboard. Keys are stored hashed, so the plaintext is shown once — copy it then.

The key travels as Authorization: Bearer <key>. The client reads it from the SUQO_API_KEY environment variable when you don't pass one, which keeps it out of your source tree.

Never commit a key.

A key grants full read and write access to your subscriptions. If one lands in version control or a shared log, rotate it from the dashboard immediately — revoking is the only fix, since the stored hash can't be recovered or reset.

Quickstart

Selling something is a genuine sequence: you need a billing period's ID before you can subscribe anyone to it, and a subscription before you can collect.

Connect

from suqo import SuqoClient

client = SuqoClient()  # reads SUQO_API_KEY

Find what you're selling

Every plan carries billing periods. The pbp_id on one of them is what a subscription attaches to.

for product in client.products.iter():
    for plan in product["plan"]:
        for period in plan["billing_periods"]:
            print(product["name"], plan["plan_name"],
                  period["label"], period["price"], period["pbp_id"])

Create the subscription

created = client.subscriptions.create(
    pbp_id="pbp_3n9k2x",
    return_url="https://yourapp.com/thanks",
    client={
        "phone": "9841000100",
        "full_name": "Ram Shrestha",
        "email": "ram@client.com",
    },
)

Send the buyer to pay

The subscription starts at pending_checkout and flips to active once payment lands.

print(created["checkout_url"])

Client

SuqoClient owns the HTTP session and exposes the three resources as attributes. Build one and keep it — it pools connections across calls.

SuqoClient(
    api_key=None,
    *,
    base_url="https://api.suqo.ai",
    timeout=30,
    max_retries=3,
    session=None,
)
ParameterDefaultNotes
api_keySUQO_API_KEYFalls back to the environment variable. Raises ValueError if neither is set.
base_urlProductionHost only — /api/v1/ is appended. Point it at https://dev.be.suqo.ai for dev.
timeout30Seconds, applied to every request.
max_retries3GET only. Pass 0 to disable.
sessionNew sessionInject your own requests.Session for proxies, custom TLS or tests.

Attributes and helpers

MemberWhat it does
.productsProduct catalogue, read only.
.subscriptionsSubscription list, creation and lifecycle.
.customersYour buyers, read only.
.request(method, path, *, params, json)Escape hatch for any endpoint. Relative paths resolve against /api/v1/; absolute URLs pass through untouched.
.paginate(path, params)Generator behind every .iter().
.close()Closes the session. The client is also a context manager.
with SuqoClient() as client:
    products = client.products.list()

Products

Your active catalogue, scoped to your account, with plans and billing periods nested inside each product. This is the one collection that doesn't require KYC.

client.products.list(page=None, page_size=None)

GET/api/v1/products/

One page of products, wrapped in the standard pagination envelope.

client.products.iter()

Generator over every product, following pagination for you.

Each product looks like this:

{
  "product_id": "205293a1-84f4-426e-8f8c-5ddb239e5d2f",
  "name": "Pro Plan Bundle",
  "description": "...",
  "type": "...",
  "is_active": true,
  "terms_and_conditions": "...",
  "features_and_benefits": "...",
  "vat": {"is_vat_active": true, "vat_type": "inclusive", "vat_percentage": "13.00"},
  "product_image": [...],
  "plan": [{
    "plan_id": "...",
    "plan_name": "Basic",
    "description": "...",
    "billing_periods": [{
      "pbp_id": "pbp_3n9k2x",
      "interval_type": "month",
      "interval_count": 1,
      "label": "Monthly",
      "price": "999.00",
      "currency": "NPR",
      "is_current": true,
      "is_limited": false,
      "is_archived": false,
      "offers": [...]
    }]
  }],
  "total_subscribers": "42",
  "created_at": "...",
  "updated_at": "..."
}
Prices already include any live offer.

The price on a billing period is what the buyer will be charged — an active, in-window discount is applied before it reaches you, so don't apply offers a second time.

Subscriptions

The full lifecycle: list, create, cancel, resume, and move the billing date.

client.subscriptions.list(page=None, page_size=None)

GET/api/v1/subscriptions/

A page of subscriptions. Unusually, this envelope also carries account-wide status totals, so a single call answers "how am I doing?" without a second query.

page = client.subscriptions.list(page_size=100)

page["total_subscriptions"]     # 85
page["active_subscriptions"]    # 60
page["due_subscriptions"]       # 10
page["inactive_subscriptions"]  # 15
page["results"]                # [...]

client.subscriptions.iter()

Generator over every subscription. Note the status totals live on the envelope, so they aren't available here.

client.subscriptions.get(subscription_id)

GET/api/v1/subscriptions/{id}/

A single subscription, shaped like each entry in results:

{
  "subscription_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "status": "active",
  "is_active": true,
  "client": {
    "phone": "9841000100",
    "full_name": "Ram Shrestha",
    "email": "ram@client.com",
    "address": "Kathmandu, Nepal",
    "billing": {...},   # or null
    "shipping": {...}   # or null
  },
  "product": {
    "product_id": "205293a1-...",
    "name": "Pro Plan Bundle",
    "plan_name": "Basic",
    "pbp_id": "pbp_3n9k2x",
    "label": "Monthly",
    "price": "999.00",
    "currency": "NPR"
  },
  "current_period_start": "2026-07-03T00:00:00Z",
  "current_period_end": "2026-08-03T00:00:00Z",
  "next_billing_cycle": "2026-08-03T00:00:00Z",
  "created_at": "2026-07-03T10:15:00Z"
}

client.subscriptions.create(pbp_id, return_url, client)

POST/api/v1/subscriptions/
FieldRequiredNotes
pbp_idYesA visible billing period belonging to one of your unarchived products.
return_urlYesWhere the buyer lands after checkout.
client.phoneYesMax 15 characters.
client.full_nameYesMax 255 characters.
client.emailYesValidated as an email address.
client.addressNoFree text.
client.billingNoIf present, billing_business_name, billing_email and billing_address all become required; billing_pan_vat stays optional.
client.shippingNoIf present, phone, full_name and email become required; address stays optional.
created = client.subscriptions.create(
    pbp_id="pbp_3n9k2x",
    return_url="https://yourapp.com/thanks",
    client={
        "phone": "9841000100",
        "full_name": "Ram Shrestha",
        "email": "ram@client.com",
        "address": "Kathmandu, Nepal",
        "billing": {
            "billing_business_name": "ABC Pvt Ltd.",
            "billing_email": "ram@client.com",
            "billing_address": "Kathmandu, Nepal",
            "billing_pan_vat": "301234567",
        },
    },
)

Returns:

{
  "created_at": "2026-08-14T10:15:00Z",
  "pbp_id": "pbp_3n9k2x",
  "subscription_id": "3fa85f64-...",
  "status": "pending_checkout",
  "checkout_url": "{frontend_url}/pay/3fa85f64-...",
  "next_billing_cycle": "2026-09-14T00:00:00Z"
}

client.subscriptions.cancel(subscription_id)

POST/api/v1/subscriptions/{id}/cancel/

Cancels at the end of the current billing period rather than immediately — the subscription moves to pending_cancellation and the buyer keeps what they paid for.

{"message": "Subscription will be cancelled at the end of the current billing period."}

client.subscriptions.resume(subscription_id)

POST/api/v1/subscriptions/{id}/resume/

Undoes a pending cancellation. Raises ValidationError if the subscription isn't in a state that can be resumed.

client.subscriptions.update_billing_cycle(subscription_id, next_billing_cycle)

POST/api/v1/subscriptions/update-billing-cycle/

Moves the next billing date. Accepts a date, a datetime, or a YYYY-MM-DD string — the SDK formats it for you. The date must be today or later.

from datetime import date

client.subscriptions.update_billing_cycle(sub_id, date(2026, 9, 1))
client.subscriptions.update_billing_cycle(sub_id, "2026-09-01")
Pushing the cycle takes no payment.

For partners who collect money themselves, this call is the renewal signal. It also revives the subscription: a cancelled or inactive row is reactivated so its status matches its new billing date. Setting the date to today lands it in the grace period as due.

Customers

Everyone who has subscribed to you, newest first. Read only.

client.customers.list(page=None, page_size=None)

GET/api/v1/customers/

client.customers.get(customer_id)

GET/api/v1/customers/{id}/

client.customers.iter()

for customer in client.customers.iter():
    print(customer["full_name"], customer["buyer_phone"])
{
  "id": 232,
  "buyer_phone": "9841827378",
  "buyer_email": "ram@client.com",
  "full_name": "Ram Shrestha",
  "created_at": "2026-08-10T17:32:28.066876+05:45"
}

full_name and buyer_email may be null or an empty string — guard both.

Pagination

Every list endpoint returns 20 items per page by default, up to 100. Each resource offers the same two shapes.

Automatic — .iter()

A generator over every item across every page. Fetching is lazy: page two only goes out once page one is consumed, so breaking early costs nothing.

for customer in client.customers.iter():
    ...

customers = list(client.customers.iter())     # materialise them all
client.customers.iter(page_size=100)          # fewer round trips

Manual — .list()

One page, with the raw envelope, for when you need count or the links:

page = client.customers.list(page=2, page_size=50)

{
  "count": 29,
  "next": "https://dev.be.suqo.ai/api/v1/customers/?page=2",
  "previous": null,
  "results": [...]
}

To walk pages yourself — this is exactly what .iter() does internally:

page = client.customers.list()
while page["next"]:
    page = client.request("GET", page["next"])   # absolute URLs pass through
There are no server-side filters.

These endpoints accept only page and page_size. Any other query parameter is silently ignored and you get everything back, so filter in Python: [s for s in client.subscriptions.iter() if s["status"] == "active"].

Errors

Any non-2xx response raises. Every exception subclasses SuqoError; anything the API itself returned subclasses SuqoAPIError and carries status_code, the decoded payload, and the raw response.

StatusExceptionUsual cause
400ValidationErrorBad field, unknown pbp_id, or a billing date in the past.
401AuthenticationErrorMissing, invalid or expired key; or the seller account is inactive.
403PermissionDeniedErrorKYC not verified.
404NotFoundErrorNo such resource on your account — other sellers' data reads as missing.
429RateLimitErrorToo many requests. GETs retry this automatically first.
5xxServerErrorThe API failed. GETs retry automatically.

Field errors are flattened into a readable message, and the original dictionary stays available on .errors:

from suqo import ValidationError

try:
    client.subscriptions.create(pbp_id="bad", return_url="https://x.com", client={...})
except ValidationError as exc:
    print(exc)            # pbp_id: does not exist or is not visible.
    print(exc.errors)     # {"pbp_id": ["does not exist or is not visible."]}
    print(exc.status_code) # 400

Catch broadly at the edges of your integration:

from suqo import SuqoAPIError

try:
    client.subscriptions.cancel(sub_id)
except SuqoAPIError as exc:
    logger.error("Suqo %s: %s", exc.status_code, exc)

Retries & timeouts

GET requests retry up to three times on 429, 500, 502, 503 and 504, with exponential backoff. Writes never retry — a replayed create would charge a buyer twice, and a replayed cycle push would move the billing date further than you meant.

client = SuqoClient(timeout=10, max_retries=5)
client = SuqoClient(max_retries=0)   # handle failures yourself

Timeouts surface as requests exceptions (requests.Timeout, requests.ConnectionError) rather than SDK errors, since nothing came back from the API to wrap.

Endpoint map

SDK callEndpoint
products.list() / .iter()GET/api/v1/products/
subscriptions.list() / .iter()GET/api/v1/subscriptions/
subscriptions.get(id)GET/api/v1/subscriptions/{id}/
subscriptions.create(...)POST/api/v1/subscriptions/
subscriptions.cancel(id)POST/api/v1/subscriptions/{id}/cancel/
subscriptions.resume(id)POST/api/v1/subscriptions/{id}/resume/
subscriptions.update_billing_cycle(...)POST/api/v1/subscriptions/update-billing-cycle/
customers.list() / .iter()GET/api/v1/customers/
customers.get(id)GET/api/v1/customers/{id}/

Interactive schema for the same API lives at /v1/swagger/.

Subscription states

StatusMeaning
pending_checkoutCreated, waiting on the buyer to pay at checkout_url.
activePaid and current. The only status where is_active is true.
duePast its billing date, inside the grace period.
pending_cancellationCancellation requested; runs to the end of the paid period.
cancelledCancellation has taken effect.
inactiveLapsed past the grace period.

Development

The test suite runs fully offline against a stub session — no key, no network, no framework required.

python test_suqo.py     # or: pytest test_suqo.py

example.py in the repository root prints all three listings against a live account, and is the quickest way to confirm a key works.

To point the SDK at a non-production environment:

client = SuqoClient(base_url="https://dev.be.suqo.ai")