Metadata-Version: 2.4
Name: synter
Version: 0.1.0
Summary: Python SDK for the Synter cross-platform advertising API
Project-URL: Homepage, https://syntermedia.ai
Project-URL: Repository, https://github.com/jshorwitz/synter-media
Project-URL: Issues, https://github.com/jshorwitz/synter-media/issues
Author-email: SynterMedia <hello@syntermedia.ai>
License-Expression: MIT
Keywords: advertising,google-ads,linkedin-ads,meta-ads,microsoft-ads,reddit-ads,tiktok-ads,x-ads
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
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: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx<1.0.0,>=0.24.0
Provides-Extra: dev
Requires-Dist: mypy>=1.5.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
Requires-Dist: pytest>=7.4.0; extra == 'dev'
Requires-Dist: respx>=0.20.0; extra == 'dev'
Requires-Dist: ruff>=0.1.0; extra == 'dev'
Description-Content-Type: text/markdown

# synter (Python)

Python SDK for the [Synter](https://syntermedia.ai) cross-platform advertising API — manage
Google, Meta, LinkedIn, Microsoft, Reddit, TikTok, and X ad campaigns from one client.

> **Not yet published to PyPI.** There is no `pip install synter` from the index yet — install
> from source (see below) until Joel gives the go-ahead to publish. This is alpha software
> (`0.1.0`); expect breaking changes before a stable `1.0`.

This SDK is one of several parallel language implementations (TypeScript, Go, Java, Rust, plus a
Go CLI) that all implement the same contract described in
[`packages/sdk-shared/SPEC.md`](../sdk-shared/SPEC.md) and
[`packages/sdk-shared/catalog.json`](../sdk-shared/catalog.json) in this monorepo. It talks
directly to `https://syntermedia.ai/api/v1/tools/run` — the same production endpoint the
published `@synterai/mcp-server` npm package uses internally, so every call here has already been
exercised in production by every MCP client (Claude, Cursor, Codex, ChatGPT).

## Status

- **Sync client (`Synter`)**: fully implemented, all 25 tools.
- **Async client (`AsyncSynter`)**: fully implemented, mirrors the sync client's entire surface.
- Not published to PyPI. Built, linted, type-checked, and tested only.

## Installation (from source)

```bash
git clone https://github.com/jshorwitz/synter-media.git
cd synter-media/packages/sdk-python
pip install -e .
```

Requires Python 3.9+.

## Getting an API key

Create a key at [syntermedia.ai/developer](https://syntermedia.ai/developer). Keys look like
`syn_` followed by 32 base64url characters. Treat it like a password — anyone with it can act on
your connected ad accounts.

## Quickstart

```python
from synter import Synter

client = Synter(api_key="syn_...")  # or read from an env var yourself

# List campaigns (defaults to Google if no platform is given)
campaigns = client.campaigns.list(platform="google", status="ENABLED", limit=20)

# Create a Search campaign
result = client.campaigns.create_search(
    campaign_name="Q4 Launch",
    daily_budget=50,
    keywords=["running shoes", "trail shoes"],
    headlines=["Buy Shoes Now", "Best Shoes 2026", "Free Shipping"],
    descriptions=["Great shoes for less.", "Shop today."],
    final_url="https://example.com/shoes",
)

# Pull performance metrics
perf = client.analytics.get_performance(platform="google", date_range="LAST_7_DAYS")
```

### Async usage

```python
import asyncio
from synter import AsyncSynter

async def main():
    async with AsyncSynter(api_key="syn_...") as client:
        campaigns = await client.campaigns.list(platform="google")
        print(campaigns)

asyncio.run(main())
```

### The escape hatch: `execute()`

Every backend script (140+ across 19 platforms) is reachable even without a typed method, via
`execute()` — the SDK-level mirror of the `run_tool` MCP tool:

```python
# Idiomatic dict form (recommended): converted to CLI flags automatically.
client.execute("google_ads_list_audiences", {"account_id": "123", "status": "ENABLED"})

# Raw CLI-flag-array form, for full parity with the underlying backend contract.
client.execute("google_ads_list_audiences", ["--account-id", "123", "--status", "ENABLED"])
```

## Error handling

Every failure raises `synter.SynterError` (or its subclass `synter.SynterValidationError` for
client-side validation failures caught before any network call):

```python
from synter import Synter, SynterError, SynterValidationError

client = Synter(api_key="syn_...")

try:
    client.campaigns.create_search(campaign_name="", daily_budget=50, ...)
except SynterValidationError as e:
    print("You called it wrong:", e.message)
except SynterError as e:
    print(f"API error [{e.status}] {e.message} (code={e.code})")
```

`SynterError` carries `message`, `status` (HTTP status, or `0` for network/timeout failures),
`code`, and `details`.

## Retries

Requests are retried up to 3 times with exponential backoff (1s base, 10s cap), only for HTTP 429
(honoring the `Retry-After` header) and network/timeout errors. Other 4xx responses are never
retried. Default request timeout is 30 seconds; override with `Synter(api_key=..., timeout=60.0)`.

## Known issues (preserved, not silently fixed)

This SDK calls the real backend exactly as it behaves in production today, including three known
quirks documented in `packages/sdk-shared/SPEC.md`:

1. `campaigns.pause()` and `campaigns.update_budget()` accept a `platform` argument (schema
   parity) but always dispatch with `platform=google` regardless of what you pass.
2. `upload_image()` accepts an optional `platform` argument but always dispatches with
   `platform=google`.
3. `campaigns.create_display()` uses `landscape_image_url`/`square_image_url` (backend flags
   `--landscape-image`/`--square-image`), while `campaigns.create_pmax()` uses the same-named
   Python parameters but different backend flags (`--landscape-image-url`/`--square-image-url`).
   The Python parameter names are consistent; the wire flags genuinely differ because the two
   backend scripts differ.

If these get fixed upstream, `catalog.json` will be updated first and every language SDK patched
together — this SDK will not "helpfully" diverge on its own.

## Resource namespaces

| Namespace | Tools |
|---|---|
| `client.campaigns` | `list`, `create_search`, `create_display`, `create_pmax`, `pause`, `update_budget` |
| `client.analytics` | `get_performance`, `get_daily_spend` |
| `client.keywords` | `add`, `add_negative` |
| `client.conversions` | `create`, `list`, `diagnose_tracking` |
| `client.creative` | `generate_image`, `generate_video` |
| `client.meta` | `create_campaign` |
| `client.linkedin` | `create_campaign` |
| `client.reddit` | `create_campaign` |
| `client.audiences` | `stage_artifact`, `sync`, `manage` |
| `client.*` (top level) | `list_ad_accounts`, `upload_image`, `list_landing_pages`, `execute` |

## Development

```bash
pip install -e ".[dev]"
pytest
mypy synter
ruff check synter tests
```

No test hits the real network or requires a `SYNTER_API_KEY` — HTTP is mocked with
[`respx`](https://github.com/lundberg/respx).

## Publishing

Nothing in this package is published to PyPI without explicit sign-off. Build/lint/test only.
