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:
| Requirement | Why 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.
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,
)
| Parameter | Default | Notes |
|---|---|---|
api_key | SUQO_API_KEY | Falls back to the environment variable. Raises ValueError if neither is set. |
base_url | Production | Host only — /api/v1/ is appended. Point it at https://dev.be.suqo.ai for dev. |
timeout | 30 | Seconds, applied to every request. |
max_retries | 3 | GET only. Pass 0 to disable. |
session | New session | Inject your own requests.Session for proxies, custom TLS or tests. |
Attributes and helpers
| Member | What it does |
|---|---|
.products | Product catalogue, read only. |
.subscriptions | Subscription list, creation and lifecycle. |
.customers | Your 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)
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": "..."
}
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)
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)
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)
| Field | Required | Notes |
|---|---|---|
pbp_id | Yes | A visible billing period belonging to one of your unarchived products. |
return_url | Yes | Where the buyer lands after checkout. |
client.phone | Yes | Max 15 characters. |
client.full_name | Yes | Max 255 characters. |
client.email | Yes | Validated as an email address. |
client.address | No | Free text. |
client.billing | No | If present, billing_business_name, billing_email and billing_address all become required; billing_pan_vat stays optional. |
client.shipping | No | If 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)
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)
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)
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")
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)
client.customers.get(customer_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
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.
| Status | Exception | Usual cause |
|---|---|---|
| 400 | ValidationError | Bad field, unknown pbp_id, or a billing date in the past. |
| 401 | AuthenticationError | Missing, invalid or expired key; or the seller account is inactive. |
| 403 | PermissionDeniedError | KYC not verified. |
| 404 | NotFoundError | No such resource on your account — other sellers' data reads as missing. |
| 429 | RateLimitError | Too many requests. GETs retry this automatically first. |
| 5xx | ServerError | The 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 call | Endpoint | |
|---|---|---|
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
| Status | Meaning |
|---|---|
pending_checkout | Created, waiting on the buyer to pay at checkout_url. |
active | Paid and current. The only status where is_active is true. |
due | Past its billing date, inside the grace period. |
pending_cancellation | Cancellation requested; runs to the end of the paid period. |
cancelled | Cancellation has taken effect. |
inactive | Lapsed 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")