Metadata-Version: 2.5
Name: synter
Version: 0.2.0
Summary: Official Synter Python SDK for the Synter cross-platform advertising API
Project-URL: Homepage, https://synterai.com
Project-URL: Documentation, https://docs.synterai.com/sdk/python
Project-URL: Repository, https://github.com/Synter-Media-AI/synter-docs/tree/main/sdk/python
Project-URL: Issues, https://github.com/Synter-Media-AI/synter-docs/issues
Project-URL: Developer Portal, https://synterai.com/developer
Project-URL: Support, https://synterai.com/contact
Author-email: Synter <hello@synterai.com>
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://synterai.com) cross-platform advertising API — manage
ad campaigns across 16 ad platforms (Google Ads, Meta, LinkedIn, Microsoft, Reddit, TikTok, X, Amazon DSP, Amazon Ads, Pinterest, Snap, Spotify, OpenAI Ads, The Trade Desk, DV360, StackAdapt) 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://synterai.com/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).

## Links and provenance

Official Synter package. The only official packages are @synterai/mcp-server, @synterai/sdk-js, PyPI synter, crates.io synter, Maven ai.syntermedia:synter-sdk. @iflow-mcp/* copies are unofficial and outdated.

- Website: https://synterai.com
- Docs: https://docs.synterai.com/sdk/python
- Public repository (docs and README): https://github.com/Synter-Media-AI/synter-docs/tree/main/sdk/python
- Issues: https://github.com/Synter-Media-AI/synter-docs/issues
- Registry: PyPI: [`synter`](https://pypi.org/project/synter/)
- Contact: hello@synterai.com

## 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 [synterai.com/developer](https://synterai.com/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")
```

### Write: pause a campaign and change its daily budget

```python
client.campaigns.pause(campaign_id="1234567890", platform="google")
result = client.campaigns.update_budget(
    campaign_id="1234567890", platform="google", daily_budget=75.0
)
if result.get("status") == "pending_review":
    print("Held for approval:", result["audit_id"])
```

Pausing and budget changes run immediately under the default workspace policy
and are audited. Writes the safety gate holds (enable/resume, delete, creates,
creative sends under the default policy) come back as HTTP 202 with
`status: "pending_review"`, an `audit_id` and the `reviewed_args`; nothing has run.
A policy block comes back as 403 (`status: "blocked"`). An API key cannot approve
its own held write; see https://docs.synterai.com/mcp/approval-model. Pause and
budget calls reach Google Ads only (the `platform` argument is not forwarded);
use `PATCH https://api.synterai.com/v1/campaigns/{id}/budget` with a `platform`
for other networks.

### 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 16 ad 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.
