Metadata-Version: 2.4
Name: ads_dev
Version: 1.0.0
Summary: The ads.dev API for Python: image, video and audio generation on your brand, the brand kit and library, and competitor ads
Keywords: ads.dev,sandstorm,ads,image generation,video generation,ai,api
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Typing :: Typed
Requires-Dist: anyio>=4.15.1
Requires-Dist: httpx>=0.28.1
Requires-Dist: pydantic>=2.14.1
Requires-Python: >=3.10
Project-URL: Homepage, https://ads.dev
Project-URL: Repository, https://github.com/Sandstorm-App/Sandstorm
Description-Content-Type: text/markdown

# ads_dev

The [ads.dev](https://ads.dev) API for Python: image, video and audio generation with 34 models on your brand, the brand kit and library, and the ads your competitors run. It has a sync client and an async one, and every answer is a typed pydantic model.

```bash
pip install ads_dev
```

```python
from ads_dev import Sandstorm

sandstorm = Sandstorm()

generation = sandstorm.generations.run("nano-banana-pro", {"prompt": "A ceramic mug on a sunlit bench, steam rising", "aspect_ratio": "4:5"})
print(generation.output[0].url)
```

```python
from ads_dev import AsyncSandstorm

async with AsyncSandstorm() as sandstorm:
    generation = await sandstorm.generations.run("nano-banana-2-lite", {"prompt": "Two lemons in a blue bowl"})
```

## A key

`Sandstorm()` reads `SANDSTORM_API_KEY`. Make a key in Sandstorm under Settings → API keys, or in one command, which signs you in in the browser and writes the key to `.env`:

```bash
curl -fsSL https://ads.dev/login.mjs -o sandstorm-login.mjs && node sandstorm-login.mjs
```

```python
Sandstorm(api_key="sk_live_…", brand="6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f", base_url="https://ads.dev", max_retries=2, timeout=60)
```

A key acts on the brand it was made on; `brand` names another brand of the same organization. `base_url` defaults to `SANDSTORM_API_URL`, then `https://ads.dev`. Pass `http=` an `httpx.Client` (or `httpx.AsyncClient`) of your own to set proxies or limits. Use the client as a context manager, or call `close()`, to release its connections.

## Generations

`run` makes a generation and answers it once it has finished, holding each request on the API with `Prefer: wait` rather than polling. A generation that fails or is canceled raises `GenerationFailed`, which carries it.

```python
still = sandstorm.generations.run("nano-banana-2-lite", {"prompt": "Two lemons in a blue bowl"})

clip = sandstorm.generations.run(
    "seedance-2.5/image-to-video",
    {"start_image": still.output[0].id, "prompt": "Slow push in as the light warms", "resolution": "720p"},
)
```

Every media input takes an asset id (`asset_…`) or a public https URL, so the output of one generation goes straight into the next. `ads_dev.types` has a model for every model's input, such as `CreateNanoBananaProInput`, if you want an input checked before it is sent.

For work that runs long, queue it and come back to it, or take a webhook:

```python
queued = sandstorm.generations.create("kling-3-pro/image-to-video", input, webhook_url="https://yourapp.com/hooks/sandstorm")
finished = sandstorm.generations.wait(queued.id)

sandstorm.generations.get(queued.id)
sandstorm.generations.cancel(queued.id)
```

A generation is `queued`, then `running`, then `succeeded`, `failed` or `canceled`; `credits` is what it was charged once it finished, and a failed one is free. Cancelling a queued generation costs nothing; one the model has already started stops where the model allows it, and otherwise finishes and is charged.

`create` sends an `Idempotency-Key` with every call and keeps it across its retries, so a retry never makes a second generation. Pass your own to make a call safe across process restarts: `create(..., idempotency_key="campaign-42-hero")`.

## Lists

A list walks every page when iterated, and `first_page()` answers one:

```python
for asset in sandstorm.assets.list(type="image"):
    print(asset.id, asset.url)

page = sandstorm.generations.list(status="succeeded", limit=50).first_page()
```

With the async client, iterate with `async for` and `await` the first page.

## Everything else

```python
sandstorm.models.list(type="video")
sandstorm.models.get("nano-banana-pro/edit")

sandstorm.brand.get()
sandstorm.assets.import_url("https://yourcdn.com/hero.png", name="Hero shot")
sandstorm.assets.get("asset_9c1e77aa")
sandstorm.offers.list(query="cold brew")
sandstorm.offers.get("offer_0f8e2b6a4c1d4e7f9a3b5c7d9e1f2a4b")
sandstorm.characters.list(kind="subject")
sandstorm.voices.list(use_case="advertisement", limit=5)

advertiser = next(iter(sandstorm.advertisers.list(query="Liquid Death")))
sandstorm.ads.list(advertiser=advertiser.id, status="active")
sandstorm.ads.get("ad_5e1c9a3b")
```

## Webhooks

A generation made with `webhook_url` is POSTed to it once it finishes, signed in `Sandstorm-Signature`. Verify the raw body before you trust it:

```python
from ads_dev import verify_webhook

secret = sandstorm.webhooks.secret()

event = verify_webhook(secret, request.body, request.headers.get("sandstorm-signature"))
if event.type == "generation.succeeded":
    print(event.data.output)
```

It raises `ValueError` for a body that was changed, a signature from another secret and a delivery older than five minutes. An event's `id` is the same on every retry of it, so a handler can skip one it has seen.

## Errors

A refusal raises `SandstormError` with the HTTP `status`, its `type` and `code`, the `param` at fault and the `request_id` to quote to support. Requests Sandstorm could not take (408, 429, 500, 502, 503, 504) and network failures are retried twice with backoff, after the `Retry-After` the API asks for.

```python
from ads_dev import SandstormError

try:
    sandstorm.generations.create("nano-banana-pro", {"prompt": "A mug on a bench"})
except SandstormError as error:
    if error.code == "insufficient_credits":
        print("Add credits in Sandstorm")
```

## Developing

```bash
uv sync
uv run pytest
uv run python scripts/generate.py
uv build
```

`ads_dev/types.py` is generated from `../sdk/openapi.json`, the API's own document, and `ads_dev/_sync/client.py` from the async client with `unasync`; `scripts/generate.py` writes both, and the tests fail when either is stale. Build with uv 0.13 or newer.
