Metadata-Version: 2.5
Name: synter
Version: 0.1.2
Summary: Python SDK for the Synter cross-platform advertising API
Project-URL: Homepage, https://syntermedia.ai
Project-URL: Documentation, https://docs.syntermedia.ai
Project-URL: Developer Portal, https://syntermedia.ai/developer
Project-URL: Support, https://syntermedia.ai/contact
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
ad campaigns across 27 ad platforms (Google Ads, Meta, LinkedIn, Microsoft, Reddit, TikTok, X, Amazon DSP, Amazon Ads, Pinterest, Snap, Spotify, OpenAI Ads, The Trade Desk, FreeWheel, DV360, CM360, Walmart Connect, Instacart Ads, Target Roundel, Criteo) from one client.

> **`0.1.2` — live on PyPI.** `pip install synter`. Pre-1.0, so the surface may change before a
> stable `1.0`; pin a version in production. `0.1.1`+ fixes `analytics.get_performance(platform="google" | "linkedin")`,
> which in `0.1.0` sent the script *filename* (`pull_google_ads_data`) instead of the dispatched script
> name (`pull_google_ads`); the backend rejected it with `400 UNKNOWN_TOOL` until it gained an alias on 2026-08-27 (synter-media PR #9684). `0.1.2` syncs `synter.__version__` with the release.

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, Devin, ChatGPT).

## Status

- **Sync client (`Synter`)**: fully implemented, all 25 tools.
- **Async client (`AsyncSynter`)**: fully implemented, mirrors the sync client's entire surface.
- Live on PyPI as `synter`.

## Installation

```bash
pip install synter
```

Requires Python 3.9+.

> **Keep your API key server-side.** The key is a secret that can spend money and modify ad
> accounts. Use this SDK from a backend, never ship it in client-side code.

## 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. Keys issued from a sandbox workspace use the `syn_test_` prefix
instead and only work against that sandbox — no live ad-account risk.

## 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 27 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

`synter` is published on PyPI: https://pypi.org/project/synter/

Releases are cut deliberately, not on every merge. A release must have a version
bump in `pyproject.toml` AND `synter/client.py` (`__version__` — the two are
asserted equal), a green `pytest` / `ruff` / `mypy`, and explicit sign-off.

`tests/test_flag_parity.py` is the gate that matters before any release: it
drives every dispatch path and diffs the emitted CLI flags against
`apps/ppc-backend/script_catalog.json`. 0.1.0 shipped with `get_performance`
sending a script FILENAME instead of the script NAME, so google and linkedin
returned 400 UNKNOWN_TOOL on every call, and five more tools sent flags their
script does not declare. That test catches the whole class.
