Quickstart
https://terrads.com/api/v1Updated September 10, 2026The API drives the same ad-run pipeline the app uses: you describe a run (product, angle, audience, formats, languages, model), the run analyses your brand context, writes copy, renders every format natively, and hands back finished creative. This page walks the shortest path from nothing to a downloaded image. The runs guide covers the interactive loop (questions and copy review) that this page deliberately switches off.
Before you start
- An active subscription on a plan that includes API access. Keys are created in Settings → API keys; each key is shown once, at creation.
- The base URL. It is printed at the top of this page; every example below
reads it from
$BASE.
export BASE="https://<the base URL shown above>"
export TOKEN="trds_live_…" # your key, exactly as shown when you created it
Every request carries the key as a bearer token:
curl -s "$BASE/brands" -H "Authorization: Bearer $TOKEN"
A bad or missing token is a 401 with invalid_api_key. See
authentication for the other refusals a key can meet.
1. Find the ids a run needs
A run is built from catalog rows that already exist in your account: a brand,
one of its products, an angle, optionally an audience, and a model. The
catalog endpoints return ids; angles and audiences belong to a brand, so those
two take ?brandId=.
curl -s "$BASE/brands" -H "Authorization: Bearer $TOKEN"
curl -s "$BASE/products?brandId=$BRAND" -H "Authorization: Bearer $TOKEN"
curl -s "$BASE/angles?brandId=$BRAND" -H "Authorization: Bearer $TOKEN"
curl -s "$BASE/audiences?brandId=$BRAND" -H "Authorization: Bearer $TOKEN"
curl -s "$BASE/models" -H "Authorization: Bearer $TOKEN"
Every catalog response is an object with one key named after the resource
— brands, products, angles, audiences, models — holding the array:
{
"brands": [
{ "id": "b1a2c3d4-0000-4000-8000-000000000001", "name": "Acme", "slug": "acme", "description": null, "createdAt": "…" }
]
}
GET /models lists the models you can render with, each with its slug and
the current credit cost the pricing uses for it. Pick a slug for the body
below. The fields every catalog endpoint returns are listed in the
endpoint reference.
2. Create the run
POST /runs returns as soon as the run is accepted; the work happens in the
background. It requires an Idempotency-Key header — a string you choose,
unique per run you intend to create — so a retried request can never charge
you twice. Read idempotency before you build a retry loop.
curl -s -X POST "$BASE/runs" \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: quickstart-$(uuidgen)" \
-H "Content-Type: application/json" \
--data @run.json
run.json — the ids come from step 1:
{
"brandId": "b1a2c3d4-0000-4000-8000-000000000001",
"config": {
"productAssetId": "b1a2c3d4-0000-4000-8000-000000000002",
"angleId": "b1a2c3d4-0000-4000-8000-000000000003",
"audienceId": "b1a2c3d4-0000-4000-8000-000000000004",
"references": [],
"formatIds": ["feed", "story"],
"languageIds": ["en"],
"concepts": 2,
"modelSlug": "example-image-model",
"copyMode": "auto",
"copyTone": "",
"cta": "",
"creativeDirection": "Bright, summery, product front and centre.",
"smartAsk": false
},
"maxCredits": 40
}
Two settings keep this run non-interactive: smartAsk: false means the run
never pauses to ask you about your references, and copyMode: "auto" means it
writes and uses copy without a review step. maxCredits is optional — it is
the most you are willing to spend on this run; if the current price is higher
you get a 409 quote_changed and nothing is charged. Omit it to accept the
current price.
The response:
{ "runId": "…", "status": "queued", "creditCost": 12 }
creditCost is what the run charged. 201 means it was just created; a
200 with the same shape means this Idempotency-Key already created it and
you are looking at the original.
3. Poll until it settles
curl -s "$BASE/runs/$RUN_ID" -H "Authorization: Bearer $TOKEN"
{
"run": { "id": "…", "status": "generating", "errorMessage": null, "createdAt": "…", "completedAt": null, "creditCost": 12 },
"pendingInput": null,
"events": [ { "seq": 1, "kind": "work", "payload": { "itemId": "…", "role": "brand", "label": "…", "state": "done" } } ]
}
Poll every few seconds until run.status is completed, failed or
canceled. pendingInput stays null for this run; when it is not null the
run is waiting for you — see runs. Pass ?after=<seq> with the
last seq you have seen to receive only new events.
4. Fetch the results
curl -s "$BASE/runs/$RUN_ID/results" -H "Authorization: Bearer $TOKEN"
{
"results": [
{ "id": "g-base", "conceptIndex": 0, "sourceGenerationId": null, "format": null, "width": 1080, "height": 1080, "imageUrl": "https://…", "createdAt": "…" },
{ "id": "g-story", "conceptIndex": null, "sourceGenerationId": "g-base", "format": "story", "width": 1080, "height": 1920, "imageUrl": "https://…", "createdAt": "…" }
],
"expiresInSeconds": 300
}
Each imageUrl is a short-lived signed link to the finished creative — the
composited, delivered image, not an intermediate render. Download within
expiresInSeconds; a fresh call returns fresh links. The first row of each
concept is its base render (conceptIndex set, format null — read the
frame from width and height); the other formats you asked for hang off it
through sourceGenerationId and name their format.
Where next
- Runs and the interactive loop — statuses, events, the questions and copy-review gates, cancel.
- Endpoint reference — every route, parameter and field.
- Errors, rate limits, idempotency.
- Machine-readable: an OpenAPI 3.1 document at
/api/v1/openapi.json, and every page here as plain markdown at/md/docs/<slug>(or sendAccept: text/markdown).