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 → poll GET /v1/jobs/:id
Auth
X-API-Key header — 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

Get your free API key →

Getting your API key

New here? You can be making your first request in under a minute:

  1. Create a free account at /signup — name, email and a password, and accept the terms. No card required.
  2. Verify your email by entering the 6-digit code we send you.
  3. 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.
  4. Send the key as the X-API-Key header (or Authorization: Bearer) on every /v1 request.
  5. Make your first call: POST /v1/campaigns with 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:

PlanPriceCredits / monthRequests / minAsset retention
free$04057 days
starter$194001530 days
pro$491,2003090 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

ActionEndpointCredits
Campaign plan (copy)POST /v1/campaigns2
Image (each)POST /v1/campaigns/:id/images2
Hook optionsPOST /v1/campaigns/:id/hooks0 (free)
Video render — basicPOST /v1/campaigns/:id/video · POST /v1/videos12
Video render — creativePOST /v1/campaigns/:id/video · POST /v1/videos20

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.

StatusMeaning
queuedaccepted, waiting for a worker
renderingin progress — progress 0–100, stage is a human-readable step
donefinished successfully
failedfinished with an error (see error)
cancelledcancelled 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:

PlanSuggested intervalCeiling
freeevery 15 seconds5 rpm
starterevery 10 seconds15 rpm
proevery 5 seconds30 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

POST/v1/campaignsrequires X-API-Key

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.

FieldTypeNotes
brand.name requiredstring≤ 60 chars.
brand.colorsstring[]≤ 5 hex colors, e.g. "#1e293b".
brand.logoUrlstringPublic http(s) URL.
goal requiredstring≤ 500 chars.
audiencestring≤ 300 chars.
tonestring≤ 200 chars.
ctastring≤ 120 chars — the action the video's CTA scene is scripted around.
languagestringen (default) · ar. "AR", "ar-EG", "ar_SA" all mean ar; anything unrecognized falls back to en (never an error).
marketstringSee markets. Default/auto → eg for Arabic, global for English. An unknown code is a 400.
stylestringSee styles. Default realistic.
aspectstringportrait (default) · landscape · square.
postCountinteger1–8, default 4.
content.linkstringPublic http(s) URL, used as the CTA destination.
content.includeHowTobooleanAsk for how-to style posts.
content.edgestring≤ 300 chars — your differentiator, woven into the copy.
consistentIdentitybooleanAlso generate one shared art direction so every image looks like one set.
content.includeEdgebooleanAsk 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.
contextstring≤ 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.
imageInstructionsstring≤ 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

GET/v1/campaigns/:idrequires X-API-Key

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

POST/v1/campaigns/:id/hooksrequires X-API-Key0 credits

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.

FieldTypeNotes
marketstringDefault/""/auto = the campaign's own market. Unknown code → 400.
forcebooleanIgnore 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

POST/v1/campaigns/:id/imagesrequires X-API-Key

Queues image generation for the campaign's posts. Returns immediately; poll the job, then read the URLs from GET /v1/campaigns/:id.

FieldTypeNotes
modelstringflash is fastest, studio is the balanced default, vivid is the most stylized.
postIdsinteger[]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.
referenceImageUrlstringPublic 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

PUT/v1/campaigns/:id/imagesrequires X-API-Key

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.

FieldTypeNotes
imagesobject[]Required, 1–8 entries.
images[].postIdintegerA post id from this campaign. Give this or order, never both.
images[].orderintegerThe post's order (0-based) instead of its id.
images[].urlstringRequired. 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

POST/v1/campaigns/:id/videorequires X-API-Key

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.

FieldTypeNotes
modestringbasic (default) is a clean image sequence; creative adds an AI-written kinetic script (hook, beats, stats, CTA) over designed templates.
aspectstringportrait (default) · landscape · square.
marketstringDefault/auto = the campaign's market. Drives script copy and narration delivery.
brandNamestring≤ 60 chars — per-render override of the displayed brand/project name.
watermarkobject{ 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.
hookobject{ headline, underline?, kicker? } — a chosen option from /hooks; overrides the AI's own opening hook.
voiceoverobjectSee below; omitted or enabled: false = silent video.
transitionstringsmooth (default) · simple · none · cinematic · push.
sfx.enabledbooleanDefault true — sound-effect layer.
captionsbooleanDefault true — on-screen scene copy.
subtitlesbooleanDefault true — burned-in narration subtitles.
arFontstringcairo (default) · tajawal · almarai · changa · messiri · amiri · lalezar — Arabic renders only.
videoHookbooleanDefault false — open on matching stock footage instead of an AI-generated hero image.

The voiceover object

FieldTypeNotes
enabledbooleanRequired to get narration.
voicestringaria · nova (female) · sami · omar (male) · mixed alternates voices across scenes.
genderstringmale · female · mixed — only consulted when voice is omitted; default female.
languagestringen · ar. Default: the selected market's language.
modelstringstandard (default) · premium.
ratenumber0.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

POST/v1/videosrequires X-API-Key

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.

FieldTypeNotes
brand.name requiredstring≤ 60 chars.
brand.colorstringCSS color, ≤ 32 chars, default "#111827".
brand.logoUrlstringPublic http(s) URL.
brand.domainstring≤ 100 chars, shown on the CTA scene.
scenesobject[]Up to 6 — { imageUrl (required, public http(s)), headline ≤120, caption ≤140 }.
scriptobject[]Up to 12 designed scenes (see below).
modestringbasic (default) · creative, which requires a non-empty script[].
hookobject{ headline ≤120 (default: brand name), subtext ≤90 (default "See what we made"), bgPrompt ≤500 } — bgPrompt describes the generated hero background.
hookVideoQuerystring≤ 120 chars, stock-footage search phrase for the opener.
videoHookbooleanDefault false — use stock footage for the opener.
langstringen (default) · ar.
marketstring≤ 20 chars, informational hint passed to the renderer.
paletteobjectHex-only color map, e.g. { "bg": "#0b1020", "accent": "#4f46e5" }; non-hex entries are dropped.
sceneStylesstring[]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.
voiceoverobjectSame 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

TypeFields (max length)
hookheadline (120), underline (60), kicker (40)
beattitle (120), sub (160)
statvalue (40), label (120)
showcasecaption (140), sub (160), imageUrl (public http(s))
ctatext (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

GET/v1/jobs/:idrequires X-API-Key
{
  "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

DELETE/v1/jobs/:idrequires X-API-Key

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

GET/v1/accountrequires X-API-Key

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

GET/healthzpublic

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

RuleBehavior
SuccessAny 2xx. Anything else is a failed delivery.
RedirectsNot followed — a 3xx is a failure, not a hop.
TimeoutYour endpoint has 10 seconds to answer.
Response bodyRead up to 16 KB, only to quote back to you in the dashboard.
Retries6 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.

CodeMarketCodeMarket
globalGlobal (English)egEgypt
usUnited StatessaSaudi Arabia
ukUnited KingdomaeUAE
joJordan / LevantmaMorocco / Maghreb

Styles

style sets the shared visual language of a campaign's images.

KeyStyleKeyStyle
realisticRealistic (default)isometricIsometric
cinematicCinematicwatercolorWatercolor
3d3D RenderlayoutEditorial Layout
cartoonCartoonneonNeon / Cyber
flatFlat IllustrationcollageCollage
minimalMinimalgradientGradient / Abstract

Errors

Every response body is JSON carrying a single error string, plus required/remaining on a 402.

StatusMeaning
400Validation error — {"error":"<what to fix>"}.
401Missing or invalid API key.
402Insufficient credits — {"error":"Insufficient credits","required":n,"remaining":n}.
404Unknown campaign/job, or one belonging to another key.
409Cancel requested on an already-finished job.
413Request body over the 1 MB limit — {"error":"Request body too large"}.
429Rate limited — {"error":"Rate limited"}, retry after Retry-After seconds.
500Unexpected server error — {"error":"Internal error"}.
502AI 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.

PlanPriceCredits / moReq / min
Free$0405
Starter$1940015
Pro$491,20030

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?
ActionCredits
Campaign plan2
Image (each)2
Hook optionsfree
Video — basic12
Video — creative20

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:

PlanPoll everyCeiling
Free15 seconds5 rpm
Starter10 seconds15 rpm
Pro5 seconds30 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?
  1. Create a free account at /signup (name, email, password).
  2. Enter the 6-digit code we email you to verify.
  3. Copy your API key — shown once; regenerate any time from your dashboard.
  4. Send it as X-API-Key (or Authorization: Bearer adg_live_YOUR_KEY) on every /v1 request to https://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.

Get your free API key →

Questions? support@sikasio.com