Metadata-Version: 2.4
Name: syndicate-links
Version: 0.1.0
Summary: Official Python SDK for Syndicate Links — the agent-native affiliate and commission platform.
Project-URL: Homepage, https://syndicatelinks.co
Project-URL: Documentation, https://syndicatelinks.co/docs/python-sdk
Project-URL: Repository, https://github.com/syndicate-links/syndicate-links-python
Project-URL: Issues, https://github.com/syndicate-links/syndicate-links-python/issues
Project-URL: Changelog, https://github.com/syndicate-links/syndicate-links-python/blob/main/CHANGELOG.md
Author-email: Syndicate Links <hello@syndicatelinks.co>
License: MIT
License-File: LICENSE
Keywords: affiliate,ai-agents,attribution,commission,mcp,syndicate-links,tracking
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.5
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Requires-Dist: twine>=5.0; extra == 'dev'
Description-Content-Type: text/markdown

# syndicate-links

[![PyPI version](https://img.shields.io/pypi/v/syndicate-links.svg?color=8b5cf6)](https://pypi.org/project/syndicate-links/)
[![Python versions](https://img.shields.io/pypi/pyversions/syndicate-links.svg?color=3b82f6)](https://pypi.org/project/syndicate-links/)
[![License: MIT](https://img.shields.io/badge/license-MIT-22c55e.svg)](https://opensource.org/licenses/MIT)
[![CI](https://img.shields.io/github/actions/workflow/status/syndicate-links/syndicate-links-python/ci.yml?branch=main)](https://github.com/syndicate-links/syndicate-links-python/actions)
[![Docs](https://img.shields.io/badge/docs-syndicatelinks.co-0ea5e9)](https://syndicatelinks.co/docs/python-sdk)

> Official Python SDK for **Syndicate Links** — the agent-native affiliate and commission platform. Works for SaaS and physical-goods merchants. Native support for AI agent publishers.

Type-checked Pydantic models, sync **and** async clients, automatic cursor pagination, retries with exponential backoff, and a clean exception hierarchy. One package, one API key, every endpoint.

---

## Installation

```bash
pip install syndicate-links
```

Requires Python 3.9+. Depends only on `httpx` and `pydantic` v2.

---

## Quick start

### 1. Merchant: launch a program and add products

```python
from syndicate_links import SyndicateClient

client = SyndicateClient(api_key="mk_live_…")

program = client.merchant.programs.create(
    name="Acme Pro",
    default_commission_pct=20,
    cookie_days=30,
    auto_approve=True,
    category="saas",
)

client.merchant.products.create(
    program_id=program.id,
    name="Acme Pro (Monthly)",
    url="https://acme.com/pro",
    price=49.00,
    commission_pct=25,  # overrides program default
)

# Or import an entire catalog in one shot
client.merchant.products.bulk_create(
    program_id=program.id,
    products=[
        {"name": "Starter", "url": "https://acme.com/starter", "price": 9},
        {"name": "Team",    "url": "https://acme.com/team",    "price": 29},
        {"name": "Pro",     "url": "https://acme.com/pro",     "price": 49},
    ],
)
```

### 2. Affiliate / publisher: join a program and create a tracking link

```python
from syndicate_links import SyndicateClient

client = SyndicateClient(api_key="ak_live_…")

# Discover programs
for program in client.affiliate.programs.iter_all(category="saas"):
    print(f"{program.name} — {program.default_commission_pct}%")

# Apply (auto-approve programs return status='approved' instantly)
partnership = client.affiliate.programs.apply(program_id="prog_…")

# Mint a tracking link
link = client.affiliate.links.create(
    program_id="prog_…",
    destination_url="https://acme.com/pro",
    source_tag="newsletter-april",
)
print(f"Share this code: {link.code}")
```

### 3. AI agent: mint an attribution token and report a conversion

```python
from syndicate_links import SyndicateClient

# Agent keys begin with `aff_agent_` and route to the affiliate sub-client.
agent = SyndicateClient(api_key="aff_agent_…")

# 1. Tracking link for the agent's recommendation
link = agent.affiliate.links.create(
    program_id="prog_…",
    destination_url="https://acme.com/pro",
    source_tag="chatgpt-plugin",
)

# 2. Sign an attribution token the merchant can later verify
token = agent.affiliate.attribution_token.create(
    program_id="prog_…",
    tracking_code=link.code,
    agent_id="acme-research-bot",
    surface="chatgpt",
    ttl_seconds=3600,
)

# 3. When the user converts, record it with AI-native attribution
conversion = agent.affiliate.events.conversion(
    tracking_code=link.code,
    order_id="ord_42",
    sale_amount=49.00,
    ai_referral="ai",          # → API attributionMethod
    ai_surface="chatgpt",      # → flows into AI endorsement reports
)

print(f"Earned ${conversion.commission_amount} on order {conversion.order_id}")
```

---

## Authentication

The Syndicate Links API uses bearer tokens. The SDK auto-detects the key type from its prefix and routes you to the correct sub-client:

| Prefix         | Key type            | Sub-client            |
| -------------- | ------------------- | --------------------- |
| `mk_live_`     | Merchant            | `client.merchant.*`   |
| `ak_live_`     | Affiliate / human   | `client.affiliate.*`  |
| `aff_agent_`   | Affiliate / AI agent| `client.affiliate.*`  |

Pass the key to the constructor:

```python
client = SyndicateClient(
    api_key="mk_live_…",
    base_url="https://api.syndicatelinks.co",  # override for dev
    timeout=30.0,
    max_retries=3,
)
```

If you try to access the wrong sub-client you get a clear, immediate error:

```python
client = SyndicateClient(api_key="ak_live_…")
client.merchant.programs.list()
# raises ForbiddenError:
#   "Merchant methods require a merchant key (mk_live_...),
#    but an affiliate key was provided."
```

Get an API key by signing up at **<https://syndicatelinks.co>** (or call `client.merchant.register(...)` / `client.affiliate.register(...)` programmatically — the API key is returned **once** so save it immediately).

---

## Pagination

Every list endpoint is cursor-paginated. You have two options.

### Manual pagination

```python
page = client.merchant.programs.list(limit=50)
print(f"Got {len(page.data)} programs, more = {page.has_more}")

while page.has_more:
    page = client.merchant.programs.list(cursor=page.cursor, limit=50)
    for program in page.data:
        ...
```

### Auto-pagination with `iter_all()`

Every list method has a sibling `iter_all()` generator that walks the cursor for you:

```python
for program in client.merchant.programs.iter_all():
    print(program.name)

for product in client.merchant.products.iter_all(program_id="prog_…"):
    print(product.name, product.price)

for partnership in client.merchant.affiliates.iter_all(status="pending"):
    client.merchant.affiliates.approve(partnership.partnership.id)
```

---

## Error handling

All errors inherit from `SyndicateError`. HTTP errors raise the matching subclass so you can catch by status:

```
SyndicateError
└── APIError
    ├── ValidationError      (400)
    ├── AuthenticationError  (401)
    ├── ForbiddenError       (403)   ← named "ForbiddenError" so it
    ├── NotFoundError        (404)     doesn't shadow Python's built-in
    ├── ConflictError        (409)     PermissionError
    ├── RateLimitError       (429)
    └── ServerError          (5xx, after retries)
└── NetworkError             (timeout, DNS, connection reset)
```

```python
from syndicate_links import (
    SyndicateClient,
    AuthenticationError,
    NotFoundError,
    ValidationError,
    NetworkError,
)

client = SyndicateClient(api_key="mk_live_…")

try:
    program = client.merchant.programs.update("prog_does_not_exist", name="x")
except NotFoundError as e:
    print(f"Nope: {e.message}")
except ValidationError as e:
    print(f"Bad input: {e.message} ({e.response})")
except AuthenticationError:
    print("Key is invalid or expired")
except NetworkError as e:
    print(f"Couldn't reach the API: {e}")
```

5xx responses and network errors are **automatically retried** up to `max_retries` times (default 3) with exponential backoff and jitter. 4xx errors are raised immediately.

---

## Async usage

For async codebases, swap in `AsyncSyndicateClient`. Same arguments, same sub-clients, every method is a coroutine.

```python
import asyncio
from syndicate_links import AsyncSyndicateClient

async def main() -> None:
    async with AsyncSyndicateClient(api_key="mk_live_…") as client:
        program = await client.merchant.programs.create(
            name="Acme Pro",
            default_commission_pct=20,
        )
        async for product in client.merchant.products.iter_all(program_id=program.id):
            print(product.name)

asyncio.run(main())
```

---

## Full API reference

### Merchant (`client.merchant`)

| Method | Description |
| --- | --- |
| `merchant.register(name, email, …)` | Create a merchant account; returns the API key **once**. |
| `merchant.programs.list(cursor, limit)` | List your programs (paginated). |
| `merchant.programs.iter_all()` | Iterate every program across pages. |
| `merchant.programs.create(name, default_commission_pct, **kwargs)` | Launch a new program. |
| `merchant.programs.update(program_id, **fields)` | Partial update. |
| `merchant.products.list(program_id, cursor, limit)` | List products. |
| `merchant.products.iter_all(program_id)` | Iterate every product. |
| `merchant.products.create(program_id, name, url, price, **kwargs)` | Create one product. |
| `merchant.products.bulk_create(program_id, products)` | Bulk import — returns `{"inserted": n}`. |
| `merchant.products.update(product_id, **fields)` | Partial update. |
| `merchant.products.delete(product_id)` | Delete a product. |
| `merchant.affiliates.list(status, cursor, limit)` | List partnerships in your programs. |
| `merchant.affiliates.iter_all(status)` | Iterate every partnership. |
| `merchant.affiliates.approve(partnership_id)` | Approve a pending partnership. |
| `merchant.affiliates.reject(partnership_id)` | Reject a pending partnership. |
| `merchant.conversions.list(cursor, limit)` | List conversions across your programs. |
| `merchant.conversions.create(tracking_code, order_id, sale_amount, currency)` | Server-side conversion report. |
| `merchant.refunds.create(order_id, refund_amount, reason)` | Issue a refund and claw back commission. |
| `merchant.reports.summary(start_date, end_date)` | Aggregate clicks / conversions / revenue. |
| `merchant.reports.ai_endorsements(start_date, end_date)` | AI-attributed conversion breakdown. |
| `merchant.billing.get()` | Plan, usage limits, tier info. |
| `merchant.profile.get()` / `update(**fields)` | Read / update company profile. |
| `merchant.payouts.list(cursor, limit)` / `stats()` | Payouts to your affiliates. |
| `merchant.settings.get()` | Composite settings (company + webhooks + Stripe). |
| `merchant.webhooks.create(url, events, secret)` | Register a webhook (secret returned **once**). |
| `merchant.webhooks.list()` | List your registered webhooks. |
| `merchant.webhooks.delete(webhook_id)` | Remove a webhook. |

### Affiliate / publisher (`client.affiliate`)

| Method | Description |
| --- | --- |
| `affiliate.register(name, email, type, …)` | Create an affiliate / agent account; API key returned **once**. |
| `affiliate.programs.list(cursor, limit, sort, category, search)` | Browse the public program catalog. |
| `affiliate.programs.iter_all(...)` | Iterate every public program. |
| `affiliate.programs.mine(cursor, limit)` | List programs you've joined. |
| `affiliate.programs.get(program_id)` | Fetch a single program. |
| `affiliate.programs.products(program_id, cursor, limit)` | List a program's products. |
| `affiliate.programs.apply(program_id)` | Apply for a partnership. |
| `affiliate.products.search(query, category, cursor, limit)` | Full-text product search across the catalog. |
| `affiliate.partnerships.list(cursor, limit)` | List your partnership applications. |
| `affiliate.links.create(program_id, destination_url, product_id, source_tag)` | Mint a tracking link. |
| `affiliate.links.list(cursor, limit)` | List your tracking links. |
| `affiliate.events.list(cursor, limit)` | Stream of clicks + conversions you've generated. |
| `affiliate.events.click(tracking_code, ip_hash, user_agent, referer)` | Programmatically record a click. |
| `affiliate.events.conversion(tracking_code, order_id, sale_amount, currency, ai_referral, ai_surface)` | **AI-native conversion** with attribution metadata. |
| `affiliate.reports.earnings(start_date, end_date)` | Daily earnings time series. |
| `affiliate.reports.clicks(start_date, end_date)` | Click totals. |
| `affiliate.reports.conversions(start_date, end_date)` | Conversion totals + revenue. |
| `affiliate.me.get()` / `update(**fields)` | Read / update your profile. |
| `affiliate.me.balance()` | Available, pending, and lifetime commission balance. |
| `affiliate.me.payouts(cursor, limit)` | Your past payouts. |
| `affiliate.payouts.claim(amount, rail, invoice, currency)` | Self-serve withdrawal (Lightning today). |
| `affiliate.dashboard.summary()` | Aggregate stats across all your programs. |
| `affiliate.dashboard.earnings_chart(days)` | Daily earnings for the last N days. |
| `affiliate.keys.create(name)` | Mint an additional agent API key. |
| `affiliate.attribution_token.create(program_id, tracking_code, agent_id, surface, ttl_seconds)` | Sign an agent attribution token. |

---

## Links

- 🌐 Website: <https://syndicatelinks.co>
- 📚 SDK docs: <https://syndicatelinks.co/docs/python-sdk>
- 📖 API reference: <https://syndicatelinks.co/docs/api>
- 🐛 Issues: <https://github.com/syndicate-links/syndicate-links-python/issues>
- 💬 Discord: <https://syndicatelinks.co/discord>

---

## License

MIT © Syndicate Links
