Official Python SDK for the AWSYS.CO URL Shortener API
Project description
awsysco — Official Python SDK
The official Python SDK for the AWSYS.CO URL Shortener API.
Installation
pip install awsysco
Requires Python 3.9+.
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)
Authentication
Generate an API key from your AWSYS dashboard. All API keys begin with awsys_.
client = Client(api_key="awsys_...")
Store keys in environment variables — never hardcode them:
import os
from awsysco import Client
client = Client(api_key=os.environ["AWSYS_API_KEY"])
API Reference
Links
| Method | Description |
|---|---|
client.links.create(url, *, custom_slug, expires_at, max_clicks) |
Create a shortened link |
client.links.list(*, limit=20, offset=0) |
List links (paginated) |
client.links.get(short_path) |
Get a single link |
client.links.update(short_path, *, expires_at, max_clicks) |
Update link settings |
client.links.delete(short_path) |
Delete a link |
# Create with options
link = client.links.create(
"https://example.com",
custom_slug="my-link",
expires_at="2025-12-31T23:59:59Z",
max_clicks=1000,
)
# Paginate
page1 = client.links.list(limit=20, offset=0)
page2 = client.links.list(limit=20, offset=20)
# Update
updated = client.links.update("my-link", max_clicks=500)
# Delete
client.links.delete("my-link")
Analytics
| Method | Description |
|---|---|
client.analytics.get_stats(short_path) |
Get raw per-click stats for a link |
client.analytics.get_aggregate_stats(short_path, *, period=None) |
Get rolled-up (aggregated) stats for a link |
stats = client.analytics.get_stats("abc123")
print(stats.total_clicks)
for click in stats.clicks:
print(click.country, click.device, click.timestamp)
get_aggregate_stats returns server-side aggregations (clicks-by-day,
country/device/UTM breakdowns, unique visitors). The breakdowns present are
tier-gated — free-tier responses include an upgrade_for_more hint and
omit the richer breakdowns; higher tiers populate device_breakdown,
utm_breakdown, hour_breakdown, etc.
agg = client.analytics.get_aggregate_stats("abc123", period="30d")
print(agg.total_clicks, agg.unique_visitors, agg.tier)
for day in agg.clicks_by_day:
print(day.date, day.clicks)
print(agg.country_breakdown) # {"US": 100, ...}
if agg.device_breakdown: # populated on Pro+
print(agg.device_breakdown.mobile, agg.device_breakdown.desktop)
if agg.upgrade_for_more: # present on free tier
print(agg.upgrade_for_more.message, agg.upgrade_for_more.available)
QR Codes
| Method | Description |
|---|---|
client.qr.get_url(short_code, *, size=300, color='000000', bg_color='FFFFFF') |
Build QR image URL |
No HTTP request is made — this method constructs and returns the URL string.
url = client.qr.get_url("abc123", size=400, color="FF5733", bg_color="FFFFFF")
# https://awsys.co/api/qr/abc123?size=400&color=FF5733&bgColor=FFFFFF
Folders
| Method | Description |
|---|---|
client.folders.list() |
List all folders |
client.folders.create(name, *, color) |
Create a folder |
client.folders.delete(folder_id) |
Delete a folder |
client.folders.assign_link(short_path, folder_id) |
Assign a link to a folder |
client.folders.remove_link(short_path) |
Remove a link from its folder |
folder = client.folders.create("Q1 Campaign", color="#FF5733")
client.folders.assign_link("abc123", folder.id)
folders = client.folders.list()
for f in folders.folders:
print(f.name, f.link_count)
client.folders.remove_link("abc123")
client.folders.delete(folder.id)
Bulk Create
| Method | Description |
|---|---|
client.bulk.create(urls) |
Create multiple links in one request |
result = client.bulk.create([
{"url": "https://example.com/page-1"},
{"url": "https://example.com/page-2", "custom_slug": "page-two"},
{"url": "https://example.com/page-3", "max_clicks": 100},
])
print(f"Created: {result.created}, Failed: {result.failed}")
for r in result.results:
print(r.short_url, r.success)
Me
| Method | Description |
|---|---|
client.me.get() |
Get the authenticated user's profile |
me = client.me.get()
print(me.email, me.subscription_tier, me.is_premium)
Usage
| Method | Description |
|---|---|
client.usage.get() |
Get live account consumption + tier limits |
Distinct from me.get() (static profile), usage.get() returns live counters
(links, clicks, QR codes, API calls, tracked clicks), the current tier limits,
and any active overage state. Limit fields may be an integer or the literal
string "unlimited".
usage = client.usage.get()
print(usage.total_links, usage.tracked_clicks_this_month)
print(usage.limits.monthly_links) # int or "unlimited"
if usage.overage.active:
print(f"Overage charge: {usage.overage.estimated_charge_cents}c")
Web2App
| Method | Description |
|---|---|
client.web2app.consume_session(token) |
Consume a Web2App deep-link session |
Web2App sessions are single-use (consumed on read) with a 24-hour TTL.
Unknown, expired, or already-consumed tokens raise AwsysNotFoundError;
malformed tokens raise AwsysValidationError.
session = client.web2app.consume_session("0123456789abcdef0123456789abcdef")
print(session.link_id, session.utm_params, session.country)
Imports
| Method | Description |
|---|---|
client.imports.start(*, provider, access_token, target_namespace=None, scan_only=None) |
Start a provider link-import job |
client.imports.get_status(job_id) |
Get the current state of an import job |
client.imports.cancel(job_id) |
Cancel an in-progress import job |
client.imports.list(*, limit=None) |
List import jobs for the account |
client.imports.wait_for_completion(job_id, *, poll_interval=2.0, timeout=120.0) |
Poll until the job reaches a terminal state |
Import jobs run asynchronously server-side. start returns a pending
ImportJob; poll get_status (or use wait_for_completion) until the status
is one of completed, partial, failed, or cancelled. Pass
scan_only=True to preview what would be imported without writing links.
job = client.imports.start(provider="bitly", access_token="<bitly-token>")
print(job.id, job.status)
# Block until the import finishes (raises TimeoutError if it overruns).
done = client.imports.wait_for_completion(job.id, poll_interval=5.0, timeout=300.0)
print(done.status, done.counts.written, done.counts.errored)
for err in done.errors:
print(err)
# Or drive it manually
for j in client.imports.list(limit=10):
print(j.id, j.provider, j.status)
client.imports.cancel(job.id)
All imports methods are available identically on AsyncClient
(await client.imports.start(...), await client.imports.wait_for_completion(...)).
Error Handling
All errors inherit from AwsysError.
from awsysco import (
Client,
AwsysError,
AwsysAuthError,
AwsysForbiddenError,
AwsysNotFoundError,
AwsysConflictError,
AwsysValidationError,
AwsysRateLimitError,
)
try:
link = client.links.get("nonexistent")
except AwsysNotFoundError:
print("Link not found")
except AwsysAuthError:
print("Invalid API key")
except AwsysConflictError as e:
print(f"Slug already taken: {e.message}")
except AwsysValidationError as e:
print(f"Bad request: {e.message} ({e.code})")
except AwsysRateLimitError as e:
print(f"Rate limited. Retry after {e.retry_after}s")
except AwsysError as e:
print(f"API error {e.status}: {e.message}")
| Exception | HTTP Status | When raised |
|---|---|---|
AwsysValidationError |
400 | Invalid request parameters |
AwsysAuthError |
401 | Missing or invalid API key |
AwsysForbiddenError |
403 | Insufficient permissions |
AwsysNotFoundError |
404 | Resource does not exist |
AwsysConflictError |
409 | Custom slug already taken |
AwsysRateLimitError |
429 | Too many requests |
AwsysError |
5xx | Server errors |
All exceptions expose .message, .code, .status, and .raw attributes.
Rate Limiting
The SDK automatically retries on 429 Too Many Requests with exponential backoff (up to 3 retries). The Retry-After response header is respected.
from awsysco import AwsysRateLimitError
try:
link = client.links.create("https://example.com")
except AwsysRateLimitError as e:
print(f"Still rate limited after retries. Retry after: {e.retry_after}s")
Custom Base URL
# Point at staging
client = Client(
api_key="awsys_...",
base_url="https://staging.awsys.co",
)
Context Manager
with Client(api_key="awsys_...") as client:
link = client.links.create("https://example.com")
print(link.short_url)
# HTTP connections are closed automatically
Models
All responses are parsed into Pydantic v2 models:
| Model | Fields |
|---|---|
Link |
id, short_url, short_code, long, clicks, created, expires_at, max_clicks, password_protected |
LinkList |
links, total, has_more |
LinkStats |
short_code, total_clicks, clicks |
ClickEvent |
timestamp, country, device, browser, os, referrer |
Folder |
id, name, color, link_count, created_at |
FolderList |
folders, limit, used |
BulkResult |
created, failed, results |
BulkLinkResult |
success, short_url, long, error |
MeResponse |
uid, email, subscription_tier, user_prefix, is_premium, features, limits |
UsageStats |
total_links, total_clicks, links_created_this_month, qr_codes_this_month, folder_count, api_calls_this_month, tracked_clicks_this_month, tier, limits, has_api_key, api_key_created_at, user_prefix, is_premium, overage |
UsageLimits |
links_per_month, monthly_links, daily_links, monthly_tracked_clicks, qr_codes, folders (each int or "unlimited"), api_calls_per_month, custom_slugs |
UsageOverage |
active, started_at, expires_at, hours_until_drop, clicks_this_cycle, spending_limit_cents, estimated_charge_cents |
Web2AppSession |
success, link_id, utm_params, routing_rule, country, clicked_at |
ImportJob |
id, user_id, provider, status, scan_only, target_namespace, scope_filter, counts, errors, created_at, updated_at |
ImportCounts |
fetched, transformed, written, errored |
AggregateAnalytics |
short_code, full_path, period, total_clicks, unique_visitors, clicks_by_day, country_breakdown, tier_limit, tier, device_breakdown, referrer_breakdown, browser_breakdown, os_breakdown, source_breakdown, hour_breakdown, utm_breakdown, upgrade_for_more |
DayClicks / HourClicks |
date/hour, clicks |
DeviceBreakdown |
mobile, desktop, tablet |
UTMBreakdown |
sources, mediums, campaigns |
UpgradeForMore |
available, message |
Development Setup
git clone https://github.com/AlphaWaveSystems/awsysco-python-sdk.git
cd awsysco-python-sdk
pip install -e ".[dev]"
# Configure test credentials
cp .env.example .env.test
# Edit .env.test — add your AWSYS_API_KEY (staging recommended)
# Run tests
pytest
# Run with 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
- Run
pytest— all tests must pass - Open a pull request
Please read SECURITY.md before contributing — never commit API keys.
License
MIT License — see LICENSE for details.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file awsysco-1.3.0.tar.gz.
File metadata
- Download URL: awsysco-1.3.0.tar.gz
- Upload date:
- Size: 44.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f4151a044302acc2615556edebc8a56c190b6ddb9c0e9048a998a1dc6ed6807f
|
|
| MD5 |
26c313663c26fac5f786f025c9bc135f
|
|
| BLAKE2b-256 |
278700c11b40e85193a90e4e4e506546902d7c7f8fba5fcf2b0402617c0e442b
|
Provenance
The following attestation bundles were made for awsysco-1.3.0.tar.gz:
Publisher:
publish.yml on AlphaWaveSystems/awsysco-python-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
awsysco-1.3.0.tar.gz -
Subject digest:
f4151a044302acc2615556edebc8a56c190b6ddb9c0e9048a998a1dc6ed6807f - Sigstore transparency entry: 1985054806
- Sigstore integration time:
-
Permalink:
AlphaWaveSystems/awsysco-python-sdk@cbf85c48120c96ebd9e375ecbff18bfd9740a1f3 -
Branch / Tag:
refs/tags/v1.3.0 - Owner: https://github.com/AlphaWaveSystems
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@cbf85c48120c96ebd9e375ecbff18bfd9740a1f3 -
Trigger Event:
push
-
Statement type:
File details
Details for the file awsysco-1.3.0-py3-none-any.whl.
File metadata
- Download URL: awsysco-1.3.0-py3-none-any.whl
- Upload date:
- Size: 46.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
55bf05bc95eabf9bd9a041f61097c4ef4ab7db65841cfce2e5519bf997b5b53d
|
|
| MD5 |
c035d4052d1ef7522944b89c7e3916b0
|
|
| BLAKE2b-256 |
2b7b40709fc91bc28bdd68012e7a5182bcb0ac5052ef0aaa7edf21c29784ba51
|
Provenance
The following attestation bundles were made for awsysco-1.3.0-py3-none-any.whl:
Publisher:
publish.yml on AlphaWaveSystems/awsysco-python-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
awsysco-1.3.0-py3-none-any.whl -
Subject digest:
55bf05bc95eabf9bd9a041f61097c4ef4ab7db65841cfce2e5519bf997b5b53d - Sigstore transparency entry: 1985054926
- Sigstore integration time:
-
Permalink:
AlphaWaveSystems/awsysco-python-sdk@cbf85c48120c96ebd9e375ecbff18bfd9740a1f3 -
Branch / Tag:
refs/tags/v1.3.0 - Owner: https://github.com/AlphaWaveSystems
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@cbf85c48120c96ebd9e375ecbff18bfd9740a1f3 -
Trigger Event:
push
-
Statement type: