Developer Documentation

ars0n API

Generate professional video, radio, and UGC actor ads programmatically. Upload images, pick an AI creator or a voice, set your style — get a finished ad back. Same engine as the dashboard, fully accessible via REST.

Getting Started

1

Create an account

Sign up at /signup, then log in to the dashboard

2

Create an API key

Dashboard → Settings → API → Create Key

3

Set env variables

ARS0N_API_BASE_URL + ARS0N_API_KEY

4

Make your first call

POST to /api/v1/generate

Environment Setup

bash
# Add to your .env file:
ARS0N_API_BASE_URL=https://app.ars0n.ai
ARS0N_API_KEY=sk_live_...    # from Dashboard → Settings → API

The Pipeline

The API supports two ad formats. Each follows its own pipeline — you just submit and poll.

Video Ad Pipeline

Upload images
Pick a voice
Submit prompt
AI plans scenes
Image editing
Video generation
Voiceover
Final compositing

Radio Ad Pipeline (audio-only)

Pick a voice
Submit prompt
AI writes script
Voiceover
Audio compositing

Radio ads are audio-only — no images or video needed. Much faster and cheaper (8-15 credits vs ~200+ for cinematic video).

Quick Start

Generate a video in 4 API calls:

Step 1 — Check your balance

bash
curl $ARS0N_API_BASE_URL/api/v1/credits \
  -H "Authorization: Bearer $ARS0N_API_KEY"
# → { "balance": 1100, "tier": "starter", "activeGenerations": 0 }

Step 2 — Find a voice

bash
curl "$ARS0N_API_BASE_URL/api/v1/voices?gender=male&accent=american" \
  -H "Authorization: Bearer $ARS0N_API_KEY"
# → { "voices": [{ "voice_id": "pNInz6obpgDQGcFmaJgB", "name": "Adam", ... }] }

Step 3 — Generate

bash
curl -X POST $ARS0N_API_BASE_URL/api/v1/generate \
  -H "Authorization: Bearer $ARS0N_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Premium wireless headphones — sleek, modern, lifestyle",
    "imageUrls": ["https://example.com/headphones.png"],
    "duration": 15,
    "voiceId": "pNInz6obpgDQGcFmaJgB",
    "aspectRatio": "9:16"
  }'
# → { "versionId": "abc-123", "totalCredits": 200, "jobCount": 8 }

Step 4 — Poll until done (required)

bash
# Poll every 10 seconds — this also drives the pipeline forward!
# Without polling, jobs stay in "pending" forever.
curl $ARS0N_API_BASE_URL/api/v1/jobs/abc-123 \
  -H "Authorization: Bearer $ARS0N_API_KEY"
# → { "status": "completed", "progress": 100, "videoUrl": "https://ars0n.ai/api/v1/video?id=..." }

Two-Step Plan Flow

For maximum control, use the two-step flow: generate a plan, preview and edit the script, then generate with your modified plan. This is the recommended approach for production integrations.

Step 1 — Generate a plan (no credits)

bash
curl -X POST $ARS0N_API_BASE_URL/api/v1/plan \
  -H "Authorization: Bearer $ARS0N_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Premium wireless headphones — sleek, modern, lifestyle",
    "adFormat": "video",
    "duration": 30,
    "voiceId": "pNInz6obpgDQGcFmaJgB",
    "imageAssetIds": ["asset-1", "asset-2"]
  }'
# → { "plan": { "voiceover_script": "...", "scenes": [...] }, "estimatedCredits": 390 }

Step 2 — Preview the voice (optional)

bash
curl -X POST $ARS0N_API_BASE_URL/api/v1/preview-voice \
  -H "Authorization: Bearer $ARS0N_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Immerse yourself in pure sound.",
    "voiceId": "pNInz6obpgDQGcFmaJgB"
  }'
# → { "audioUrl": "https://...", "durationSeconds": 3.2 }

Step 3 — Edit the script + add custom clips

Modify plan.voiceover_segments, reorder scenes, or insert your own video clips between AI scenes. Upload clips via POST /api/v1/assets (MP4/WebM/MOV, max 100MB, max 90s). Clips are auto-transcoded to 720p with audio stripped. Use the returned assetId in customClips (each segment plays up to 30s).

Step 4 — Generate with your modified plan

bash
curl -X POST $ARS0N_API_BASE_URL/api/v1/generate \
  -H "Authorization: Bearer $ARS0N_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Premium wireless headphones",
    "adFormat": "video",
    "duration": 30,
    "voiceId": "pNInz6obpgDQGcFmaJgB",
    "imageAssetIds": ["asset-1", "asset-2"],
    "plan": { "...your edited plan..." },
    "customClips": [{ "position": 2, "asset_id": "clip-id", "voiceover_text": "See it in action.", "duration_seconds": 6 }]
  }'
# → { "versionId": "ver_xyz", "totalCredits": 390, "jobCount": 12 }

Step 5 — Poll until done

bash
curl $ARS0N_API_BASE_URL/api/v1/jobs/ver_xyz \
  -H "Authorization: Bearer $ARS0N_API_KEY"
# → { "status": "completed", "progress": 100, "videoUrl": "https://..." }

Important: Polling is required

The /jobs/{versionId} endpoint doesn't just report status — it actively drives the pipeline forward. You must poll every 10 seconds until the status reaches "completed" or "failed". Without polling, jobs will remain in "pending" indefinitely.

Base URL: All endpoints are prefixed with /api/v1. API credits are deducted upfront when you call /generate. If generation fails, credits are automatically refunded. Use /estimate to preview costs before committing.

Image Limits by Duration

15s → max 3 images·30s → max 5 images·60s → max 9 images

Authentication

Every request requires a Bearer token. Keys use the sk_live_ prefix and are scoped to your organization.

Required Environment Variables

  • ARS0N_API_BASE_URL — Your Arson instance URL (e.g. https://app.ars0n.ai)
  • ARS0N_API_KEY — Your API key (sk_live_...)
bash
# Set both env vars, then use them in requests:
export ARS0N_API_BASE_URL="https://app.ars0n.ai"
export ARS0N_API_KEY="sk_live_a1b2c3d4e5f6..."

curl $ARS0N_API_BASE_URL/api/v1/credits \
  -H "Authorization: Bearer $ARS0N_API_KEY"

# Every language works the same way:
# Just set the Authorization header to "Bearer <your-key>"
python
import os, requests

BASE = os.environ["ARS0N_API_BASE_URL"]
API_KEY = os.environ["ARS0N_API_KEY"]

resp = requests.get(
    f"{BASE}/api/v1/credits",
    headers={"Authorization": f"Bearer {API_KEY}"},
)
print(resp.json())
typescript
const BASE = process.env.ARS0N_API_BASE_URL!;
const API_KEY = process.env.ARS0N_API_KEY!;

const res = await fetch(
  `${BASE}/api/v1/credits`,
  {
    headers: {
      Authorization: `Bearer ${API_KEY}`,
    },
  }
);
console.log(await res.json());

Creating Keys

1

Go to Settings → API

Click "Create Key" and name it (e.g. "Production", "Staging")

2

Copy the key immediately

The full key is shown only once. We store a SHA-256 hash — the plain key cannot be recovered.

3

Store as an environment variable

ARS0N_API_KEY=sk_live_... — never hardcode keys in source.

Key Properties

Formatsk_live_ + 64 hex chars
ScopeOrganization-level — all members share keys
LimitUnlimited keys per org (use one per environment)
RevokeInstant — revoked keys return 401 immediately

Security

  • Never commit keys to git or include in client-side code
  • Rotate periodically — create new, deploy, revoke old
  • Use separate keys for dev / staging / production
  • Monitor "last used" in the dashboard, revoke idle keys

Endpoints

All endpoints require Bearer authentication. Base path: /api/v1

versionId vs jobId

A versionId identifies the entire generation (returned by /generate). A jobIdidentifies a single step within that generation (e.g. one scene's image edit or video render). Use versionId for /jobs/{versionId} (status polling) and /jobs/{versionId}/cancel. Use jobId for /jobs/{jobId}/retry and /jobs/{jobId}/regenerate.

Pipeline Job Types

Video ads create a DAG of jobs: image_composition (AI image editing, 2 variants scored per scene) → video_synthesis (image-to-video per scene) + voiceover (AI speech synthesis) → compositing (final assembly).

Radio ads (audio-only) create: voiceover (AI speech synthesis) → audio_compositing (mixes voiceover with optional background music — always included for radio ads).

UGC ads (via /ugc/generate) additionally use: actor_portrait (generates the locked actor portrait every scene is conditioned on — only present when creating a fresh actor rather than picking a library one) and talking_head (one per script beat — the actor-locked spoken performance), alongside image_composition / video_synthesis for product b-roll and a final compositing job.

POST/api/v1/generateDeducts API credits

Start a video or radio ad generation pipeline. Provide a prompt, a voice, and optional settings. Video ads also require product images.

Request Body

FieldTypeReqDescription
adFormatstringOptional"video" (default) or "radio". Radio ads are audio-only — no images or video needed.
promptstringRequiredCreative prompt describing your ad (1-2000 chars)
imageUrlsstring[]*Product image URLs. Server downloads them. Required for video ads. Max depends on duration: 3 for 15s, 5 for 30s, 9 for 60s. Not needed for radio ads.
imageAssetIdsstring[]*Pre-uploaded asset IDs from /v1/assets. Same limits as imageUrls. Not needed for radio ads.
durationnumberRequiredAd length: 15, 30, or 60 seconds (applies to both video and radio)
voiceIdstringRequiredVoice ID from /v1/voices
aspectRatiostringOptional"9:16" (default) or "16:9"
qualitystringOptional"standard" (default) | "premium". Premium renders at higher fidelity and costs more credits.
logoAssetIdstringOptionalLogo overlay — upload via /v1/assets first. Video ads only.
musicAssetIdstringOptionalBackground music — upload MP3/WAV via /v1/assets. For radio ads, mixed with voiceover during audio compositing.
musicVolumenumberOptionalBackground music volume (0.0-1.0, default 0.3). Applies to both video and radio ads.
companyNamestringOptionalCompany name shown in CTA end card (max 100)
ctaDesignstringOptional"minimal" | "bold" | "gradient" | "split" | "glass" | "neon" | "editorial" | "brutalist"
gradientColorstringOptionalHex color for gradient CTA (#FF5500)
voiceStylestringOptionalHint for AI voice direction (max 200). Applies to both video and radio ads.
musicMoodstringOptionalHint for AI music selection (max 200). Applies to both video and radio ads.
tonestringOptionalCreative tone — "luxury", "playful", "corporate" (max 200). Applies to both video and radio ads.
editStylestringOptionalImage editing direction applied to every scene (max 1000). Guides both the AI scene planner and image editor. E.g. "place all screenshots on realistic iPhone screens held by people in lifestyle settings"
emotionstringOptionalEmotion style for animation and voiceover (max 200). Applies to both video and radio ads.
qualityLoopVariants1 | 2 | 3OptionalImage edit variants per scene. Default 2. Use 3 for max quality. Cost per scene scales: 10 / 19 / 29 credits for 1 / 2 / 3 variants.
planobjectOptionalPre-generated plan from /v1/plan. Skips AI scene planning and uses your edited script and scenes directly. All text fields in the plan are screened against our content policy — a violating plan is rejected with a 400. When a plan is submitted, quotedCredits is required.
quotedCreditsnumberOptionalThe credit total returned by /v1/plan (or /v1/estimate). Required when submitting a plan. If the recomputed cost exceeds it — plan edits changed scene durations or models, or prices moved — the request is rejected with 409 PRICE_CHANGED and the new total instead of charging more than you were quoted. Re-submit with quotedCredits set to newTotal to accept.
customClipsobject[]OptionalCustom video clips to insert between AI-generated scenes. Each clip: { position, asset_id, voiceover_text?, duration_seconds }. Max 10 clips. Upload videos via /v1/assets first.

* Either imageUrls or imageAssetIds is required for video ads (not both). Radio ads do not require images.

Response

json
{
  "projectId": "proj_abc123",
  "versionId": "ver_def456",   // Use this to poll status
  "totalCredits": 181,
  "jobCount": 6
}
GET/api/v1/jobs/{versionId}

Critical: Polling drives the pipeline

This endpoint doesn't just report status — it actively advances pending jobs. Without polling, API-created jobs will sit in "pending" forever. You must poll every 10 seconds until the version reaches a terminal state.

Poll generation progress. Call every 10 seconds until status is "completed" or "failed".

Both videoUrl and audioUrl are always present in the response. For video ads, videoUrl contains the final video and audioUrl is null. For radio ads, audioUrl contains the final audio and videoUrl is null.

Both are absolute URLs pointing at GET /api/v1/video, which requires your Authorization: Bearer header — putting one directly in a browser <video src> tag returns a 401. Fetch the URL from your backend with the header set: the endpoint answers with a 302 redirect to a short-lived signed URL (valid 2 hours), which you can follow server-side or hand to your client player.

json
{
  "versionId": "ver_def456",
  "adFormat": "video",           // "video" or "radio"
  "status": "generating",       // "generating" | "completed" | "failed"
  "progress": 60,               // 0-100
  "videoUrl": null,              // Set for video ads when completed
  "audioUrl": null,              // Set for radio ads when completed
  "totalCredits": 181,
  "jobs": {
    "total": 6,
    "completed": 3,
    "failed": 0,
    "details": [
      {
        "id": "job_abc",
        "type": "image_composition",
        "status": "completed",  // "waiting" | "pending" | "processing" | "polling" | "completed" | "failed"
        "output_asset_id": "asset_123",
        "assetUrl": "https://ars0n.ai/api/v1/video?id=asset_123",
        "error_message": null,  // string when failed, null otherwise
        "params": { "scene_index": 0 },
        "depends_on": [],
        "credit_cost": 19       // present when the row carries it (present on queued jobs too) (completed/failed)
      },
      {
        "id": "job_def",
        "type": "video_synthesis",
        "status": "waiting",    // waiting for dependencies (job_abc) to complete
        "output_asset_id": null,
        "assetUrl": null,
        "error_message": null,
        "params": { "scene_index": 0 },
        "depends_on": ["job_abc"]
      }
    ]
  }
}

Job statuses: waiting — job is waiting for its dependencies to complete before it can start. pending — ready to be picked up on the next poll. processing — actively running. polling — submitted to an external provider, awaiting result. completed / failed — terminal states.

GET/api/v1/credits

Check your API credit balance, current tier, and active generation count.

Partner accounts with unlimited API credits receive balance: null and unlimited: true.

json
{
  "balance": 450,                 // null when unlimited is true
  "unlimited": false,             // true for partner accounts with unlimited API credits
  "tier": "starter",
  "activeGenerations": 1,         // distinct in-flight pipelines
  "limits": {
    "maxConcurrentGenerations": 1,
    "rateLimitRpm": 60
  }
}
POST/api/v1/assets

Upload images, audio, or video clips as multipart form data. Use the returned assetId in generate calls. Images and video frames are screened against our content policy before they are stored — a violating upload is rejected with a 400 and nothing is saved.

Images

PNG, JPEG, WebP — max 10MB

Audio

MP3, WAV, OGG, AAC, M4A — max 15MB

Video

MP4, WebM, MOV — max 100MB

Form Fields

  • file required — The file to upload
  • category optional "product_image" | "logo" | "music" | "clip" (inferred from MIME type if omitted; any other value is rejected with a 400)
json
{
  "assetId": "asset_abc123",    // Use in imageAssetIds, logoAssetId, etc.
  "type": "image",              // "image", "audio", or "video"
  "url": "https://...",
  "durationSeconds": null       // Set for video clips (post-transcode duration)
}

Video Clip Processing

Uploaded video clips are automatically transcoded server-side to 720p H.264 at 30fps with audio stripped. This ensures compatibility with the compositing pipeline. The returned assetId can be used in customClips when calling /api/v1/generate. Maximum clip duration: 90 seconds (each customClips segment may use up to 30 seconds of it). Maximum file size: 100MB.

GET/api/v1/voices

Browse the voice library. Use the voice_id from the response as your voiceId in generate calls.

Query Parameters (all optional)

  • gender"male", "female"
  • accent"american", "british", "australian", etc.
  • age"young", "middle_aged", "old"
  • q — Free-text search across name and description
json
{
  "voices": [
    {
      "voice_id": "pNInz6obpgDQGcFmaJgB",
      "name": "Adam",
      "gender": "male",
      "accent": "american",
      "age": "middle_aged",
      "description": "Deep, warm, authoritative voice",
      "preview_url": "https://ars0n.ai/api/voice-sample?voiceId=pNInz6obpgDQGcFmaJgB",
      "use_case": "narration",
      "category": "premade"
    }
  ]
}
POST/api/v1/plan

Generate a creative plan without starting the pipeline. No credits deducted. Returns the plan, cost estimate, and breakdown.

Request Body

FieldTypeReqDescription
promptstringRequiredCreative prompt describing your ad (1-2000 chars)
adFormatstringOptional"video" (default) or "radio"
durationnumberRequiredAd length: 15, 30, or 60 seconds
voiceIdstringRequiredVoice ID from /v1/voices
voiceStylestringOptionalVoice delivery style hint for the script (max 200 chars)
musicMoodstringOptionalBackground music mood hint (max 200 chars)
tonestringOptionalOverall ad tone (max 200 chars)
emotionstringOptionalEmotional register for the narration (max 200 chars)
qualityLoopVariants1 | 2 | 3OptionalImage edit variants per scene. Default 2. Cost scales 10 / 19 / 29 credits per scene.
aspectRatiostringOptional"9:16" (default) or "16:9"
qualitystringOptional"standard" (default) | "premium". Premium renders at higher fidelity and costs more credits.
imageAssetIdsstring[]OptionalPre-uploaded asset IDs from /v1/assets
companyNamestringOptionalCompany name shown in CTA end card (max 100)
ctaDesignstringOptionalCTA design style. See /generate for valid values.
gradientColorstringOptionalHex color for gradient CTA (#FF5500)
editStylestringOptionalImage editing direction applied to every scene (max 1000)
musicAssetIdstringOptionalBackground music asset ID
musicVolumenumberOptionalBackground music volume (0.0-1.0, default 0.3). Applies to both video and radio ads.
logoAssetIdstringOptionalLogo overlay asset ID

Response

json
{
  "plan": {
    "voiceover_script": "Immerse yourself in pure sound...",
    "voiceover_segments": [
      { "text": "Immerse yourself in pure sound.", "emotion": "warm", "duration_hint": 3 }
    ],
    "scenes": [
      {
        "type": "product_showcase",
        "image_instruction": "Product hero shot on a marble slab",
        "animation_prompt": "slow push in",
        "source_image_index": 0,
        "duration_seconds": 6,
        "text_overlay": "Pure sound"
      }
    ],
    "music_mood": "upbeat electronic",
    "voice_style": "energetic and confident"
  },
  "estimatedCredits": 390,
  "breakdown": [
    { "operation": "image_composition", "credits": 19 },
    { "operation": "video_synthesis", "credits": 43 },
    { "operation": "voiceover", "credits": 5 },
    { "operation": "compositing", "credits": 9 }
  ],
  "jobCount": 12,
  "sceneCount": 5,
  "estimatedDurationSeconds": 30
}
POST/api/v1/preview-voiceDeducts 3 credits

Generate a TTS voice preview. Returns a temporary audio URL (1hr expiry) and exact duration. Costs 3 credits per preview.

Request Body

FieldTypeReqDescription
textstringRequiredText to synthesize (max 1500 chars)
voiceIdstringRequiredVoice ID from /v1/voices

Response

json
{
  "audioUrl": "https://...",
  "durationSeconds": 12.4
}

UGC Ads (AI Actors)

UGC ads are creator-style talking-head videos: a library AI actor (identity-locked, with their own cloned voice) presents your app, product, service, food, or apparel to camera — with optional b-roll, a floating product card, and phone-in-hand app demos. This is the same pipeline the dashboard assistant uses.

Workflow

1. GET /v1/actors — pick an avatar + background (each portrait has its own actorId). 2. POST /v1/ugc/plan — get the editable script and the exact price (nothing charged). 3. POST /v1/ugc/generate — pass the (optionally edited) plan plus quotedCredits to start. 4. Poll GET /v1/jobs/{versionId} every 10 seconds until completed — polling drives the pipeline.

GET/api/v1/actors

List the actor library: system avatars (available to every account) plus any custom actors on your organization. Avatars are grouped by identity with one portrait per background/setting — use the portrait's actorId in the UGC endpoints. Each avatar has a default cloned voice, so voiceId is optional.

Response

json
{
  "avatars": [
    {
      "avatarId": "jake",
      "name": "Jake",
      "gender": "male",
      "ageRange": "20s",
      "ethnicity": "white",
      "defaultVoiceId": "O4MibTBw7Z25BKBEb5N0",
      "portraits": [
        { "actorId": "a1b2c3d4-...", "setting": "bedroom", "portraitUrl": "https://..." },
        { "actorId": "e5f6a7b8-...", "setting": "car", "portraitUrl": "https://..." }
      ]
    }
  ]
}
POST/api/v1/ugc/planNo credits deducted

Generate the editable ad script (beats with dialogue, captions, b-roll prompts) and the exact credit price for the run. If you edit the returned plan, POST it back here as plan to re-price the edited script for free, then pass it to /ugc/generate with quotedCredits set to the latest estimatedCredits — edits that move a price bucket otherwise 409.

Request Body

FieldTypeReqDescription
descriptionstringRequiredWhat you're advertising and the angle (1-2000 chars)
ugcTypestringRequired"app" | "product" | "service" | "food" | "apparel" | "otherugc"
actorIdstringRequiredA portrait's actorId from /v1/actors (picks avatar + background)
voiceIdstringOptionalVoice ID. Defaults to the avatar's own cloned voice (recommended — keeps lip-sync + identity consistent)
durationSecondsnumberOptional10-60. Default 15 (30 when b-roll clips are attached). 10s is the cheapest "hook" SKU. Longer scripts move the talking-head price bucket — see Credits
layoutstringOptionalAd format: "talking_head" (default, pure talking head) | "handheld_demo" (talk → first-person phone-in-hand demo) | "holding_phone" (beta) | "phone_demo" (creator collapses to a corner bubble; your screens/recording play in a big phone mockup) | "app_bg_corner" (same family, screens/recording fill the frame) | "pointing_up" | "product_in_hand" | "wearing". Unknown or unreleased layouts are rejected with 400. Layouts are also validated against ugcType: talking_head and pointing_up fit every topic; handheld_demo/holding_phone/phone_demo/app_bg_corner are app-only; product_in_hand is for product/food/apparel/otherugc; wearing is for apparel/product/otherugc; service accepts only talking_head and pointing_up. A mismatch is rejected with 400 { code: "LAYOUT_TOPIC_MISMATCH" } naming the allowed layouts. Every layout except "talking_head" renders your product on screen and therefore requires imageAssetIds. NOTE: omitting layout is not identical to sending "talking_head" — with an image attached, the default composites the product into the talking shot; explicit "talking_head" never composites.
imageAssetIdsstring[]OptionalUp to 6 product/app IMAGE assets from /v1/assets. REQUIRED for every layout except "talking_head" and the screen layouts — the others composite, mock up or cut away to your image, so omitting it is rejected with 400 { code: "IMAGE_REQUIRED" } before any credits are deducted. Ids referencing audio/video assets are rejected with the same code. Screen layouts (app_bg_corner, phone_demo) cycle the images as full-frame screens and need at least 2 (3-5 look best) OR a screen-recording clip in brollAssetIds (a clip alone fully satisfies them — it plays as the screen content) — otherwise 400 { code: "MORE_IMAGES_REQUIRED" }. A plan whose beats carry no spoken dialogue at all is rejected with 400 { code: "SCRIPT_EMPTY" }, and an actor whose portrait is no longer available with 400 { code: "ACTOR_UNAVAILABLE" } — all before any charge
brollAssetIdsstring[]OptionalUp to 3 of your own VIDEO clips (upload via /v1/assets; non-video ids are rejected with 400 { code: "BROLL_NOT_VIDEO" }). How the FIRST clip is used depends on the layout: handheld_demo/holding_phone play it inside a phone mockup as the demo beat; app_bg_corner/phone_demo play it as the screen content; product_in_hand/talking_head cut away to it full-frame. pointing_up never uses clips. Clips beyond the first are currently unused.
includeBrollbooleanOptionalSet false for a pure talking-head ad with no generated b-roll beats. Default true. Ignored by corner-card/screen layouts (pointing_up, app_bg_corner, phone_demo), which never use b-roll — and by "handheld_demo", where the cutaway IS the layout's second beat, so false is ignored rather than silently producing a plain talking head under that layout's name. Want talking-head-only? Use layout "talking_head". Tip: with includeBroll true and NO image or clip attached, the cutaway is a text-to-video shot generated from the script — it will not show your actual product.
cornerCardPositionstringOptionalWhere the floating product card pops in: "top-left" | "top-right" (default) | "top-center" | "middle-left" | "middle-right" | "bottom-left" | "bottom-right" | "bottom-center". For pointing_up the card is always coerced to a TOP placement (the creator physically points at it); for product_in_hand the card only appears when a SECOND image is attached (imageAssetIds[1] — the first is composited into the creator's hand).
cornerCardTiltbooleanOptionalSlight polaroid tilt on the product card. Default true; false = straight
aspectRatiostringOptional"9:16" (default, TikTok/Reels) or "16:9" (YouTube)
qualitystringOptional"standard" (default) | "premium". Applies to generated b-roll: premium renders at higher fidelity and costs more credits.
companyNamestringOptionalBrand/product name for the script and CTA (max 100)
wearableKindstringOptionalApparel only — where the item is worn: "clothing" | "hat" | "glasses" | "jewelry" | "shoes" | "watch" | "bag". Strongly recommended for ugcType "apparel": it decides placement (hat on the head, shoes on the feet). Omitted = the model infers it.
ctaTextstringOptionalCTA end-card text, e.g. "Link in bio" (max 80)
ctaSubtextstringOptionalCTA banner subtext under the headline (max 80). Default "Link in bio"
ctaUrlstringOptionalDestination link — your App Store page or site (https, max 300). Rendered ON the video as a scannable QR code in the final seconds, so viewers can act on the ad. Also accepted by /ugc/plan

Response

json
{
  "plan": {
    "beats": [
      { "type": "talking_head", "dialogue": "I was today years old when...", "caption": "..." },
      { "type": "product_broll", "broll_prompt": "...", "caption": "..." },
      { "type": "cta", "dialogue": "...", "caption": "Link in bio" }
    ],
    "hook_variations": [ { "hook_dialogue": "..." } ]
  },
  "estimatedCredits": 90,
  "duration": 30,
  "engine": "lipsync"
}
POST/api/v1/ugc/generateDeducts API credits

Start the UGC generation. Takes the exact same fields as /ugc/plan plus the fields below. Omit plan to let the AI script it in one call. Returns a versionId — poll GET /v1/jobs/{versionId} exactly like a classic video ad. Credits are refunded automatically if the pipeline fails.

Additional Fields

FieldTypeReqDescription
planobjectOptionalThe (optionally edited) plan from /v1/ugc/plan. All text in a client-supplied plan is screened against our content policy — a violating plan is rejected with a 400. To re-quote an EDITED plan without charging, pass it back to /v1/ugc/plan (which also accepts plan) and use the fresh estimatedCredits as quotedCredits
idempotencyKeystringOptional8-128 chars, your own unique value per intended generation. Retrying a timed-out request with the SAME key returns the original run ({ versionId, replayed: true, status }) instead of charging and generating twice. If that original run FAILED, replays return it with status: "failed" — use a NEW key to generate again. Strongly recommended for SDK/queue callers with automatic retries
quotedCreditsnumberOptionalThe estimatedCredits you were shown. Required whenever a plan is submitted (400 without it). If script edits move the price above this, the request returns 409 PRICE_CHANGED with the new total instead of silently charging more
ctaAccentColorstringOptionalCTA banner accent color as a 6-digit hex, e.g. "#7C5CFF". Defaults to the house red. Also accepted by /ugc/plan

Response

json
{
  "projectId": "proj_abc123",
  "versionId": "ver_def456",   // Poll GET /v1/jobs/{versionId}
  "totalCredits": 90,
  "jobCount": 5,
  "duration": 30,
  "engine": "lipsync"
}

// 409 when an edited plan changed the price:
// { "error": "...", "code": "PRICE_CHANGED", "newTotal": 147 }

Full Example

bash
# 1. Pick an avatar
curl $ARS0N_API_BASE_URL/api/v1/actors \
  -H "Authorization: Bearer $ARS0N_API_KEY"

# 2. Script + price (nothing charged)
curl -X POST $ARS0N_API_BASE_URL/api/v1/ugc/plan \
  -H "Authorization: Bearer $ARS0N_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "FORGE pre-workout — clean energy, no crash, mixes instantly",
    "ugcType": "product",
    "actorId": "<actorId from /v1/actors>",
    "layout": "product_in_hand",
    "imageAssetIds": ["<assetId from /v1/assets>"],
    "durationSeconds": 30,
    "cornerCardPosition": "top-right",
    "ctaText": "Link in bio"
  }'
# → { "plan": {...}, "estimatedCredits": 90, ... }

# 3. Generate (same body + plan + quotedCredits)
curl -X POST $ARS0N_API_BASE_URL/api/v1/ugc/generate \
  -H "Authorization: Bearer $ARS0N_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ ...same fields..., "plan": <edited plan>, "quotedCredits": 97 }'
# → { "versionId": "ver_...", "totalCredits": 90, ... }

# 4. Poll until completed (polling drives the pipeline)
curl $ARS0N_API_BASE_URL/api/v1/jobs/ver_... \
  -H "Authorization: Bearer $ARS0N_API_KEY"
# → { "status": "completed", "videoUrl": "https://..." }
POST/api/v1/jobs/{jobId}/retry

Retry a failed job. Resets it to pending so it will be re-processed on the next poll. Only works on jobs with status: "failed". Each job has a configurable retry limit (default 2) — returns 400if exhausted. Retrying a job in a failed version re-deducts that job's credit cost plus the cost of its downstream jobs when the earlier attempt was refunded or was cancelled while already running (an in-flight cancellation is never refunded, so its re-run is a new charge), and returns 402 if your balance is insufficient.

json
{
  "job": {
    "id": "job_abc123",
    "type": "video_synthesis",
    "status": "pending",
    "retry_count": 2
  }
}
POST/api/v1/jobs/{jobId}/regenerateDeducts API credits

Regenerate a single completed scene. Resets the image edit, video generation, and compositing jobs for that scene. Only works on video_synthesis jobs with status: "completed". Much cheaper than a full re-generation.

json
{
  "cost": 71,                    // API credits deducted (image edit 19 + 4s video 43 + compositing 9)
  "jobsReset": [                 // Jobs that were reset to pending
    "nb_job_id",
    "video_job_id",
    "comp_job_id"
  ]
}
POST/api/v1/versions/{versionId}/editDeducts API credits (only for re-generated jobs)

Edit an existing completed version. Creates a new version with smart regeneration — only changed components are re-processed. Unchanged clips are reused at no cost.

Request Body

FieldTypeReqDescription
planobjectOptionalModified plan object. Only changed scenes and voiceover segments trigger re-generation. When provided, the plan must include voiceover_script, voiceover_segments ([{ text, emotion, duration_hint }]), scenes, music_mood, and voice_style.
customClipsobject[]OptionalCustom video clips: [{ position, asset_id, voiceover_text?, duration_seconds }]. Max 10 clips.
idempotencyKeystringOptionalIdempotency key (8-128 chars). Re-sending the same key returns the original edit instead of creating a duplicate version.
quotedCreditsnumberOptionalThe credit total you expect this edit to cost (from estimatedCredits of a prior identical edit, or your own calculation). When provided and the recomputed cost of the new jobs exceeds it, the edit is rejected with 409 PRICE_CHANGED and the new total instead of charging more than you were quoted.

Response

json
{
  "projectId": "proj_abc123",
  "versionId": "ver_new789",
  "reusedJobs": ["job_video_1", "job_video_3"],
  "newJobs": ["job_voiceover", "job_compositing"],
  "estimatedCredits": 13
}
GET/api/v1/video?id={assetId}

Retrieve a video, audio, or image asset. Returns a 302 redirect to a signed URL valid for 2 hours — it never streams bytes itself. The assetUrl from job details already points here.

Like every v1 endpoint, the request must carry your Authorization: Bearer header, so this URL cannot be used as a bare <video src> in a browser (it would 401). Call it from your backend and either follow the redirect there or pass the signed URL to your player. The signed URL is served by the storage host, which handles Rangerequests for seeking. Assets are scoped to your organization — you cannot access other orgs' assets.

GET/api/v1/projects

List your API-created projects with their latest version status. Paginated. Each project includes its most recent version with current status, output URLs, and credit cost.

Query Parameters

  • limit — 1-100 (default 20)
  • offset — default 0

Response

json
{
  "projects": [
    {
      "id": "proj_abc123",
      "name": "Product launch video ad",
      "thumbnailUrl": null,
      "createdAt": "2026-03-24T15:51:01Z",
      "latestVersion": {
        "id": "ver_def456",
        "status": "completed",       // "generating" | "completed" | "failed"
        "adFormat": "video",          // "video" or "radio"
        "videoUrl": "https://...",    // Set when video ad completes
        "audioUrl": null,             // Set when radio ad completes
        "totalCredits": 140
      }
    }
  ],
  "pagination": {
    "limit": 20,
    "offset": 0,
    "count": 5
  }
}
GET/api/v1/packages

List available API credit packages and their prices. Use the id to purchase in the dashboard.

json
{
  "packages": [
    { "id": "...", "name": "API Starter", "credits": 1100, "priceCents": 3900, "priceDisplay": "$39", "popular": false },
    { "id": "...", "name": "API Growth",  "credits": 3500, "priceCents": 11900, "priceDisplay": "$119", "popular": true },
    { "id": "...", "name": "API Scale",   "credits": 9000, "priceCents": 29900, "priceDisplay": "$299", "popular": false }
  ]
}
GET/api/v1/usage

Returns aggregate API credit usage for your organization. Excludes failed generations. Useful for billing reconciliation.

Query Parameters

FieldTypeReqDescription
fromstringOptionalISO 8601 start date. Default: 30 days ago.
tostringOptionalISO 8601 end date. Default: now.

Response

json
{
  "period": {
    "from": "2026-02-21T00:00:00Z",
    "to": "2026-03-23T00:00:00Z"
  },
  "tier": "starter",             // current API tier
  "cost_cents": 1312,            // total USD spent (in cents) during period
  "rate_per_credit": 0.0354545455, // USD per credit at your tier ($39 / 1,100 package rate)
  "credits": {
    "used": 370,
    "purchased": 2000,
    "refunded": 10
  },
  "generations": {
    "total": 5,
    "completed": 4,
    "generating": 1,
    "projects": 3
  }
}
POST/api/v1/estimate

Estimate the credit cost of a generation before committing. No credits are deducted.

Request Body

FieldTypeReqDescription
adFormatstringOptional"video" (default) or "radio". Radio ads cost significantly less.
imageCountnumberOptionalNumber of product images (1-10). Required for video ads. Not needed for radio.
durationnumberRequiredAd length: 15, 30, or 60 seconds
aspectRatiostringOptional"9:16" (default) or "16:9". Video ads only.
qualitystringOptional"standard" (default) | "premium". Premium renders at higher fidelity and costs more credits.
qualityLoopVariants1 | 2 | 3OptionalImage edit variants per scene. Default 2. Video ads only. Cost scales: 10 / 19 / 29 credits per scene for 1 / 2 / 3 variants.
hasMusicbooleanOptionalWhether background music will be included (radio ads only). Does not affect the estimated cost — audio compositing is always included.

Video Estimate Example

json
// Request: { "adFormat": "video", "imageCount": 3, "duration": 15 }
// Response:
{
  "adFormat": "video",
  "estimatedCredits": 199,
  "breakdown": [
    { "operation": "image_composition", "credits": 19 },
    { "operation": "image_composition", "credits": 19 },
    { "operation": "image_composition", "credits": 19 },
    { "operation": "video_synthesis", "credits": 43 },
    { "operation": "video_synthesis", "credits": 43 },
    { "operation": "video_synthesis", "credits": 43 },
    { "operation": "voiceover", "credits": 4 },
    { "operation": "compositing", "credits": 9 }
  ],
  "jobCount": 8,
  "sceneCount": 3
}

Radio Estimate Example

json
// Request: { "adFormat": "radio", "duration": 30 }
// Response:
{
  "adFormat": "radio",
  "estimatedCredits": 10,
  "breakdown": [
    { "operation": "voiceover", "credits": 4 },
    { "operation": "audio_compositing", "credits": 6 }
  ],
  "jobCount": 2,
  "sceneCount": 0
}
POST/api/v1/jobs/{versionId}/cancelPartial credit refund

Cancel an in-progress generation. Marks all pending and processing jobs as failed, and refunds credits for jobs that hadn't started yet. Only works on versions with status: "generating".

json
{
  "cancelled": true,
  "jobsCancelled": 4,
  "creditsRefunded": 72
}
DELETE/api/v1/projects/{projectId}

Delete a project and all its versions and jobs. Cannot delete a project with active ("generating") versions — cancel them first. The id may be either the projectId or a versionId from the generate response — a version id resolves to its parent project.

json
{ "deleted": true }
DELETE/api/v1/assets/{assetId}

Delete an uploaded asset. Removes the file from storage and the database record. Cannot delete assets referenced by active (pending/processing) generation jobs.

json
{ "deleted": true }

Credits & Pricing

API calls consume API credits — a separate balance from your dashboard credits. API access is included with the Pro plan; API credits can only be purchased on an active Pro subscription. Purchase packages in Settings or view them at /pricing.

A credit is a fixed unit of provider cost, so each operation is priced in proportion to what it actually consumes: a second of lip-sync is cheap, a second of cinematic video generation is not. Short ads are therefore substantially cheaper, and a balance goes further the shorter your ads are.

Check Your Balance

bash
curl $ARS0N_API_BASE_URL/api/v1/credits \
  -H "Authorization: Bearer $ARS0N_API_KEY"

# → {
#   "balance": 1100,
#   "tier": "starter",
#   "activeGenerations": 1,
#   "limits": { "maxConcurrentGenerations": 1, "rateLimitRpm": 60 }
# }

Estimate Before You Generate

Use POST /api/v1/estimate to preview the exact credit cost before committing. No credits are deducted. Treat this as the source of truth — the tables below are illustrative.

bash
# Cinematic video estimate
curl -X POST $ARS0N_API_BASE_URL/api/v1/estimate \
  -H "Authorization: Bearer $ARS0N_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "adFormat": "video", "imageCount": 3, "duration": 15 }'

# → { "adFormat": "video", "estimatedCredits": 199, "breakdown": [...], "jobCount": 8, "sceneCount": 3 }

# Radio estimate
curl -X POST $ARS0N_API_BASE_URL/api/v1/estimate \
  -H "Authorization: Bearer $ARS0N_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "adFormat": "radio", "duration": 30 }'

# → { "adFormat": "radio", "estimatedCredits": 10, "breakdown": [...], "jobCount": 2, "sceneCount": 0 }

Cost Per Operation

Credits are deducted upfront when you call /generate. The response tells you exactly what was charged.

StepCreditsPer
UGC talking head (10s) — dashboard only25Per ad — lip-synced actor speech
UGC talking head (15s)38Per ad
UGC talking head (30s)76Per ad
UGC talking head (60s)151Per ad
Image editing (1 variant)10Per scene
Image editing (2 variants)19Per scene (default — scored, best picked)
Image editing (3 variants)29Per scene (max quality)
Video generation — Fast43 / 65 / 86Per clip — 4s / 6s / 8s
Video generation — Premium115 / 173 / 230Per clip — 4s / 6s / 8s
Voiceover (video)4 / 5 / 10Per ad — 15s / 30s / 60s
Voiceover (radio)2 / 4 / 9Per ad — 15s / 30s / 60s
Video compositing9-27Scales with length: 30s=9 / 60s=15 / 90s=21 / 120s=27
Audio compositing6Per radio ad
AI creator portrait10One-time per custom avatar

UGC ad: talking_head[duration] + voiceover[duration] + compositing + Σ(b-roll) + Σ(product composites)

Cinematic video ad: Σ(editing[variants]) + Σ(video[model, clip_duration]) + voiceover[duration] + compositing

Radio ad: voiceover[duration] + audio compositing

The exact price always comes back from POST /api/v1/ugc/plan or POST /api/v1/estimate as estimatedCredits, and /ugc/generate accepts a quotedCredits guard that returns 409 PRICE_CHANGED rather than charging a price you did not agree to.

Worked Examples

AdBreakdownCredits
UGC 10s hook (dashboard only — the v1 API floors at 15s)25 talking head + 4 voiceover + 9 compositing38
UGC 15s ad38 + 4 + 951
UGC 30s ad76 + 5 + 990
UGC 15s + b-roll cutaway51 + 10 image edit + 86 generated 8s clip147
Cinematic 15s, 3 scenes3x19 editing + 3x43 video (4s clips) + 4 + 9199
Cinematic 30s, 5 scenes5x19 + video mix (2x43 + 3x65) + 5 + 9390
Cinematic 60s, 9 scenes9x19 + video mix (1x43 + 6x65 + 2x86) + 10 + 15801
Radio 15s / 30s / 60svoiceover + 6 audio compositing8 / 10 / 15

Cinematic scene counts are typical, not fixed — cost varies with quality and qualityLoopVariants. Use POST /api/v1/estimate for exact costs.

Voice Preview

Each voice preview via POST /api/v1/preview-voice costs 3 credits. This generates a real audio file so you can hear and verify the voiceover before committing to full generation.

Custom Clips

User-uploaded video clips inserted between AI-generated scenes have no generation cost — they skip image and video synthesis entirely. Only compositing costs apply. This is also the cheapest way to add b-roll to a UGC ad: supplying your own footage costs nothing, where a generated generated cutaway adds 96 credits.

Editing Versions

When editing via POST /versions/{id}/edit, only re-generated jobs are charged. Unchanged clips are reused at no cost. Typical edit cost: 13-15 credits (voiceover + recomposite).

Credit Expiry

Purchased credits expire 30 days after purchase — API and dashboard credits alike. Free signup credits are valid for 1 month. Spending always draws from the credits closest to expiring first, so purchased credits never lapse while longer-lived ones sit unused. Bonus credits granted by support never expire.

List Packages

bash
curl $ARS0N_API_BASE_URL/api/v1/packages \
  -H "Authorization: Bearer $ARS0N_API_KEY"

# → { "packages": [
#     { "name": "API Starter", "credits": 1100, "priceCents": 3900  },
#     { "name": "API Growth",  "credits": 3500, "priceCents": 11900 },
#     { "name": "API Scale",   "credits": 9000, "priceCents": 29900 }
# ] }

See full pricing details and plan comparison at /pricing →

Rate Limits & Concurrency

Two types of limits protect the system: rate limits (requests per minute) and concurrency limits (parallel video jobs). Both scale with your tier.

Tiers

TierRequests/minConcurrent generationsBest For
Free301Limits shown for reference — key creation requires Pro
Starter601Small apps & MVPs
Pro1202Production workloads
Enterprise3003High-volume pipelines

Rate Limit Details

  • Window type: Fixed 60-second window per API key (bucket resets each minute)
  • Per-key limits: Each API key has its own rate limit counter based on your tier
  • Per-org aggregate: Your organization is also limited to 3x your tier RPM across all keys combined (e.g. Starter: 180 total RPM across all keys)
  • Fail-closed: If the rate limiter is unavailable, requests are denied (not allowed)

Response Headers

Every API response includes rate limit headers. Use them to throttle your client and avoid hitting limits.

bash
# Headers included on every response:
X-RateLimit-Limit: 60          # Your max requests per window
X-RateLimit-Remaining: 42      # Requests left in this window
X-RateLimit-Reset: 1711234567  # Unix timestamp when window resets
X-Request-Id: a1b2c3d4-...    # Unique ID for debugging

Handling 429 — Rate Limited

Two scenarios return 429:

Too many requests

Wait until X-RateLimit-Reset before retrying.

json
{ "error": "Rate limit exceeded. Try again later." }

Too many concurrent generations

Wait for an active generation to finish. The response tells you the current count. One in-flight pipeline counts as one generation, regardless of how many jobs it spawns.

json
{
  "error": "Concurrency limit reached. Wait for active generations to complete.",
  "active": 3,
  "max": 3
}

Python retry with backoff

python
import time

def api_call_with_retry(fn, max_retries=3):
    for attempt in range(max_retries):
        resp = fn()
        if resp.status_code != 429:
            return resp

        reset_at = int(resp.headers.get("X-RateLimit-Reset", 0))
        wait = max(reset_at - time.time(), 1)
        print(f"Rate limited. Waiting {wait:.0f}s...")
        time.sleep(wait)

    raise Exception("Max retries exceeded")

Errors

All errors follow the same shape. Use the HTTP status code for control flow and the error field for user-facing messages.

json
{
  "error": "Insufficient credits: 100 required but only 45 available",
  "details": { ... },  // Present on validation errors (400)
  "code": "..."        // Present on 500s — a stable machine code, e.g. "provider_unavailable"
}

Status Codes

CodeWhenFix
200SuccessProcess the response
201Resource created (asset upload)Use the returned assetId
400Invalid request body or params — including cancelling a version whose status is not "generating"Check details for field errors
401Missing, invalid, or revoked keyCheck your Authorization header
402Insufficient API creditsPurchase more credits at /settings/billing
403Asset not owned by your org, or plan too lowVerify asset ownership or upgrade your plan
404Resource not foundCheck the version / project ID
409Conflict — the price changed for an edited UGC plan (PRICE_CHANGED), or a concurrent cancel already transitioned the version out of "generating" while your cancel was in flight (cancelling a version that was already in a non-generating state returns 400 instead)Re-confirm with the returned newTotal, or check the current status before retrying
429Rate or concurrency limit hitWait for X-RateLimit-Reset
500Server error. Carries a stable code (e.g. provider_unavailable) — branch on that, not on the message textRetry with backoff. Include X-Request-Id in support requests.
503Platform at capacity (returned by /v1/generate and /v1/ugc/generate)Retry with backoff

Insufficient Credits (402)

When your API credit balance is too low for the requested generation:

json
{
  "error": "Insufficient API credits",
  "required": 140,
  "available": 45,
  "message": "This generation requires 140 API credits but you only have 45. Purchase API credits at /settings/billing."
}

Error Codes

Machine-readable codes returned in the code field alongside error:

CodeStatusDescription
SCRIPT_TOO_LONG400Returned by the generate, plan, and edit endpoints (/v1/generate, /v1/ugc/plan, /v1/ugc/generate, /v1/versions/:id/edit) when a script exceeds the 60-second speech budget (~210 words, 2000 characters max). The script is rejected before any credits are charged — it is never silently truncated. The errormessage states the limit and your script's actual length; shorten the script and retry
PRICE_CHANGED409Returned by POST /v1/generate, POST /v1/ugc/generate, and POST /v1/versions/:id/edit when your edited plan changes the price from the quoted amount. The response body includes error, code: "PRICE_CHANGED", and newTotal — re-confirm with the new total to proceed

Validation Errors (400)

When request validation fails, the details field contains field-level errors:

json
{
  "error": "Validation failed",
  "details": {
    "fieldErrors": {
      "duration": ["Duration must be 15, 30, or 60"],
      "voiceId": ["Required"]
    },
    "formErrors": []
  }
}

Request IDs

Successful and expected-error responses include X-Request-Id. Log it — when something goes wrong, include it in support tickets for instant lookup. Authentication failures and unexpected internal errors may lack the header; include the timestamp and endpoint instead.

python
resp = requests.post(f"{BASE_URL}/generate", headers=headers, json=payload)
if not resp.ok:
    request_id = resp.headers.get("X-Request-Id")
    print(f"Failed: {resp.status_code} — Request ID: {request_id}")
    print(resp.json())

Full Examples

Complete end-to-end workflows: upload assets, find a voice, generate a video, and download the result.

Python — Full Workflow

Upload a product image, pick a voice, generate a video with all customization options, and poll until complete.

python
import requests, time, os

API_KEY = os.environ["ARS0N_API_KEY"]
BASE    = os.environ["ARS0N_API_BASE_URL"] + "/api/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}


# ── 1. Upload product image ──────────────────────────────────
with open("product.png", "rb") as f:
    resp = requests.post(
        f"{BASE}/assets",
        headers=HEADERS,
        files={"file": ("product.png", f, "image/png")},
        data={"category": "product_image"},
    )
    resp.raise_for_status()
    image_id = resp.json()["assetId"]
    print(f"Uploaded image: {image_id}")


# ── 2. Upload logo ───────────────────────────────────────────
with open("logo.png", "rb") as f:
    resp = requests.post(
        f"{BASE}/assets",
        headers=HEADERS,
        files={"file": ("logo.png", f, "image/png")},
        data={"category": "logo"},
    )
    resp.raise_for_status()
    logo_id = resp.json()["assetId"]


# ── 3. Upload background music ───────────────────────────────
with open("bg-music.mp3", "rb") as f:
    resp = requests.post(
        f"{BASE}/assets",
        headers=HEADERS,
        files={"file": ("bg-music.mp3", f, "audio/mpeg")},
        data={"category": "music"},
    )
    resp.raise_for_status()
    music_id = resp.json()["assetId"]


# ── 4. Find a voice ──────────────────────────────────────────
resp = requests.get(
    f"{BASE}/voices",
    headers=HEADERS,
    params={"gender": "female", "accent": "american"},
)
resp.raise_for_status()
voices = resp.json()["voices"]
voice_id = voices[0]["voice_id"]
print(f"Using voice: {voices[0]['name']} ({voice_id})")


# ── 5. Generate video with full options ───────────────────────
resp = requests.post(f"{BASE}/generate", headers={
    **HEADERS, "Content-Type": "application/json",
}, json={
    # Required
    "prompt": "Premium noise-canceling headphones. Sleek design, deep bass.",
    "imageAssetIds": [image_id],
    "duration": 30,
    "voiceId": voice_id,

    # Style
    "aspectRatio": "9:16",
    "ctaDesign": "gradient",
    "gradientColor": "#FF5500",
    "companyName": "SoundWave Audio",
    "logoAssetId": logo_id,
    "musicAssetId": music_id,

    # Creative direction
    "voiceStyle": "energetic and confident",
    "musicMood": "upbeat electronic",
    "tone": "premium luxury tech",
    "emotion": "excitement",
    "editStyle": "Product floating in mid-air with dramatic studio lighting, reflections on glossy surface, cinematic depth of field",

    # Pipeline tuning
    "qualityLoopVariants": 3,  # Max quality — 3 variants per scene (29 credits/scene vs 19 for default 2)
})
resp.raise_for_status()
data = resp.json()
version_id = data["versionId"]
print(f"Generation started: {version_id}")
print(f"Credits charged: {data['totalCredits']}")
print(f"Jobs queued: {data['jobCount']}")


# ── 6. Poll for completion ────────────────────────────────────
while True:
    resp = requests.get(f"{BASE}/jobs/{version_id}", headers=HEADERS)
    resp.raise_for_status()
    status = resp.json()

    pct = status["progress"]
    jobs = status["jobs"]
    print(f"  {status['status']} — {pct}% "
          f"({jobs['completed']}/{jobs['total']} jobs)")

    if status["status"] == "completed":
        print(f"\nVideo ready: {status['videoUrl']}")
        break
    elif status["status"] == "failed":
        print(f"\nFailed. Check job details:")
        for j in jobs["details"]:
            if j["status"] == "failed":
                print(f"  {j['type']}: {j['error_message']}")
        break

    time.sleep(10)

TypeScript — Full Workflow

Same workflow in Node.js with proper error handling and typed responses.

typescript
import fs from "fs";

const API_KEY = process.env.ARS0N_API_KEY!;
const BASE = process.env.ARS0N_API_BASE_URL + "/api/v1";
const headers = { Authorization: `Bearer ${API_KEY}` };


// ── Helper: upload a file ────────────────────────────────────
async function uploadAsset(
  filePath: string,
  category: string,
): Promise<string> {
  const file = fs.readFileSync(filePath);
  const form = new FormData();
  form.append("file", new Blob([file]), filePath.split("/").pop()!);
  form.append("category", category);

  const res = await fetch(`${BASE}/assets`, {
    method: "POST",
    headers,
    body: form,
  });
  if (!res.ok) throw new Error(`Upload failed: ${await res.text()}`);
  return (await res.json()).assetId;
}


// ── Helper: poll until done ──────────────────────────────────
async function pollUntilDone(versionId: string): Promise<{
  status: string;
  videoUrl: string | null;
}> {
  while (true) {
    const res = await fetch(`${BASE}/jobs/${versionId}`, { headers });
    const data = await res.json();
    console.log(`  ${data.status} — ${data.progress}%`);

    if (data.status === "completed" || data.status === "failed") {
      return data;
    }
    await new Promise((r) => setTimeout(r, 10_000));
  }
}


// ── Main ─────────────────────────────────────────────────────
async function main() {
  // 1. Upload assets
  const imageId = await uploadAsset("./product.png", "product_image");
  const logoId  = await uploadAsset("./logo.png", "logo");
  const musicId = await uploadAsset("./music.mp3", "music");
  console.log("Assets uploaded");

  // 2. Find a voice
  const voicesRes = await fetch(
    `${BASE}/voices?gender=male&q=narrator`,
    { headers },
  );
  const { voices } = await voicesRes.json();
  const voiceId = voices[0].voice_id;
  console.log(`Using voice: ${voices[0].name}`);

  // 3. Generate
  const genRes = await fetch(`${BASE}/generate`, {
    method: "POST",
    headers: { ...headers, "Content-Type": "application/json" },
    body: JSON.stringify({
      prompt: "Premium headphones — immersive sound, modern design",
      imageAssetIds: [imageId],
      duration: 15,
      voiceId,
      aspectRatio: "9:16",
      logoAssetId: logoId,
      musicAssetId: musicId,
      companyName: "SoundWave",
      ctaDesign: "bold",
      tone: "premium tech",
    }),
  });

  if (!genRes.ok) throw new Error(await genRes.text());
  const { versionId, totalCredits } = await genRes.json();
  console.log(`Started (${totalCredits} credits)`);

  // 4. Poll
  const result = await pollUntilDone(versionId);
  if (result.videoUrl) {
    console.log(`Video: ${result.videoUrl}`);
  }
}

main().catch(console.error);

Python — Radio Ad (Audio-Only)

Generate an audio-only radio ad — no images or video needed. Much faster and cheaper than video ads.

python
import requests, time, os

API_KEY = os.environ["ARS0N_API_KEY"]
BASE    = os.environ["ARS0N_API_BASE_URL"] + "/api/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}


# ── 1. Find a voice ──────────────────────────────────────────
resp = requests.get(
    f"{BASE}/voices",
    headers=HEADERS,
    params={"gender": "male", "accent": "american"},
)
resp.raise_for_status()
voice_id = resp.json()["voices"][0]["voice_id"]


# ── 2. (Optional) Upload background music ────────────────────
with open("bg-music.mp3", "rb") as f:
    resp = requests.post(
        f"{BASE}/assets",
        headers=HEADERS,
        files={"file": ("bg-music.mp3", f, "audio/mpeg")},
        data={"category": "music"},
    )
    resp.raise_for_status()
    music_id = resp.json()["assetId"]


# ── 3. Generate radio ad ─────────────────────────────────────
resp = requests.post(f"{BASE}/generate", headers={
    **HEADERS, "Content-Type": "application/json",
}, json={
    "adFormat": "radio",          # Audio-only — no images needed
    "prompt": "Premium noise-canceling headphones. Deep bass, sleek design.",
    "duration": 30,
    "voiceId": voice_id,
    "musicAssetId": music_id,     # Optional background music

    # Creative direction
    "voiceStyle": "warm and authoritative",
    "tone": "premium tech",
})
resp.raise_for_status()
data = resp.json()
version_id = data["versionId"]
print(f"Radio ad started: {version_id} ({data['totalCredits']} credits)")


# ── 4. Poll for completion ────────────────────────────────────
while True:
    resp = requests.get(f"{BASE}/jobs/{version_id}", headers=HEADERS)
    status = resp.json()
    print(f"  {status['status']} — {status['progress']}%")

    if status["status"] == "completed":
        print(f"\nAudio ready: {status['audioUrl']}")
        break
    elif status["status"] == "failed":
        print("\nFailed.")
        break

    time.sleep(5)  # Radio ads are faster — poll every 5s

TypeScript — Radio Ad (Audio-Only)

Generate a radio ad in TypeScript — same polling pattern, just fewer parameters and an audio result.

typescript
const API_KEY = process.env.ARS0N_API_KEY!;
const BASE = process.env.ARS0N_API_BASE_URL + "/api/v1";
const headers = { Authorization: `Bearer ${API_KEY}` };

// 1. Find a voice
const voicesRes = await fetch(`${BASE}/voices?gender=female`, { headers });
const { voices } = await voicesRes.json();
const voiceId = voices[0].voice_id;

// 2. Generate radio ad (no images needed)
const genRes = await fetch(`${BASE}/generate`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({
    adFormat: "radio",
    prompt: "Premium noise-canceling headphones — deep bass, sleek design",
    duration: 30,
    voiceId,
    tone: "premium tech",
  }),
});
const { versionId, totalCredits } = await genRes.json();
console.log(`Radio ad started (${totalCredits} credits)`);

// 3. Poll until done
while (true) {
  const res = await fetch(`${BASE}/jobs/${versionId}`, { headers });
  const data = await res.json();
  console.log(`  ${data.status} — ${data.progress}%`);

  if (data.status === "completed") {
    console.log(`Audio: ${data.audioUrl}`);
    break;
  } else if (data.status === "failed") {
    console.log("Failed");
    break;
  }
  await new Promise((r) => setTimeout(r, 5_000));
}

cURL — Quick Generate

Minimal example using image URLs (no pre-upload needed).

bash
# Generate with image URLs — server downloads them for you
curl -X POST $ARS0N_API_BASE_URL/api/v1/generate \
  -H "Authorization: Bearer $ARS0N_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Luxury skincare product — clean, minimal, elegant",
    "imageUrls": [
      "https://example.com/product-front.png",
      "https://example.com/product-side.png"
    ],
    "duration": 15,
    "voiceId": "pNInz6obpgDQGcFmaJgB",
    "aspectRatio": "16:9",
    "companyName": "Glow Labs",
    "ctaDesign": "minimal"
  }'

cURL — Quick Radio Ad

Generate an audio-only ad with a single command — no images needed.

bash
curl -X POST $ARS0N_API_BASE_URL/api/v1/generate \
  -H "Authorization: Bearer $ARS0N_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "adFormat": "radio",
    "prompt": "Organic cold-pressed juice — fresh, healthy, delicious",
    "duration": 15,
    "voiceId": "pNInz6obpgDQGcFmaJgB",
    "voiceStyle": "energetic and friendly",
    "tone": "health-conscious lifestyle"
  }'

cURL — Estimate Costs

Preview credit costs before committing. Works for both video and radio ads.

bash
# Video estimate
curl -X POST $ARS0N_API_BASE_URL/api/v1/estimate \
  -H "Authorization: Bearer $ARS0N_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "adFormat": "video", "imageCount": 3, "duration": 15 }'

# → { "adFormat": "video", "estimatedCredits": 199, "breakdown": [...], "jobCount": 8 }

# Radio estimate
curl -X POST $ARS0N_API_BASE_URL/api/v1/estimate \
  -H "Authorization: Bearer $ARS0N_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "adFormat": "radio", "duration": 30 }'

# → { "adFormat": "radio", "estimatedCredits": 10, "breakdown": [...], "jobCount": 2 }

Python — Estimate Costs

python
import requests, os

API_KEY = os.environ["ARS0N_API_KEY"]
BASE    = os.environ["ARS0N_API_BASE_URL"] + "/api/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}

# Video estimate
resp = requests.post(f"{BASE}/estimate", headers=HEADERS, json={
    "adFormat": "video",
    "imageCount": 3,
    "duration": 15,
})
resp.raise_for_status()
video_est = resp.json()
print(f"Video: {video_est['estimatedCredits']} credits, {video_est['jobCount']} jobs")

# Radio estimate
resp = requests.post(f"{BASE}/estimate", headers=HEADERS, json={
    "adFormat": "radio",
    "duration": 30,
})
resp.raise_for_status()
radio_est = resp.json()
print(f"Radio: {radio_est['estimatedCredits']} credits, {radio_est['jobCount']} jobs")

TypeScript — Estimate Costs

typescript
const API_KEY = process.env.ARS0N_API_KEY!;
const BASE = process.env.ARS0N_API_BASE_URL + "/api/v1";
const headers = {
  Authorization: `Bearer ${API_KEY}`,
  "Content-Type": "application/json",
};

// Video estimate
const videoRes = await fetch(`${BASE}/estimate`, {
  method: "POST",
  headers,
  body: JSON.stringify({ adFormat: "video", imageCount: 3, duration: 15 }),
});
const videoEst = await videoRes.json();
console.log(`Video: ${videoEst.estimatedCredits} credits, ${videoEst.jobCount} jobs`);

// Radio estimate
const radioRes = await fetch(`${BASE}/estimate`, {
  method: "POST",
  headers,
  body: JSON.stringify({ adFormat: "radio", duration: 30 }),
});
const radioEst = await radioRes.json();
console.log(`Radio: ${radioEst.estimatedCredits} credits, ${radioEst.jobCount} jobs`);

cURL — Upload Assets

Upload a product image

bash
curl -X POST $ARS0N_API_BASE_URL/api/v1/assets \
  -H "Authorization: Bearer $ARS0N_API_KEY" \
  -F "file=@product-photo.png" \
  -F "category=product_image"

# → { "assetId": "abc-123", "type": "image", "url": "https://..." }

Upload background music

bash
curl -X POST $ARS0N_API_BASE_URL/api/v1/assets \
  -H "Authorization: Bearer $ARS0N_API_KEY" \
  -F "file=@background.mp3" \
  -F "category=music"

# → { "assetId": "def-456", "type": "audio", "url": "https://..." }

Python — Two-Step Plan Flow

Generate a plan, edit the script, preview the voice, then generate with your modified plan.

python
import requests

BASE = "https://ars0n.ai"
headers = {"Authorization": "Bearer sk_live_YOUR_KEY"}

# 1. Generate plan
plan_res = requests.post(f"{BASE}/api/v1/plan", headers=headers, json={
    "prompt": "Premium skincare serum with vitamin C",
    "adFormat": "video",
    "duration": 30,
    "voiceId": "voice_id",
    "imageAssetIds": ["asset-1", "asset-2"]
})
plan_data = plan_res.json()

# 2. Edit the script
plan = plan_data["plan"]
plan["voiceover_segments"][0]["text"] = "Discover radiant skin."
plan["voiceover_script"] = " ".join(s["text"] for s in plan["voiceover_segments"])

# 3. Preview voice
preview = requests.post(f"{BASE}/api/v1/preview-voice", headers=headers, json={
    "text": plan["voiceover_script"],
    "voiceId": "voice_id"
})
print(f"Duration: {preview.json()['durationSeconds']}s")

# 4. Generate with modified plan. quotedCredits is required with a plan —
#    if your edits changed the price, the API responds 409 PRICE_CHANGED
#    with newTotal instead of charging more than you were quoted.
gen = requests.post(f"{BASE}/api/v1/generate", headers=headers, json={
    "prompt": "Premium skincare serum",
    "adFormat": "video",
    "duration": 30,
    "voiceId": "voice_id",
    "imageAssetIds": ["asset-1", "asset-2"],
    "plan": plan,
    "quotedCredits": plan_data["estimatedCredits"]
})
version_id = gen.json()["versionId"]

TypeScript — Two-Step Plan Flow

Same two-step flow in TypeScript, including a custom clip insertion.

typescript
const BASE = "https://ars0n.ai";
const headers = { Authorization: "Bearer sk_live_YOUR_KEY", "Content-Type": "application/json" };

// 1. Generate plan
const planRes = await fetch(`${BASE}/api/v1/plan`, {
  method: "POST", headers,
  body: JSON.stringify({ prompt: "Premium skincare", adFormat: "video", duration: 30, voiceId: "voice_id", imageAssetIds: ["asset-1"] })
});
const { plan, estimatedCredits } = await planRes.json();

// 2. Edit script
plan.voiceover_segments[0].text = "Discover radiant skin.";
plan.voiceover_script = plan.voiceover_segments.map(s => s.text).join(" ");

// 3. Generate with modified plan + custom clip. quotedCredits is required
//    with a plan — a 409 PRICE_CHANGED response means your edits changed
//    the price; re-confirm with the returned newTotal.
const genRes = await fetch(`${BASE}/api/v1/generate`, {
  method: "POST", headers,
  body: JSON.stringify({
    prompt: "Premium skincare", adFormat: "video", duration: 30,
    voiceId: "voice_id", imageAssetIds: ["asset-1"],
    plan,
    quotedCredits: estimatedCredits,
    customClips: [{ position: 1, asset_id: "clip-asset-id", voiceover_text: "See results.", duration_seconds: 8 }]
  })
});

cURL — Browse Voices

bash
# List all voices
curl $ARS0N_API_BASE_URL/api/v1/voices \
  -H "Authorization: Bearer $ARS0N_API_KEY"

# Filter by gender + accent
curl "$ARS0N_API_BASE_URL/api/v1/voices?gender=female&accent=british" \
  -H "Authorization: Bearer $ARS0N_API_KEY"

# Search by name or description
curl "$ARS0N_API_BASE_URL/api/v1/voices?q=narrator" \
  -H "Authorization: Bearer $ARS0N_API_KEY"

# Each voice has a voice_id — use it in the generate call:
# → { "voices": [{ "voice_id": "abc...", "name": "Charlotte", ... }] }