syndicate-links
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
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
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
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
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:
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:
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
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:
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)
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.
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
Metadata
Release files for syndicate-links 0.1.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 | |
|---|---|---|---|
| syndicate_links-0.1.0.tar.gz | 31.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| syndicate_links-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 57.5 kB
Release files / syndicate_links-0.1.0.tar.gz
| Download URL | syndicate_links-0.1.0.tar.gz |
|---|---|
| Size | 31.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
12d1b40eaac6a6216e8a88e15567ec296a466e07316c0546db06f5050baccefa
|
|
BLAKE2b-256 checksum How to use checksums |
41a7a714b59304dd28c35ce12ed77465b207ddb3fdfac67c5bfff941b953e022
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|
Release files / syndicate_links-0.1.0-py3-none-any.whl
| Download URL | syndicate_links-0.1.0-py3-none-any.whl |
|---|---|
| Size | 25.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
17fa64dbac3f3c10658415d68353811635568d8586e670a8b52fc33852947344
|
|
BLAKE2b-256 checksum How to use checksums |
f701bef5e4bffdbb9d399a74c8399ac962f700b198465539c76d91c0b6519efc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|