AdGen API — AI Ad Campaign Generation API
Brief in → ad copy, branded images and a rendered campaign video out. One REST API, English and Arabic, with a free tier and an instant API key.
Base URL: https://adgen-api.sikasio.com
You describe the brand and the goal. AdGen plans the campaign — a headline, body, caption, hashtags and an image prompt per post — then generates the branded images and renders them into a short vertical ad video with an AI-written script and optional narration. Copy, images, narration and pacing are produced by AdGen AI. Planning is synchronous; images and video renders are jobs you poll.
Quick facts
- Base URL
https://adgen-api.sikasio.com- Flow
POST /v1/campaigns→…/images→…/video→ pollGET /v1/jobs/:id- Auth
X-API-Keyheader — instant free key at /signup- Image models
flash·studio·vivid- Voices
aria·nova·sami·omar·mixed(EN + Arabic)- Output
- JSON campaign plan, hosted image URLs, MP4 video + thumbnail on the AdGen CDN
- Credits
- campaign plan 2 · image 2 · hook options free · video 12 (basic) / 20 (creative)
- Pricing
- Free $0 (40 credits/mo) · Starter $19/mo (400) · Pro $49/mo (1,200)
- Provider
- Sikasio · support@sikasio.com
Getting your API key
New here? You can be making your first request in under a minute:
- Create a free account at /signup — name, email and a password, and accept the terms. No card required.
- Verify your email by entering the 6-digit code we send you.
- Copy your API key — it's shown once, right after verification, so save it somewhere safe. You can regenerate a fresh key any time from your dashboard.
- Send the key as the
X-API-Keyheader (orAuthorization: Bearer) on every/v1request. - Make your first call:
POST /v1/campaignswith a brief — see the quick start below.
Already have an account? Sign in. Forgot your password? Reset it here.
Authentication
Every /v1 request must carry your API key, either way:
X-API-Key: adg_live_YOUR_KEY
Authorization: Bearer adg_live_YOUR_KEY
A missing or unknown key returns 401 {"error":"Invalid API key"}. Keys are scoped to
their own data: a campaign or job belonging to another key returns 404, never
403.
GET /healthz is the one endpoint that needs no API key — and it costs no credits.
Everything else under /v1 requires a key. Request bodies are capped at 1 MB, and every
response body is JSON.
CORS: enabled (Access-Control-Allow-Origin: *) — auth is a header, not
a cookie, so you can call the API and embed the returned asset URLs directly from browser apps.
Plans & pricing
Every plan sets a monthly credit allowance, a requests-per-minute ceiling, and how long finished assets stay hosted:
| Plan | Price | Credits / month | Requests / min | Asset retention |
|---|---|---|---|---|
free | $0 | 40 | 5 | 7 days |
starter | $19 | 400 | 15 | 30 days |
pro | $49 | 1,200 | 30 | 90 days |
Paid plans are billed monthly. Allowances reset at the first instant of each UTC
calendar month (resets_at on GET /v1/account) and unused
credits do not roll over. Upgrade or cancel any time from your dashboard.
Finished assets are served from stable URLs on the AdGen CDN and may be removed after the retention window for your plan — download anything you need to keep.
Credits
| Action | Endpoint | Credits |
|---|---|---|
| Campaign plan (copy) | POST /v1/campaigns | 2 |
| Image (each) | POST /v1/campaigns/:id/images | 2 |
| Hook options | POST /v1/campaigns/:id/hooks | 0 (free) |
| Video render — basic | POST /v1/campaigns/:id/video · POST /v1/videos | 12 |
| Video render — creative | POST /v1/campaigns/:id/video · POST /v1/videos | 20 |
Charged on success. A campaign plan is reserved when the request starts and refunded
in full if copy generation fails (a failed plan creates no campaign and costs nothing). An image batch
reserves 2 × selected posts and refunds every image that fails; if none succeeds, the whole batch is
refunded. A video render is priced and checked when it is queued — so you get a 402
immediately if you cannot afford it — and charged only when it finishes done. A failed or
cancelled render costs nothing.
Holds: why a 402 can arrive with credits left
The price of every render still queued or running is held against your balance from the
moment it is queued, so queueing renders back-to-back cannot walk past your allowance — the request
that no longer fits is refused with a 402 up front. Holds are not spend yet, so
used_this_month and remaining on
GET /v1/account exclude them, while reserved reports
the held total and available is your allowance after spend and holds —
what you can actually queue right now. The remaining in a 402 is that same
after-holds figure.
Holds gate every paid action, not just the next render: a campaign plan
(2) or an image batch (2 × posts) is checked against
available too, so an outstanding render can push a plan or an image request to a
402 while remaining still looks sufficient. Wait for the render to finish
(its hold becomes the charge) or cancel it (the hold is released immediately).
402 {"error":"Insufficient credits","required":12,"remaining":4}
Quick start
1. Plan the campaign
curl -X POST "https://adgen-api.sikasio.com/v1/campaigns" \
-H "X-API-Key: adg_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"brand": { "name": "Acme Coffee", "colors": ["#1e293b"] },
"goal": "Launch our cold brew subscription",
"audience": "office workers in Cairo",
"tone": "friendly, confident",
"cta": "Start your subscription",
"language": "en",
"postCount": 4
}'
{ "id": "9f3c1a77b2e40d51", "status": "ready",
"posts": [ { "id": 1, "order": 0, "headline": "…", "body": "…", "caption": "…",
"hashtags": ["…"], "imagePrompt": "…", "imageStatus": "planned", "imageUrl": null } ] }
2. Generate the images
curl -X POST "https://adgen-api.sikasio.com/v1/campaigns/9f3c1a77b2e40d51/images" \
-H "X-API-Key: adg_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "model": "studio" }'
{ "jobId": "4a7d2f0e91bc38da" }
Poll the job at your plan's cadence until it's done, then read the
image URLs back from GET /v1/campaigns/9f3c1a77b2e40d51.
3. Pick an opening hook (free)
curl -X POST "https://adgen-api.sikasio.com/v1/campaigns/9f3c1a77b2e40d51/hooks" \
-H "X-API-Key: adg_live_YOUR_KEY" -H "Content-Type: application/json" -d '{}'
{ "options": [ { "style": "question", "lang": "en",
"headline": "Ready for\nthe next level?", "underline": "next level",
"kicker": "Acme Coffee" } ], "cached": false }
4. Render the video
curl -X POST "https://adgen-api.sikasio.com/v1/campaigns/9f3c1a77b2e40d51/video" \
-H "X-API-Key: adg_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "creative",
"aspect": "portrait",
"hook": { "headline": "Ready for\nthe next level?", "underline": "next level" },
"voiceover": { "enabled": true, "voice": "aria" }
}'
{ "jobId": "1c9be05af7d2461a" }
5. Poll the job
curl "https://adgen-api.sikasio.com/v1/jobs/1c9be05af7d2461a" \
-H "X-API-Key: adg_live_YOUR_KEY"
{ "id": "1c9be05af7d2461a", "status": "done", "progress": 100,
"videoUrl": "https://adgen-cdn.sikasio.com/…mp4",
"thumbnailUrl": "https://adgen-cdn.sikasio.com/…jpg" }
Jobs & polling
Image batches and video renders are asynchronous: they answer 202 {"jobId":"…"} and you
poll GET /v1/jobs/:id.
| Status | Meaning |
|---|---|
queued | accepted, waiting for a worker |
rendering | in progress — progress 0–100, stage is a human-readable step |
done | finished successfully |
failed | finished with an error (see error) |
cancelled | cancelled by you |
Finished assets (videoUrl, thumbnailUrl and each post's
imageUrl) are stable URLs served from the AdGen CDN.
Rather than polling a video render to the end, you can have AdGen POST a signed event to your server when it finishes — see webhooks. Polling still works, and remains the authority: webhook delivery is best-effort.
Polling cadence
Every poll spends from your plan's rate limit, so pick an interval your ceiling can sustain for the whole job:
| Plan | Suggested interval | Ceiling |
|---|---|---|
free | every 15 seconds | 5 rpm |
starter | every 10 seconds | 15 rpm |
pro | every 5 seconds | 30 rpm |
Nothing is lost by polling slowly: a video render takes a few minutes and an image batch runs about
30–60 seconds per image, so a 15-second interval still reports a finished job within a few percent of its
total runtime. Leave headroom — the ceiling covers all /v1 calls, so a loop that
also reads GET /v1/campaigns/:id between polls needs a slower interval than the table.
If you do get a 429, wait the number of seconds in the Retry-After header
and carry on — a rate-limited poll never affects the job, which keeps running.
Create a campaign
Turns a brief into a campaign with planned posts. Synchronous — the response carries
the finished posts and typically takes 5–20 seconds (it scales with
postCount), so set your client timeout to at least 60s. A failure means no campaign was
created and no credits were spent.
| Field | Type | Notes |
|---|---|---|
brand.name required | string | ≤ 60 chars. |
brand.colors | string[] | ≤ 5 hex colors, e.g. "#1e293b". |
brand.logoUrl | string | Public http(s) URL. |
goal required | string | ≤ 500 chars. |
audience | string | ≤ 300 chars. |
tone | string | ≤ 200 chars. |
cta | string | ≤ 120 chars — the action the video's CTA scene is scripted around. |
language | string | en (default) · ar. "AR", "ar-EG", "ar_SA" all mean ar; anything unrecognized falls back to en (never an error). |
market | string | See markets. Default/auto → eg for Arabic, global for English. An unknown code is a 400. |
style | string | See styles. Default realistic. |
aspect | string | portrait (default) · landscape · square. |
postCount | integer | 1–8, default 4. |
content.link | string | Public http(s) URL, used as the CTA destination. |
content.includeHowTo | boolean | Ask for how-to style posts. |
content.edge | string | ≤ 300 chars — your differentiator, woven into the copy. |
consistentIdentity | boolean | Also generate one shared art direction so every image looks like one set. |
content.includeEdge | boolean | Ask for a "why we beat the alternatives" post. Implied when content.edge is set; set it explicitly to get the post without naming an angle. |
context | string | ≤ 4000 chars. What the product actually is — description, positioning, key facts. Without it the copywriter has only the brand name and the copy turns generic. |
imageInstructions | string | ≤ 500 chars, applied to every image of this campaign. |
Returns 201:
{
"id": "9f3c1a77b2e40d51",
"status": "ready",
"artDirection": "…",
"posts": [
{ "id": 1, "order": 0, "headline": "…", "body": "…", "caption": "…",
"hashtags": ["…"], "imagePrompt": "…", "imageStatus": "planned", "imageUrl": null }
]
}
artDirection appears only when consistentIdentity was requested and an art
direction was produced. imageStatus is one of planned ·
generating · done · failed.
Errors: 400 validation
({"error":"brand.name is required"}), 402 insufficient credits,
502 {"error":"Copy generation failed. Please retry."} — nothing charged, safe to retry.
Read a campaign
Reads a campaign back, including the current image state of every post — this is where finished image URLs appear after an image batch completes.
{
"id": "9f3c1a77b2e40d51",
"status": "ready",
"artDirection": "…",
"brief": { "brand": { "name": "Acme", "colors": ["#1e293b"], "logoUrl": null },
"goal": "…", "audience": "…", "tone": "…", "cta": "…",
"language": "en", "market": "global", "style": "realistic", "aspect": "portrait" },
"posts": [ { "id": 1, "order": 0, "imageStatus": "done",
"imageUrl": "https://adgen-cdn.sikasio.com/…", "…": "…" } ]
}
artDirection and brief.cta are present only when they were set;
posts[] has the same shape as in the create response. An unknown id — or one owned by
another key — is 404 {"error":"Campaign not found"}.
Opening-hook options
Scroll-stopping opening-hook options for the campaign's video, written from its own copy in the
market's language. Costs 0 credits (but still needs your API key, like every
/v1 endpoint), and cached for 24 hours per market.
| Field | Type | Notes |
|---|---|---|
market | string | Default/""/auto = the campaign's own market. Unknown code → 400. |
force | boolean | Ignore the cache and regenerate. |
{ "options": [ { "style": "question", "lang": "en",
"headline": "Ready for\nthe next level?",
"underline": "next level", "kicker": "Acme" } ],
"cached": false }
Ten options come back in the normal case — two variants each of five archetypes:
question, bold, pain, stat,
curiosity. headline may contain a single \n (a two-line hook);
underline and kicker are optional. Any usable set of 4 or more options is
cached for 24h, so a short set can be served with cached: true for the rest of the day —
pass force: true to regenerate. If copy generation fails outright, a 2-option built-in set
is returned and is not cached, so the next call retries.
Pass a chosen option straight into POST /v1/campaigns/:id/video as
hook: { headline, underline?, kicker? }.
Generate campaign images
Queues image generation for the campaign's posts. Returns immediately; poll the job, then read the
URLs from GET /v1/campaigns/:id.
| Field | Type | Notes |
|---|---|---|
model | string | flash is fastest, studio is the balanced default, vivid is the most stylized. |
postIds | integer[] | Post ids from the campaign. Default: every post whose imageStatus is not done — so posts never generated and posts whose last attempt failed. Ids not belonging to this campaign → 400. |
referenceImageUrl | string | Public http(s) URL of a brand reference (logo, product shot). |
referenceImageUrl caveat (current behavior)
The URL is not sent to the image model. Supplying it only adds an instruction to integrate your brand's logo/visual identity tastefully into the composition — the image is not visually conditioned on that file. Treat it as a hint, not a reference image. (Real image conditioning is planned.)
202 { "jobId": "4a7d2f0e91bc38da" }
Credits: 2 × selected posts, reserved now and refunded per failed image. Images are generated one
after another — expect roughly 30–60 seconds per image, so a 4-post batch runs 2–4
minutes. Per-post progress shows up as the job's stage ("image 2/4"). Partial
success is a done job: the posts that landed have imageStatus: "done" with a
CDN imageUrl, the rest are failed and refunded.
One batch per campaign at a time. While a campaign's batch is
queued or rendering, a second request for the same campaign is refused with
409 and nothing is charged — poll the job, then retry. A batch that ended
done, failed or cancelled never blocks a retry.
Re-generating an image is allowed at any time — pass the
postIds you want redone, or send no postIds to redo everything not yet
done (a post whose last attempt failed is included, so a plain retry needs no
hand-picked ids). If a retry fails, the image you already had is kept: the post reads
imageStatus: "failed" while imageUrl still points at the last image that
succeeded, so a failed retry never costs you a working asset. A post that never had one keeps
imageUrl: null. A successful retry replaces the URL.
Errors: 400 unknown model, bad referenceImageUrl,
{"error":"postIds must be an array of integers"},
{"error":"Unknown postId 7"},
{"error":"No posts selected — every post already has an image"}; 402
insufficient credits; 404 unknown campaign;
409 {"error":"An image batch is already running for this campaign"}.
Use your own images
Attaches images you already have to the campaign's posts — from your own pipeline, a designer, or an
earlier campaign. Free (no model runs), and it makes the campaign renderable without
buying images here: POST /v1/campaigns/:id/video reads exactly these
URLs.
| Field | Type | Notes |
|---|---|---|
images | object[] | Required, 1–8 entries. |
images[].postId | integer | A post id from this campaign. Give this or order, never both. |
images[].order | integer | The post's order (0-based) instead of its id. |
images[].url | string | Required. Public http(s) URL the renderer can fetch. |
PUT /v1/campaigns/4a7d.../images
{ "images": [ { "order": 0, "url": "https://cdn.example.com/a.png" },
{ "order": 1, "url": "https://cdn.example.com/b.png" } ] }
200 { "posts": [ { "id": 1, "order": 0, "imageStatus": "done", "imageUrl": "https://cdn.example.com/a.png", ... } ] }
Only the posts you list change; the rest keep whatever they had. Attaching over an existing image replaces it. The whole request is all-or-nothing — one bad entry rejects the request and writes nothing, so a campaign is never left half-updated.
Errors: 400
{"error":"images must be a non-empty array"},
{"error":"Every images entry needs exactly one of postId or order"},
{"error":"Unknown postId 7"},
{"error":"Every images entry needs a public http(s) url"};
404 unknown campaign;
409 {"error":"An image batch is already running for this campaign"} — a running batch owns
these posts, so wait for it to finish.
Render the campaign video
Renders the campaign into a short vertical ad video: the generated post images become the visuals, AdGen AI writes the script, picks the color system and (when enabled) the narration pace. Requires at least one post with a generated image — up to 6 are used, in order.
| Field | Type | Notes |
|---|---|---|
mode | string | basic (default) is a clean image sequence; creative adds an AI-written kinetic script (hook, beats, stats, CTA) over designed templates. |
aspect | string | portrait (default) · landscape · square. |
market | string | Default/auto = the campaign's market. Drives script copy and narration delivery. |
brandName | string | ≤ 60 chars — per-render override of the displayed brand/project name. |
watermark | object | { enabled: true, position?, size?, opacity?, text?, logoUrl?, showName? } — a persistent brand mark (logo and/or text) over every frame and the thumbnail. position: tl · tr · bl · br (default) · center; size: small · medium (default) · large; opacity 0.15–1 (default 0.7); text ≤ 40 chars, defaults to the brand name; logoUrl defaults to the campaign's brand logo. |
hook | object | { headline, underline?, kicker? } — a chosen option from /hooks; overrides the AI's own opening hook. |
voiceover | object | See below; omitted or enabled: false = silent video. |
transition | string | smooth (default) · simple · none · cinematic · push. |
sfx.enabled | boolean | Default true — sound-effect layer. |
captions | boolean | Default true — on-screen scene copy. |
subtitles | boolean | Default true — burned-in narration subtitles. |
arFont | string | cairo (default) · tajawal · almarai · changa · messiri · amiri · lalezar — Arabic renders only. |
videoHook | boolean | Default false — open on matching stock footage instead of an AI-generated hero image. |
The voiceover object
| Field | Type | Notes |
|---|---|---|
enabled | boolean | Required to get narration. |
voice | string | aria · nova (female) · sami · omar (male) · mixed alternates voices across scenes. |
gender | string | male · female · mixed — only consulted when voice is omitted; default female. |
language | string | en · ar. Default: the selected market's language. |
model | string | standard (default) · premium. |
rate | number | 0.9 · 1 · 1.1 · 1.25 · 1.5. Omit for auto — AdGen AI picks the pace from the campaign's energy, language and the actual narration volume. |
Arabic narration is Modern Standard Arabic (فصحى), delivered with the selected market's accent and cultural register.
202 { "jobId": "1c9be05af7d2461a" }
Credits: 12 (basic) / 20 (creative), charged when the job reaches
done.
Errors: 400 {"error":"Campaign has no generated images yet"},
400 unknown market, 402 insufficient credits, 404 unknown
campaign.
Render a video directly
Render a video from raw scenes and/or a script without a stored campaign — you supply the images and copy. Same engine, same job semantics and same prices.
| Field | Type | Notes |
|---|---|---|
brand.name required | string | ≤ 60 chars. |
brand.color | string | CSS color, ≤ 32 chars, default "#111827". |
brand.logoUrl | string | Public http(s) URL. |
brand.domain | string | ≤ 100 chars, shown on the CTA scene. |
scenes | object[] | Up to 6 — { imageUrl (required, public http(s)), headline ≤120, caption ≤140 }. |
script | object[] | Up to 12 designed scenes (see below). |
mode | string | basic (default) · creative, which requires a non-empty script[]. |
hook | object | { headline ≤120 (default: brand name), subtext ≤90 (default "See what we made"), bgPrompt ≤500 } — bgPrompt describes the generated hero background. |
hookVideoQuery | string | ≤ 120 chars, stock-footage search phrase for the opener. |
videoHook | boolean | Default false — use stock footage for the opener. |
lang | string | en (default) · ar. |
market | string | ≤ 20 chars, informational hint passed to the renderer. |
palette | object | Hex-only color map, e.g. { "bg": "#0b1020", "accent": "#4f46e5" }; non-hex entries are dropped. |
sceneStyles | string[] | Up to 12 style hints (≤ 40 chars each) rotated across scenes. |
aspect, transition, sfx.enabled, captions, subtitles, arFont | — | Same values and defaults as the campaign video above. |
voiceover | object | Same shape as above, with two direct-mode differences: language defaults to lang (there is no market to follow) and rate defaults to 1 (no auto pacing — there is no campaign whose energy could be read). |
You must provide scenes[], script[], or both.
script[] entries
| Type | Fields (max length) |
|---|---|
hook | headline (120), underline (60), kicker (40) |
beat | title (120), sub (160) |
stat | value (40), label (120) |
showcase | caption (140), sub (160), imageUrl (public http(s)) |
cta | text (120), sub (160), url (200) |
An unknown type is a 400 (never silently dropped), and any
imageUrl must be a public http(s) URL.
202 { "jobId": "1c9be05af7d2461a" }
Errors: 400 {"error":"brand.name is required"},
400 {"error":"Provide scenes[] or script[]"},
400 {"error":"mode 'creative' requires script[]"},
400 {"error":"Every scene needs a public http(s) imageUrl"},
400 {"error":"Unknown scene type '…'"}, 402 insufficient credits.
Retrieve a job
{
"id": "1c9be05af7d2461a",
"status": "rendering",
"progress": 62,
"stage": "encoding",
"videoUrl": null,
"thumbnailUrl": null,
"error": null,
"createdAt": 1785000000000,
"finishedAt": null
}
createdAt / finishedAt are epoch milliseconds. videoUrl and
thumbnailUrl are CDN URLs, filled in when status is done. For
image batches the job carries progress only — the images themselves are read from
GET /v1/campaigns/:id. An unknown id, or one owned by another key,
is 404 {"error":"Job not found"}.
A video render typically takes a few minutes: choose your interval from the
polling cadence table and honor Retry-After on a 429.
Cancel a job
Cancel a job — before it starts or mid-render.
200 {"status":"cancelled"} // stopped immediately (was still queued)
200 {"status":"cancelling"} // in progress; stops at its next checkpoint
409 {"error":"Job already finished"} if it already reached a terminal status;
404 for an unknown id. Cancelled work is refunded — image batches per undelivered image,
and a cancelled render was never charged.
Check your account
Your plan, this month's spend, and the live price list.
{
"name": "acme-production",
"plan": "starter",
"monthly_credits": 400,
"used_this_month": 6,
"remaining": 394,
"reserved": 12,
"available": 382,
"resets_at": "2026-08-01T00:00:00.000Z",
"costs": { "plan": 2, "image": 2, "hooks": 0, "video_basic": 12, "video_creative": 20 }
}
used_this_month is the net spend for the current UTC month (refunds subtracted), and
remaining is the allowance minus that spend — neither counts the credits held by renders
still in flight. reserved is the total held by outstanding renders, and
available is the allowance after spend and holds: the figure every
affordability check uses, so it is what you can actually queue right now (see
credits). resets_at is the first instant of the next UTC month.
Health check
No key, no credits — 200 {"ok":true} while the service is up. Live component status is
on the status page.
Webhooks
Instead of polling — or alongside it — AdGen POSTs a signed JSON event to your server the moment a job
reaches a terminal state. Configure one endpoint in your dashboard: paste an
https:// URL, save, and copy the signing secret, which is shown once.
Scope: video renders only. job.completed and job.failed are
emitted for video render jobs (POST /v1/campaigns/:id/video and POST /v1/videos).
An image batch emits nothing today — poll its job and read the finished images from
GET /v1/campaigns/:id.
The endpoint belongs to your account, not to a key: regenerating your API key does not affect it, and events for jobs queued under an older key keep arriving. The URL must be public — loopback, private, link-local, CGNAT and multicast targets are refused when you save them and again at delivery time.
The request
POST /hooks/adgen HTTP/1.1
Host: your-app.example.com
Content-Type: application/json
X-AdGen-Signature: t=<unix-seconds>,v1=<hex>
X-AdGen-Delivery-Id: 4711
{
"event": "job.completed",
"jobId": "1c9be05af7d2461a",
"campaignId": "9f3c1a77b2e40d51",
"status": "done",
"videoUrl": "https://cdn.example/v.mp4",
"thumbnailUrl": "https://cdn.example/t.jpg",
"error": null,
"creditsCharged": 12,
"createdAt": 1785000000000,
"finishedAt": 1785000060000
}
All ten keys are always present (null means "does not apply"), and new keys may be added
later, so ignore ones you do not recognize. status, videoUrl,
thumbnailUrl, error, createdAt and finishedAt carry
exactly the values GET /v1/jobs/:id returns for the same job, in the same
epoch-millisecond units — poll and listen at once and you can never see two different answers for them —
and the webhook's jobId is that endpoint's id. Three fields are
webhook-only: event, campaignId and
creditsCharged; that last one has no counterpart when you reconcile by polling, so record it
when the event arrives if you need per-render billing.
creditsCharged is what you were actually billed, net of refunds and
0 for failed or cancelled renders. A cancellation is a job.failed with
"status":"cancelled" — it produced no video, so it belongs in your failure branch.
X-AdGen-Delivery-Id is the idempotency key: one delivery has one id, and it
is stable across every retry. Dedupe on it.
Verifying the signature
X-AdGen-Signature is t=<unix-seconds>,v1=<hex>, where
v1 is HMAC-SHA256 over ${timestamp}.${body} with your secret, in lowercase hex.
The timestamp is inside the signed bytes, which is what makes a captured request expire rather
than replay forever. body must be the raw request bytes: re-serializing a
parsed object changes key order and spacing, and the signature will not match. Allow
300 seconds of clock skew and reject anything outside it.
app.post('/hooks/adgen', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body.toString('utf8')
const [t, v1] = req.get('X-AdGen-Signature').split(',').map((p) => p.slice(p.indexOf('=') + 1))
const expected = crypto.createHmac('sha256', SECRET).update(`${t}.${raw}`).digest('hex')
if (expected.length !== v1.length || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))) {
return res.sendStatus(400)
}
res.sendStatus(200) // answer FIRST, do the work after
queueForProcessing(JSON.parse(raw))
})
That snippet is the minimum. Two more rules are worth adding: reject a signature whose t
falls outside your tolerance window (that is what makes a captured request expire), and reject a header
carrying a repeated field — an attacker who can append ,v1=… must not get to choose
which of two signatures you check.
Responses, retries and give-up
| Rule | Behavior |
|---|---|
| Success | Any 2xx. Anything else is a failed delivery. |
| Redirects | Not followed — a 3xx is a failure, not a hop. |
| Timeout | Your endpoint has 10 seconds to answer. |
| Response body | Read up to 16 KB, only to quote back to you in the dashboard. |
| Retries | 6 attempts, spaced 1m → 5m → 30m → 2h → 6h — about 8.6 hours before the delivery is retired. |
Answer 2xx as soon as you have stored the event and do the work afterwards: a handler that
renders something of its own before replying will hit the timeout and be retried. Four cases are
retired without retrying, because no later attempt could succeed: a URL that is not a
public http(s) address; a hostname that resolves to a private address (refused at
connect time on the first attempt — including a split-horizon name that
answers with both a public and a private record, since every address returned has to pass); a webhook
removed while the delivery was queued; and a webhook repointed to a different URL, whose
already-queued deliveries are retired rather than sent to an endpoint you have disowned.
You can rotate the signing secret without touching the URL at any time. Queued deliveries keep flowing, because each attempt signs with the current secret — accept both the old and the new one while you deploy the change.
What is guaranteed
At most once at the source. An event fires only on a job's real terminal transition, so
a duplicate or late finish never fires a second one — but queuing it is deliberately best-effort (webhook
bookkeeping must never fail a render that already succeeded), so a crash between the status write and the
queue insert drops the event permanently. At-least-once in delivery: a retry can send the
same event twice, so dedupe on X-AdGen-Delivery-Id.
GET /v1/jobs/:id is the authority — if you must never miss
a terminal state, sweep your outstanding job ids on a slow timer and read them back.
Cancellation is asymmetric: DELETE /v1/jobs/:id answering
{"status":"cancelled"} is terminal and sends no webhook — that response
is the notification — while {"status":"cancelling"} means a worker still holds the
job and a job.failed with "status":"cancelled" follows when it stops.
Markets
market adapts copy, hooks and narration to a market's language, customs and way of
speaking. auto (or omitting the field) picks eg for Arabic campaigns and
global for English ones. Arabic markets are written in Modern Standard Arabic with the
market's own register.
| Code | Market | Code | Market |
|---|---|---|---|
global | Global (English) | eg | Egypt |
us | United States | sa | Saudi Arabia |
uk | United Kingdom | ae | UAE |
jo | Jordan / Levant | ma | Morocco / Maghreb |
Styles
style sets the shared visual language of a campaign's images.
| Key | Style | Key | Style |
|---|---|---|---|
realistic | Realistic (default) | isometric | Isometric |
cinematic | Cinematic | watercolor | Watercolor |
3d | 3D Render | layout | Editorial Layout |
cartoon | Cartoon | neon | Neon / Cyber |
flat | Flat Illustration | collage | Collage |
minimal | Minimal | gradient | Gradient / Abstract |
Errors
Every response body is JSON carrying a single error string, plus
required/remaining on a 402.
| Status | Meaning |
|---|---|
400 | Validation error — {"error":"<what to fix>"}. |
401 | Missing or invalid API key. |
402 | Insufficient credits — {"error":"Insufficient credits","required":n,"remaining":n}. |
404 | Unknown campaign/job, or one belonging to another key. |
409 | Cancel requested on an already-finished job. |
413 | Request body over the 1 MB limit — {"error":"Request body too large"}. |
429 | Rate limited — {"error":"Rate limited"}, retry after Retry-After seconds. |
500 | Unexpected server error — {"error":"Internal error"}. |
502 | AI generation failed; nothing was charged — retry. |
Two of these are raised before your request reaches an endpoint — a body that is not valid JSON is
400 {"error":"Invalid JSON body"} and one over the size cap is a 413 — so they
are answered before the API key is even checked.
Rate limits
Each key gets a requests-per-minute ceiling from its plan, counted over all
/v1 endpoints together (cheap reads included). Over the ceiling is
429 {"error":"Rate limited"} with Retry-After: 60. The window is a fixed clock
minute, so a 429 clears at the next minute boundary — wait out Retry-After and
retry; nothing is charged and no job is affected.
A separate ceiling applies per source IP address across every endpoint, answering with
429 {"error":"Too many requests"} — normal use never reaches it.
Polling a job is the one loop that can realistically reach the ceiling; see polling cadence for the interval to use on each plan.
FAQ
What is AdGen API?
AdGen API is a developer REST API for AI ad campaign generation, built by Sikasio and served
from https://adgen-api.sikasio.com. You POST a short brief (brand, goal, audience,
tone) to POST /v1/campaigns with an X-API-Key header and get back a
campaign of planned posts — headline, body, caption, hashtags and an image prompt each. From
there you can queue branded images for those posts and render them into a short-form campaign
video with optional AI narration. Copy, images, narration and pacing are produced by
AdGen AI. English and Arabic, free tier with an instant API key.
How do I generate an ad campaign with an API?
Send your brief to POST https://adgen-api.sikasio.com/v1/campaigns with your key
in the X-API-Key header. Only brand.name and goal are
required; the call is synchronous and returns the finished posts in 5–20 seconds.
curl -X POST "https://adgen-api.sikasio.com/v1/campaigns" \
-H "X-API-Key: adg_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"brand":{"name":"Acme Coffee"},"goal":"Launch our cold brew subscription","postCount":4}'
A campaign plan costs 2 credits, and a failed plan costs nothing.
Is there a free ad campaign generation API?
Yes — a permanent free plan, no card required: $0 for 40 credits per month at 5 requests per minute, with a 7-day asset retention window.
| Plan | Price | Credits / mo | Req / min |
|---|---|---|---|
| Free | $0 | 40 | 5 |
| Starter | $19 | 400 | 15 |
| Pro | $49 | 1,200 | 30 |
Sign up — your API key is issued instantly.
How do I generate branded images for a campaign?
POST https://adgen-api.sikasio.com/v1/campaigns/:id/images with an optional
model (flash, studio or vivid) and optional
postIds. It answers 202 with a jobId; poll the job, then
read each post's imageUrl from GET /v1/campaigns/:id.
curl -X POST "https://adgen-api.sikasio.com/v1/campaigns/CAMPAIGN_ID/images" \
-H "X-API-Key: adg_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"studio"}'
Images cost 2 credits each, take roughly 30–60 seconds per image, and every image that fails is refunded.
How do I turn a campaign into a video ad?
Once at least one post has a generated image, POST
https://adgen-api.sikasio.com/v1/campaigns/:id/video. mode: "basic" is a clean
image sequence (12 credits); mode: "creative" adds an AI-written kinetic script over
designed templates (20 credits). Add a voiceover object for narration.
curl -X POST "https://adgen-api.sikasio.com/v1/campaigns/CAMPAIGN_ID/video" \
-H "X-API-Key: adg_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"mode":"creative","voiceover":{"enabled":true,"voice":"aria"}}'
It renders in a few minutes, and the finished videoUrl and
thumbnailUrl appear on GET /v1/jobs/:id. Nothing is charged unless the
render finishes.
How do credits work and what does each action cost?
| Action | Credits |
|---|---|
| Campaign plan | 2 |
| Image (each) | 2 |
| Hook options | free |
| Video — basic | 12 |
| Video — creative | 20 |
Charged on success only: a failed plan, image or render costs nothing and
cancelled work is refunded. The price of a render still in flight is held against your
balance, so reserved and available on
GET https://adgen-api.sikasio.com/v1/account tell you what you can queue right now.
Allowances reset at the first instant of each UTC month and do not roll over.
What are the rate limits and how often should I poll a job?
Your plan's ceiling covers all /v1 calls together — free 5/minute, starter
15/minute, pro 30/minute. Because every poll spends from it, use these intervals on
GET https://adgen-api.sikasio.com/v1/jobs/:id:
| Plan | Poll every | Ceiling |
|---|---|---|
| Free | 15 seconds | 5 rpm |
| Starter | 10 seconds | 15 rpm |
| Pro | 5 seconds | 30 rpm |
Over the ceiling is 429 with Retry-After: 60. A
rate-limited poll never affects the job, which keeps running.
How do I get an API key for AdGen?
- Create a free account at /signup (name, email, password).
- Enter the 6-digit code we email you to verify.
- Copy your API key — shown once; regenerate any time from your dashboard.
- Send it as
X-API-Key(orAuthorization: Bearer adg_live_YOUR_KEY) on every/v1request tohttps://adgen-api.sikasio.com.
The whole flow takes under a minute and needs no card.
Does AdGen generate Arabic ad campaigns?
Yes. Set language: "ar" on POST
https://adgen-api.sikasio.com/v1/campaigns and the copy, hooks, on-screen captions and
narration are written in Modern Standard Arabic (فصحى), adapted to a market's register:
eg, sa, ae, jo or ma — with
global, us and uk for English.
Arabic renders support seven display fonts (cairo, tajawal,
almarai, changa, messiri, amiri,
lalezar) and four narration voices — aria and nova
(female), sami and omar (male) — plus mixed, which
alternates voices across scenes.
How long are generated images and videos hosted?
Finished assets — each post's imageUrl plus a render's videoUrl and
thumbnailUrl — are served from stable URLs on the AdGen CDN
(adgen-cdn.sikasio.com) and may be removed after the retention window
for your plan: 7 days on Free, 30 days on Starter, 90 days on Pro. Download anything you need to
keep; you can always re-read the current URLs from
GET https://adgen-api.sikasio.com/v1/campaigns/:id.
The window is fixed when the asset is written: an asset keeps the retention window of the plan your key held at the moment it was uploaded. Changing plan applies to assets produced afterwards — an upgrade does not extend assets already delivered, and a downgrade does not shorten them. A URL, once issued, is never rewritten.
Can I use the generated ads commercially?
Yes. Copy, images and videos generated on any plan of
https://adgen-api.sikasio.com — including the free tier — can be used in commercial
advertising, stores, apps and social campaigns, subject to the
terms of service. You are responsible for the brands, trademarks and
reference material you submit, and for any advertising-disclosure or platform rules that apply
where you publish.
Get an API key
Sign up and get a free key instantly — 40 credits a month, no card required. Upgrade to
starter or pro from your dashboard whenever you need
more.
Questions? support@sikasio.com