awsysco — Official Python SDK
What
The official Python SDK for the AWSYS.CO URL Shortener API — a typed, sync-and-async client covering every capability the platform exposes to an API key: links, analytics, QR codes, folders, bulk create, account/profile/usage, tags, trust scoring, data export, branded namespaces, UTM templates, webhooks, saved views, custom domains, affiliate programs, AgentLink, Web2App, and provider imports. Every method is documented and Pydantic-typed; every error maps to a specific, catchable exception.
Install
pip install awsysco
Requires Python 3.9+. See Supported Versions below.
Authentication
Generate an API key from your AWSYS dashboard.
All API keys begin with awsys_.
from awsysco import Client
client = Client(api_key="awsys_...")
Or set AWSYS_API_KEY in your environment and omit api_key entirely:
export AWSYS_API_KEY=awsys_...
client = Client() # picks up AWSYS_API_KEY automatically
Passing neither raises AwsysConfigurationError before any network call is made.
Quick Start
from awsysco import Client
client = Client(api_key="awsys_your_key_here")
# Shorten a URL
link = client.links.create("https://example.com/very/long/path")
print(link.short_url) # https://awsys.co/abc123
# Get analytics
stats = client.analytics.get_stats(link.short_code)
print(f"Total clicks: {stats.total_clicks}")
# Build a QR code URL
qr_url = client.qr.get_url(link.short_code, size=400)
print(qr_url)
Core Resources
Every resource below is available on client.<resource> (sync Client) and
identically on await client.<resource>.<method>(...) (AsyncClient).
Links — client.links
link = client.links.create("https://example.com", custom_slug="my-link", max_clicks=1000)
page = client.links.list(limit=20, offset=0)
one = client.links.get("my-link")
updated = client.links.update("my-link", max_clicks=500)
client.links.delete("my-link")
# Auto-paginate every link (see Pagination below)
for link in client.links.list_all():
print(link.short_code)
Analytics — client.analytics
stats = client.analytics.get_stats("abc123")
print(stats.total_clicks)
agg = client.analytics.get_aggregate_stats("abc123", period="30d")
print(agg.total_clicks, agg.unique_visitors, agg.country_breakdown)
if agg.upgrade_for_more: # present on free tier; richer breakdowns need Pro+
print(agg.upgrade_for_more.message)
# Requires the account's "Live Globe" feature flag; 403 FEATURE_DISABLED otherwise.
recent = client.analytics.get_recent_clicks(limit=10)
QR Codes — client.qr
# Client-side URL builder — no HTTP request is made.
url = client.qr.get_url("abc123", size=400, color="FF5733", bg_color="FFFFFF")
settings = client.qr.get_settings("abc123")
client.qr.update_settings("abc123", {"color": "#ff0000"})
Folders — client.folders
folder = client.folders.create("Q1 Campaign", color="#FF5733")
client.folders.assign_link("abc123", folder.id)
client.folders.update(folder.id, name="Q1 Campaign (final)")
client.folders.remove_link("abc123")
client.folders.delete(folder.id)
Bulk Create — client.bulk
result = client.bulk.create([
{"url": "https://example.com/page-1"},
{"url": "https://example.com/page-2", "custom_slug": "page-two"},
])
print(result.created, result.failed)
Me / Profile / Usage
Three distinct, non-overlapping views of the account:
me = client.me.get() # subscription tier, features, limits
profile = client.profile.get() # account profile: email, display name
usage = client.usage.get() # live consumption counters + overage state
client.profile.update(display_name="New Name")
Tags — client.tags
client.tags.add("abc123", "promo")
client.tags.remove("abc123", "promo")
Trust Score — client.trust_score
result = client.trust_score.scan("abc123")
print(result.score, result.status, result.threats)
Data Export — client.data_export
csv_text = client.data_export.export_links() # all links, as CSV
stats_csv = client.data_export.export_link_stats("abc123")
Namespace — client.namespace
info = client.namespace.get()
available = client.namespace.check("acme")
client.namespace.claim("acme")
client.namespace.release()
UTM Templates — client.utm_templates
client.utm_templates.create("Launch", "newsletter", "email", "sept")
for t in client.utm_templates.list():
print(t.name)
client.utm_templates.delete(t.id)
Webhooks — client.webhooks
webhook = client.webhooks.create("https://you.example/hook", ["link.created", "link.click"])
client.webhooks.update(webhook.id, enabled=False)
client.webhooks.test(webhook.id, "link.created")
client.webhooks.delete(webhook.id)
Saved Views — client.saved_views
view = client.saved_views.create("My View", {"tag": "promo"})
client.saved_views.update(view.id, name="Renamed View")
client.saved_views.delete(view.id)
Custom Domains — client.custom_domains
client.custom_domains.add("go.example.com")
client.custom_domains.verify("go.example.com")
client.custom_domains.update("go.example.com", default_redirect="https://example.com/")
# .activate() is deprecated — Firebase-only, unreachable with an API key. Use the dashboard.
Affiliate — client.affiliate
program = client.affiliate.create_program("Partner Program", "cpa_return", cpa_rate=15)
client.affiliate.discover(limit=20)
client.affiliate.join(program.id, partner_code="LAUNCH")
stats = client.affiliate.get_program_stats(program.id, period="30d")
AgentLink — client.agentlink
client.agentlink.subscribe("dev@example.com") # public, no auth required
stats = client.agentlink.get_account_stats(period_days=7)
Web2App — client.web2app
Sessions are single-use (consumed on read) with a 24-hour TTL.
session = client.web2app.consume_session("0123456789abcdef0123456789abcdef")
print(session.link_id, session.utm_params, session.country)
Imports — client.imports
job = client.imports.start(provider="bitly", access_token="<bitly-token>")
done = client.imports.wait_for_completion(job.id, poll_interval=5.0, timeout=300.0)
print(done.status, done.counts.written, done.counts.errored)
csv_map = client.imports.get_redirect_map_csv(job.id)
json_map = client.imports.get_redirect_map_json(job.id)
Pagination
Only links.list() is paginated (offset/limit; the platform ignores offsets on
every other list endpoint, so the SDK doesn't fake them). limit is clamped to the
platform max of 100 client-side.
page1 = client.links.list(limit=20, offset=0)
page2 = client.links.list(limit=20, offset=20)
print(page1.has_more) # None if the platform didn't send pagination info
# Or auto-paginate everything:
for link in client.links.list_all(limit=100):
...
# Async equivalent:
async for link in async_client.links.list_all():
...
The iterator stops when the platform reports has_more=False, or a page comes
back shorter than the requested limit (including empty) — this guards against a
response that omits has_more entirely.
Errors
All errors inherit from AwsysError.
from awsysco import (
Client,
AwsysError,
AwsysConfigurationError,
AwsysAuthError,
AwsysForbiddenError,
AwsysNotFoundError,
AwsysConflictError,
AwsysValidationError,
AwsysRateLimitError,
AwsysServerError,
AwsysNetworkError,
AwsysTimeoutError,
)
try:
link = client.links.get("nonexistent")
except AwsysNotFoundError:
print("Link not found")
except AwsysRateLimitError as e:
print(f"Rate limited (code={e.code}). Retry after {e.retry_after}s, resets {e.resets_at}")
except AwsysServerError as e:
print(f"Platform error {e.status}: {e.message}")
except AwsysTimeoutError:
print("Request timed out")
except AwsysError as e:
print(f"API error {e.status}: {e.message}")
| Exception | HTTP status | When raised |
|---|---|---|
AwsysConfigurationError |
– | Missing API key or invalid base_url, before any network call |
AwsysValidationError |
400 | Invalid request parameters |
AwsysAuthError |
401 | Missing or invalid API key |
AwsysForbiddenError |
403 | Insufficient permissions / tier / feature flag |
AwsysNotFoundError |
404 | Resource does not exist |
AwsysConflictError |
409 | Custom slug already taken |
AwsysRateLimitError |
429 | Too many requests (.code, .retry_after, .resets_at) |
AwsysServerError |
5xx | Platform-side error |
AwsysNetworkError |
– | Connection failure (no HTTP response at all) |
AwsysTimeoutError |
– | Request exceeded its timeout (subclass of AwsysNetworkError) |
AwsysError |
any | Base class; catches everything above |
Every exception exposes .message, .code, .status, and .raw (the parsed —
or raw text — response body). The platform emits several error-body shapes
({error:true,code,message}, {error:"<string>",code}, {success:false,...},
and occasionally non-JSON bodies); the SDK tolerates all of them.
Configuration
client = Client(
api_key="awsys_...", # or omit to read AWSYS_API_KEY
base_url="https://staging.awsys.co", # or omit to read AWSYS_BASE_URL, default https://awsys.co
timeout=30.0, # per-request default, in seconds
max_retries=3, # retry attempts for 429s / retryable 5xx / transport errors
)
base_url must start with http:// or https:// (anything else raises
AwsysConfigurationError); a plain http:// URL is accepted but emits a warning.
The API key and base URL are never included in repr(client), log output, or any
exception — repr() shows a redacted awsys_...last4 form.
Advanced
Async: every resource and method is mirrored on AsyncClient.
import asyncio
from awsysco import AsyncClient
async def main():
async with AsyncClient(api_key="awsys_...") as client:
link = await client.links.create("https://example.com")
print(link.short_url)
asyncio.run(main())
Retries: 429 is retried for every HTTP method (except quota-exhaustion 429s —
HOURLY_LIMIT_EXCEEDED/DAILY_LIMIT_EXCEEDED/MONTHLY_LIMIT_EXCEEDED — which raise
immediately, since waiting a few seconds can't help). 502/503/504 and
transport-level failures (connection reset/refused, DNS) are retried only for
idempotent methods (GET/PUT/DELETE) — the platform has no idempotency keys, so
a retried POST/PATCH could create duplicates. Backoff uses the Retry-After
header when present (seconds or an HTTP-date), otherwise 1s × 2^attempt capped at
30s, with full jitter. Set max_retries=0 to disable retries entirely.
Timeouts: 30s default per attempt, configurable at the client level
(Client(timeout=...)) or per call via each resource method's underlying request.
Context managers: with Client(...) as client: / async with AsyncClient(...) as client:
close the underlying HTTP connection pool automatically.
Models
Every response is parsed into a Pydantic v2 model. All fields are Optional
(the platform's response shapes vary by tier/feature flags), unknown fields from
the platform are preserved rather than rejected, and timestamp fields accept both
plain ISO-8601 strings and Firestore's {_seconds,_nanoseconds} shape (normalized
to an ISO-8601 string — models expose timestamps as str, not a native datetime,
in the 1.x series; native datetime support is planned for 2.0).
Supported Versions
Python 3.9, 3.10, 3.11, 3.12, and 3.13 are tested in CI on every push. Runtime
dependencies: httpx>=0.27,<1, pydantic>=2,<3.
Development Setup
git clone https://github.com/AlphaWaveSystems/awsysco-python-sdk.git
cd awsysco-python-sdk
pip install -e ".[dev]"
# Configure test credentials (staging recommended)
cp .env.example .env.test
# Edit .env.test — add your AWSYS_API_KEY
# Unit + contract tests — no network required
pytest -m "not integration"
# Full suite, including live staging integration tests
pytest
# Lint and type-check
ruff check .
mypy awsysco
# Coverage
pytest --cov=awsysco --cov-report=term-missing
Contributing
- Fork the repository
- Create a feature branch (
git checkout -b feat/my-feature) - Make your changes and add tests (see
tests/test_contract.pyif you're touching request/response shapes — it's driven bytests/contracts/sdk-contract.json) - Run
pytest -m "not integration",ruff check ., andmypy awsysco— all must pass - Open a pull request
Security
See SECURITY.md for the secret-hygiene policy (never commit API keys) and SECURITY-REVIEW.md for the SDK's own security review (dependency audit, redaction guarantees, TLS/base-URL validation). Report vulnerabilities to security@awsys.co — do not open a public issue.
License
MIT License — see LICENSE for details.
Metadata
Release files for awsysco 1.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| awsysco-1.4.0.tar.gz | 121.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| awsysco-1.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 182.7 kB
Release files / awsysco-1.4.0.tar.gz
| Download URL | awsysco-1.4.0.tar.gz |
|---|---|
| Size | 121.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
87a84423c833f5f3b56bb977cfe7ca938f185ab9e63d8bfae115fbd4f4829de3
|
|
BLAKE2b-256 checksum How to use checksums |
c4daf02dc4e1826857f72792152a17a358cbf87d51f6a47816bcb880e891c850
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 10, 2026.
Transparency logRelease files / awsysco-1.4.0-py3-none-any.whl
| Download URL | awsysco-1.4.0-py3-none-any.whl |
|---|---|
| Size | 61.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3871bb105c015cf09d7611ef5049a928ff8bb7bfdf2e7cbf1b120f646816092b
|
|
BLAKE2b-256 checksum How to use checksums |
1838bcd169e88cc574698f344a5b924425dad615a895e4888a2d904556131a9a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 10, 2026.
Transparency log