ads.dev
v1.1.0
Published
The ads.dev API for Node, Deno, Bun and edge runtimes: image, video and audio generation on your brand, the brand kit and library, and competitor ads
Maintainers
Readme
ads.dev
The ads.dev API for Node, Deno, Bun and edge runtimes: image, video and audio generation with 34 models on your brand, the brand kit and library, and the ads your competitors run. Every model's input is typed, so the model you name decides what input takes.
It is published as ads.dev, adsdotdev, sandstormapp and @sandstormapp/sdk, the same package under four names, and needs nothing at runtime but fetch and Web Crypto.
npm install ads.devimport { Sandstorm } from 'ads.dev'
const sandstorm = new Sandstorm()
const generation = await sandstorm.generations.run('nano-banana-pro', {
prompt: 'A ceramic mug on a sunlit bench, steam rising',
aspect_ratio: '4:5',
})
console.log(generation.output[0]?.url)A key
new 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.mjsnew Sandstorm({
apiKey: process.env.SANDSTORM_API_KEY,
brand: '6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f',
baseUrl: 'https://ads.dev',
maxRetries: 2,
timeoutMs: 60_000,
})A key acts on the brand it was made on; brand names another brand of the same organization. baseUrl defaults to SANDSTORM_API_URL, then https://ads.dev. Keep the key on your server: anyone who holds it spends your credits.
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 throws GenerationFailed, which carries it.
const still = await sandstorm.generations.run('nano-banana-2-lite', { prompt: 'Two lemons in a blue bowl' })
const clip = await 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.
For work that runs long, queue it and come back to it, or take a webhook:
const queued = await sandstorm.generations.create('kling-3-pro/image-to-video', input, { webhookUrl: 'https://yourapp.com/hooks/sandstorm' })
const finished = await sandstorm.generations.wait(queued.id)
await sandstorm.generations.get(queued.id)
await 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:
await sandstorm.generations.create('nano-banana-pro', input, { idempotencyKey: 'campaign-42-hero' })Lists
Every list answers one page when awaited and walks every page when iterated:
const page = await sandstorm.generations.list({ status: 'succeeded', limit: 50 })
for await (const asset of sandstorm.assets.list({ type: 'image' })) console.log(asset.id, asset.url)Everything else
await sandstorm.models.list({ type: 'video' })
await sandstorm.models.get('nano-banana-pro/edit')
await sandstorm.brand.get()
await sandstorm.assets.import('https://yourcdn.com/hero.png', { name: 'Hero shot' })
await sandstorm.assets.get('asset_9c1e77aa')
await sandstorm.offers.list({ query: 'cold brew' })
await sandstorm.offers.get('offer_0f8e2b6a4c1d4e7f9a3b5c7d9e1f2a4b')
await sandstorm.characters.list({ kind: 'subject' })
await sandstorm.voices.list({ use_case: 'advertisement', limit: 5 })
const [advertiser] = (await sandstorm.advertisers.list({ query: 'Liquid Death' })).data
await sandstorm.ads.list({ advertiser: advertiser!.id, status: 'active' })
await sandstorm.ads.get('ad_5e1c9a3b')Webhooks
A generation made with webhookUrl is POSTed to it once it finishes, signed in Sandstorm-Signature. Verify the raw body before you trust it:
import { verifyWebhook } from 'ads.dev'
const secret = await sandstorm.webhooks.secret()
const event = await verifyWebhook({ secret, body: await request.text(), signature: request.headers.get('sandstorm-signature') })
if (event.type === 'generation.succeeded') console.log(event.data.output)It refuses 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 throws SandstormError with the HTTP status, its type and code, the param at fault and the requestId 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.
import { SandstormError } from 'ads.dev'
try {
await sandstorm.generations.create('nano-banana-pro', { prompt: 'A mug on a bench' })
} catch (error) {
if (error instanceof SandstormError && error.code === 'insufficient_credits') console.log('Add credits in Sandstorm')
}Developing
openapi.json is the API's own document, held to what the API serves by the agent project's tests; yarn generate turns it into src/openapi.ts, and yarn ci:test fails when the two disagree. yarn release publishes the build as ads.dev, adsdotdev, sandstormapp and @sandstormapp/sdk.
