ads_dev
The 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.
pip install ads_dev
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)
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:
curl -fsSL https://ads.dev/login.mjs -o sandstorm-login.mjs && node sandstorm-login.mjs
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.
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:
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:
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
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:
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.
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
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.
Metadata
Release files for ads_dev 1.0.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 | Provenance | |
|---|---|---|---|---|
| ads_dev-1.0.0.tar.gz | 18.6 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ads_dev-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 40.7 kB
Release files / ads_dev-1.0.0.tar.gz
| Download URL | ads_dev-1.0.0.tar.gz |
|---|---|
| Size | 18.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ab3363801045007246809205aaee0e9d6473ffb4c6e642d5a3f30c544504ca6c
|
|
BLAKE2b-256 checksum How to use checksums |
845ae155970f6e822efabd233b6d408c7f41c8fa026954c820a823c4333161ee
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / ads_dev-1.0.0-py3-none-any.whl
| Download URL | ads_dev-1.0.0-py3-none-any.whl |
|---|---|
| Size | 22.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e4dc3135b9302bac9ef33c3aec790ef979c042aadf03f356461a48c9590521b6
|
|
BLAKE2b-256 checksum How to use checksums |
12fd50cc3f4b4d9b53185745666e37f8ea16524c9c1056681c05ba21ab463e38
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for ads_dev 1.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.