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
Create an account
Sign up at /signup, then log in to the dashboard
Create an API key
Dashboard → Settings → API → Create Key
Set env variables
ARS0N_API_BASE_URL + ARS0N_API_KEY
Make your first call
POST to /api/v1/generate
Environment Setup
# Add to your .env file:
ARS0N_API_BASE_URL=https://app.ars0n.ai
ARS0N_API_KEY=sk_live_... # from Dashboard → Settings → APIThe Pipeline
The API supports two ad formats. Each follows its own pipeline — you just submit and poll.
Video Ad Pipeline
Radio Ad Pipeline (audio-only)
Radio ads are audio-only — no images or video needed. Much faster and cheaper (8-15 credits vs ~265+ for cinematic video).
Quick Start
Generate a video in 4 API calls:
Step 1 — Check your balance
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
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
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": 100, "jobCount": 5 }Step 4 — Poll until done (required)
# 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)
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": 230 }Step 2 — Preview the voice (optional)
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 30s). Clips are auto-transcoded to 720p with audio stripped. Use the returned assetId in customClips.
Step 4 — Generate with your modified plan
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": 434, "jobCount": 7 }Step 5 — Poll until done
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_...)
# 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>"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())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
Go to Settings → API
Click "Create Key" and name it (e.g. "Production", "Staging")
Copy the key immediately
The full key is shown only once. We store a SHA-256 hash — the plain key cannot be recovered.
Store as an environment variable
ARS0N_API_KEY=sk_live_... — never hardcode keys in source.
Key Properties
| Format | sk_live_ + 64 hex chars |
| Scope | Organization-level — all members share keys |
| Limit | Unlimited keys per org (use one per environment) |
| Revoke | Instant — 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).
/api/v1/generateDeducts API creditsStart a video or radio ad generation pipeline. Provide a prompt, a voice, and optional settings. Video ads also require product images.
Request Body
| Field | Type | Req | Description |
|---|---|---|---|
| adFormat | string | Optional | "video" (default) or "radio". Radio ads are audio-only — no images or video needed. |
| prompt | string | Required | Creative prompt describing your ad (1-2000 chars) |
| imageUrls | string[] | * | 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. |
| imageAssetIds | string[] | * | Pre-uploaded asset IDs from /v1/assets. Same limits as imageUrls. Not needed for radio ads. |
| duration | number | Required | Ad length: 15, 30, or 60 seconds (applies to both video and radio) |
| voiceId | string | Required | Voice ID from /v1/voices |
| aspectRatio | string | Optional | "9:16" (default) or "16:9" |
| quality | string | Optional | "standard" (default) | "premium". Premium renders at higher fidelity and costs more credits. |
| logoAssetId | string | Optional | Logo overlay — upload via /v1/assets first. Video ads only. |
| musicAssetId | string | Optional | Background music — upload MP3/WAV via /v1/assets. For radio ads, mixed with voiceover during audio compositing. |
| musicVolume | number | Optional | Background music volume (0.0-1.0, default 0.3). Applied during compositing for both video and radio. |
| companyName | string | Optional | Company name shown in CTA end card (max 100) |
| ctaDesign | string | Optional | "minimal" | "bold" | "gradient" | "split" | "glass" | "neon" | "editorial" | "brutalist" |
| gradientColor | string | Optional | Hex color for gradient CTA (#FF5500) |
| voiceStyle | string | Optional | Hint for AI voice direction (max 200). Applies to both video and radio ads. |
| musicMood | string | Optional | Hint for AI music selection (max 200). Applies to both video and radio ads. |
| tone | string | Optional | Creative tone — "luxury", "playful", "corporate" (max 200). Applies to both video and radio ads. |
| editStyle | string | Optional | Image 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" |
| emotion | string | Optional | Emotion style for animation and voiceover (max 200). Applies to both video and radio ads. |
| qualityLoopVariants | 1 | 2 | 3 | Optional | Image edit variants per scene. Default 2. Use 3 for max quality. Cost per scene scales: 5 / 10 / 15 credits for 1 / 2 / 3 variants. |
| plan | object | Optional | Pre-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. Backwards compatible — omit to let the AI plan automatically. |
| customClips | object[] | Optional | Custom 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
{
"projectId": "proj_abc123",
"versionId": "ver_def456", // Use this to poll status
"totalCredits": 115,
"jobCount": 6
}/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 you can use directly in your app.
{
"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": 115,
"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": []
},
{
"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.
/api/v1/creditsCheck your API credit balance, current tier, and active generation count.
Partner accounts with unlimited API credits receive balance: null and unlimited: true.
{
"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
}
}/api/v1/assetsUpload images or audio as multipart form data. Use the returned assetId in generate calls.
Images
PNG, JPEG, WebP — max 10MB
Audio
MP3, WAV, OGG, AAC, M4A — max 15MB
Video
MP4, WebM, MOV — max 100MB
Form Fields
filerequired — The file to uploadcategoryoptional — "product_image" | "logo" | "music" (inferred from MIME type if omitted)
{
"assetId": "asset_abc123", // Use in imageAssetIds, logoAssetId, etc.
"type": "image", // "image", "audio", or "video"
"publicUrl": "https://..."
}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: 30 seconds. Maximum file size: 100MB.
/api/v1/voicesBrowse 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
{
"voices": [
{
"voice_id": "pNInz6obpgDQGcFmaJgB",
"name": "Adam",
"gender": "male",
"accent": "american",
"age": "middle_aged",
"description": "Deep, warm, authoritative voice",
"preview_url": "/api/voice-sample?voiceId=pNInz6obpgDQGcFmaJgB",
"use_case": "narration",
"category": "premade"
}
]
}/api/v1/planGenerate a creative plan without starting the pipeline. No credits deducted. Returns the plan, cost estimate, and breakdown.
Request Body
| Field | Type | Req | Description |
|---|---|---|---|
| prompt | string | Required | Creative prompt describing your ad (1-2000 chars) |
| adFormat | string | Optional | "video" (default) or "radio" |
| duration | number | Required | Ad length: 15, 30, or 60 seconds |
| voiceId | string | Required | Voice ID from /v1/voices |
| voiceStyle | string | Optional | Voice delivery style hint for the script (max 100 chars) |
| musicMood | string | Optional | Background music mood hint (max 100 chars) |
| tone | string | Optional | Overall ad tone (max 100 chars) |
| emotion | string | Optional | Emotional register for the narration (max 100 chars) |
| qualityLoopVariants | 1 | 2 | 3 | Optional | Image edit variants per scene. Default 2. Cost scales 5 / 10 / 15 credits per scene. |
| aspectRatio | string | Optional | "9:16" (default) or "16:9" |
| quality | string | Optional | "standard" (default) | "premium". Premium renders at higher fidelity and costs more credits. |
| imageAssetIds | string[] | Optional | Pre-uploaded asset IDs from /v1/assets |
| companyName | string | Optional | Company name shown in CTA end card (max 100) |
| ctaDesign | string | Optional | CTA design style. See /generate for valid values. |
| gradientColor | string | Optional | Hex color for gradient CTA (#FF5500) |
| editStyle | string | Optional | Image editing direction applied to every scene (max 1000) |
| musicAssetId | string | Optional | Background music asset ID |
| musicVolume | number | Optional | Background music volume (0.0-1.0, default 0.3) |
| logoAssetId | string | Optional | Logo overlay asset ID |
Response
{
"plan": {
"voiceover_script": "Immerse yourself in pure sound...",
"voiceover_segments": [
{ "text": "Immerse yourself in pure sound.", "scene_index": 0 }
],
"scenes": [
{ "scene_index": 0, "description": "Product hero shot", "duration_seconds": 6 }
],
"music_mood": "upbeat electronic",
"voice_style": "energetic and confident"
},
"estimatedCredits": 434,
"breakdown": [
{ "operation": "image_composition", "credits": 19 },
{ "operation": "video_generation", "credits": 65 },
{ "operation": "voiceover", "credits": 5 },
{ "operation": "compositing", "credits": 9 }
],
"jobCount": 8,
"sceneCount": 5,
"estimatedDurationSeconds": 30
}/api/v1/preview-voiceDeducts 3 creditsGenerate a TTS voice preview. Returns a temporary audio URL (1hr expiry) and exact duration. Costs 3 credits per preview.
Request Body
| Field | Type | Req | Description |
|---|---|---|---|
| text | string | Required | Text to synthesize (max 5000 chars) |
| voiceId | string | Required | Voice ID from /v1/voices |
Response
{
"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.
/api/v1/actorsList 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
{
"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://..." }
]
}
]
}/api/v1/ugc/planNo credits deductedGenerate the editable ad script (beats with dialogue, captions, b-roll prompts) and the exact credit price for the run. Edit the returned plan, then pass it to /ugc/generate with quotedCredits set to estimatedCredits.
Request Body
| Field | Type | Req | Description |
|---|---|---|---|
| description | string | Required | What you're advertising and the angle (1-2000 chars) |
| ugcType | string | Required | "app" | "product" | "service" | "food" | "apparel" | "otherugc" |
| actorId | string | Required | A portrait's actorId from /v1/actors (picks avatar + background) |
| voiceId | string | Optional | Voice ID. Defaults to the avatar's own cloned voice (recommended — keeps lip-sync + identity consistent) |
| durationSeconds | number | Optional | 15-60. Default 15 (30 when b-roll clips are attached). Longer scripts move the talking-head price bucket — see Credits |
| layout | string | Optional | Ad format: "talking_head" (default, pure talking head) | "handheld_demo" (talk → first-person phone-in-hand demo; app only) | "holding_phone" | "phone_demo" (creator collapses to a corner bubble over full-frame app screens) | "app_bg_corner" (same family, background-first) | "pointing_up" | "product_in_hand" | "wearing". Unknown or unreleased layouts are rejected with 400. |
| imageAssetIds | string[] | Optional | Up to 6 product/app images from /v1/assets. Required in practice for layouts that show the product (corner card, in-hand, wearing, app demos) |
| brollAssetIds | string[] | Optional | Up to 3 of your own video clips (upload via /v1/assets) cut in as b-roll |
| includeBroll | boolean | Optional | Set false for a pure talking-head ad with no generated b-roll beats. Default true (ignored by corner-card layouts, which never use b-roll) |
| cornerCardPosition | string | Optional | Where the floating product card pops in (pointing_up / product_in_hand layouts): "top-left" | "top-right" (default) | "top-center" | "middle-left" | "middle-right" | "bottom-left" | "bottom-right" | "bottom-center" |
| cornerCardTilt | boolean | Optional | Slight polaroid tilt on the product card. Default true; false = straight |
| aspectRatio | string | Optional | "9:16" (default, TikTok/Reels) or "16:9" (YouTube) |
| quality | string | Optional | "standard" (default) | "premium". Applies to generated b-roll: premium renders at higher fidelity and costs more credits. |
| companyName | string | Optional | Brand/product name for the script and CTA (max 100) |
| wearableKind | string | Optional | Apparel 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. |
| ctaText | string | Optional | CTA end-card text, e.g. "Link in bio" (max 80) |
| ctaSubtext | string | Optional | CTA banner subtext under the headline (max 80). Default "Link in bio" |
| ctaAccentColor | string | Optional | CTA banner accent color as a 6-digit hex, e.g. "#7C5CFF". Defaults to the house red |
Response
{
"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"
}/api/v1/ugc/generateDeducts API creditsStart the UGC generation. Takes the exact same fields as /ugc/plan plus the two 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
| Field | Type | Req | Description |
|---|---|---|---|
| plan | object | Optional | The (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 |
| quotedCredits | number | Optional | The estimatedCredits you were shown. If script edits move the price above this, the request returns 409 PRICE_CHANGED with the new total instead of silently charging more |
Response
{
"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
# 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://..." }/api/v1/jobs/{jobId}/retryRetry 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 400 if exhausted. No additional credits are charged for retries.
{
"job": {
"id": "job_abc123",
"type": "video_synthesis",
"status": "pending",
"retry_count": 2
}
}/api/v1/jobs/{jobId}/regenerateDeducts API creditsRegenerate a single completed scene. Resets the image edit, video generation, and compositing jobs for that scene. Only works on video_generation jobs with status: "completed". Much cheaper than a full re-generation.
{
"cost": 47, // API credits deducted
"jobsReset": [ // Jobs that were reset to pending
"nb_job_id",
"video_job_id",
"comp_job_id"
]
}/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 Unchanged clips are reused at no cost.
Request Body
| Field | Type | Req | Description |
|---|---|---|---|
| plan | object | Optional | Modified plan object. Only changed scenes and voiceover segments trigger re-generation. |
| customClips | object[] | Optional | Custom video clips: [{ position, asset_id, voiceover_text?, duration_seconds }]. Max 10 clips. |
Response
{
"projectId": "proj_abc123",
"versionId": "ver_new789",
"reusedJobs": ["job_video_1", "job_video_3"],
"newJobs": ["job_voiceover", "job_compositing"],
"estimatedCredits": 13
}/api/v1/video?id={assetId}Retrieve a video, audio, or image asset. Returns a 302 redirect to a signed URL valid for 2 hours. Use the assetUrl from job details directly — it already points here.
Supports Rangeheaders for video seeking. Assets are scoped to your organization — you cannot access other orgs' assets.
/api/v1/projectsList 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
{
"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
}
}/api/v1/packagesList available API credit packages and their prices. Use the id to purchase in the dashboard.
{
"packages": [
{ "id": "...", "name": "API Starter", "credits": 500, "priceCents": 3900, "priceDisplay": "$39", "popular": false },
{ "id": "...", "name": "API Growth", "credits": 2000, "priceCents": 11900, "priceDisplay": "$119", "popular": true },
{ "id": "...", "name": "API Scale", "credits": 5500, "priceCents": 29900, "priceDisplay": "$299", "popular": false }
]
}/api/v1/usageReturns aggregate API credit usage for your organization. Excludes failed generations. Useful for billing reconciliation.
Query Parameters
| Field | Type | Req | Description |
|---|---|---|---|
| from | string | Optional | ISO 8601 start date. Default: 30 days ago. |
| to | string | Optional | ISO 8601 end date. Default: now. |
Response
{
"period": {
"from": "2026-02-21T00:00:00Z",
"to": "2026-03-23T00:00:00Z"
},
"tier": "starter", // current API tier
"cost_cents": 11900, // total USD spent (in cents) during period
"credits": {
"used": 370,
"purchased": 2000,
"refunded": 10
},
"generations": {
"total": 5,
"completed": 4,
"generating": 1,
"projects": 3
}
}/api/v1/estimateEstimate the credit cost of a generation before committing. No credits are deducted.
Request Body
| Field | Type | Req | Description |
|---|---|---|---|
| adFormat | string | Optional | "video" (default) or "radio". Radio ads cost significantly less. |
| imageCount | number | Optional | Number of product images (1-10). Required for video ads. Not needed for radio. |
| duration | number | Required | Ad length: 15, 30, or 60 seconds |
| aspectRatio | string | Optional | "9:16" (default) or "16:9". Video ads only. |
| quality | string | Optional | "standard" (default) | "premium". Premium renders at higher fidelity and costs more credits. |
| qualityLoopVariants | 1 | 2 | 3 | Optional | Image edit variants per scene. Default 2. Video ads only. Cost scales: 5 / 10 / 15 credits per scene for 1 / 2 / 3 variants. |
| hasMusic | boolean | Optional | Whether background music will be included (radio ads only). Does not affect the estimated cost — audio compositing is always included. |
Video Estimate Example
// Request: { "adFormat": "video", "imageCount": 3, "duration": 15 }
// Response:
{
"adFormat": "video",
"estimatedCredits": 265,
"breakdown": [
{ "operation": "image_composition", "credits": 19 },
{ "operation": "image_composition", "credits": 19 },
{ "operation": "image_composition", "credits": 19 },
{ "operation": "video_generation", "credits": 65 },
{ "operation": "video_generation", "credits": 65 },
{ "operation": "video_generation", "credits": 65 },
{ "operation": "voiceover", "credits": 4 },
{ "operation": "compositing", "credits": 9 }
],
"jobCount": 8,
"sceneCount": 3
}Radio Estimate Example
// Request: { "adFormat": "radio", "duration": 30 }
// Response:
{
"adFormat": "radio",
"estimatedCredits": 10,
"breakdown": [
{ "operation": "voiceover", "credits": 4 },
{ "operation": "audio_compositing", "credits": 6 }
],
"jobCount": 2,
"sceneCount": 0
}/api/v1/jobs/{versionId}/cancelPartial credit refundCancel 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".
{
"cancelled": true,
"jobsCancelled": 4,
"creditsRefunded": 72
}/api/v1/projects/{projectId}Delete a project and all its versions and jobs. Cannot delete a project with active ("generating") versions — cancel them first.
{ "deleted": true }/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.
{ "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
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.
# 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": 265, "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.
| Step | Credits | Per |
|---|---|---|
| UGC talking head (10s) | 25 | Per ad — lip-synced actor speech |
| UGC talking head (15s) | 38 | Per ad |
| UGC talking head (30s) | 76 | Per ad |
| UGC talking head (60s) | 151 | Per ad |
| Image editing (1 variant) | 10 | Per scene |
| Image editing (2 variants) | 19 | Per scene (default — scored, best picked) |
| Image editing (3 variants) | 29 | Per scene (max quality) |
| Video generation — Fast | 43 / 65 / 86 | Per clip — 4s / 6s / 8s |
| Video generation — Premium | 115 / 173 / 230 | Per clip — 4s / 6s / 8s |
| Voiceover (video) | 4 / 5 / 10 | Per ad — 15s / 30s / 60s |
| Voiceover (radio) | 2 / 4 / 9 | Per ad — 15s / 30s / 60s |
| Video compositing | 9 | Per video ad |
| Audio compositing | 6 | Per radio ad |
| AI creator portrait | 10 | One-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
| Ad | Breakdown | Credits |
|---|---|---|
| UGC 10s hook | 25 talking head + 4 voiceover + 9 compositing | 38 |
| UGC 15s ad | 38 + 4 + 9 | 51 |
| UGC 30s ad | 76 + 5 + 9 | 90 |
| UGC 15s + Veo b-roll | 51 + 10 image edit + 86 Veo 8s | 147 |
| Cinematic 15s, 3 scenes | 3x19 editing + 3x65 video + 4 + 9 | 265 |
| Cinematic 30s, 5 scenes | 5x19 + 5x65 + 5 + 9 | 434 |
| Cinematic 60s, 9 scenes | 9x19 + 9x65 + 10 + 9 | 775 |
| Radio 15s / 30s / 60s | voiceover + 6 audio compositing | 8 / 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 Veo 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
API credits expire 12 months after purchase — they are a prepaid balance, not a monthly allowance. Dashboard credits, which arrive with each subscription renewal, expire 30 days from grant. Bonus credits granted by support never expire.
List Packages
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
| Tier | Requests/min | Concurrent generations | Best For |
|---|---|---|---|
| Free | 30 | 1 | Testing & prototyping |
| Starter | 60 | 1 | Small apps & MVPs |
| Pro | 120 | 2 | Production workloads |
| Enterprise | 300 | 3 | High-volume pipelines |
Rate Limit Details
- Window type: Sliding window (60-second rolling window per API key)
- 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.
# 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 debuggingHandling 429 — Rate Limited
Two scenarios return 429:
Too many requests
Wait until X-RateLimit-Reset before retrying.
{ "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.
{
"error": "Concurrency limit reached. Wait for active generations to complete.",
"active": 3,
"max": 3
}Python retry with backoff
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.
{
"error": "Insufficient credits: 100 required but only 45 available",
"details": { ... } // Present on validation errors (400)
}Status Codes
| Code | When | Fix |
|---|---|---|
| 200 | Success | Process the response |
| 201 | Resource created (asset upload) | Use the returned assetId |
| 400 | Invalid request body or params | Check details for field errors |
| 401 | Missing, invalid, or revoked key | Check your Authorization header |
| 402 | Insufficient API credits | Purchase more credits at /settings/billing |
| 403 | Asset not owned by your org, or plan too low | Verify asset ownership or upgrade your plan |
| 404 | Resource not found | Check the version / project ID |
| 409 | Conflict (e.g., cancelling a version that is no longer generating) | Check the current status before retrying |
| 429 | Rate or concurrency limit hit | Wait for X-RateLimit-Reset |
| 500 | Server error | Retry with backoff. Include X-Request-Id in support requests. |
Insufficient Credits (402)
When your API credit balance is too low for the requested generation:
{
"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
Specific error codes returned in the error field for common failure scenarios:
| Code | Status | Description |
|---|---|---|
| PLAN_EXPIRED | 400 | Plan submitted to generate references stale or missing asset IDs |
| CLIP_TOO_LONG | 400 | Uploaded video clip exceeds 30-second maximum duration |
| TOO_MANY_CLIPS | 400 | More than 10 custom clips in a single video |
| CLIP_FORMAT_UNSUPPORTED | 400 | Video format not supported. Use MP4, WebM, or MOV |
| CLIP_TOO_LARGE | 400 | Video file exceeds 100MB maximum size |
| VERSION_NOT_EDITABLE | 409 | Cannot edit a version that is still generating |
Validation Errors (400)
When request validation fails, the details field contains field-level errors:
{
"error": "Validation failed",
"details": {
"fieldErrors": {
"duration": ["Duration must be 15, 30, or 60"],
"voiceId": ["Required"]
},
"formErrors": []
}
}Request IDs
Every response includes X-Request-Id. Log it — when something goes wrong, include it in support tickets for instant lookup.
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.
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 (15 credits/scene vs 10 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.
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.
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 5sTypeScript — Radio Ad (Audio-Only)
Generate a radio ad in TypeScript — same polling pattern, just fewer parameters and an audio result.
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).
# 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.
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.
# 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": 140, "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": 5, "breakdown": [...], "jobCount": 2 }Python — Estimate Costs
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
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
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", "publicUrl": "https://..." }Upload background music
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", "publicUrl": "https://..." }Python — Two-Step Plan Flow
Generate a plan, edit the script, preview the voice, then generate with your modified plan.
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
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
})
version_id = gen.json()["versionId"]TypeScript — Two-Step Plan Flow
Same two-step flow in TypeScript, including a custom clip insertion.
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 } = 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
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,
customClips: [{ position: 1, asset_id: "clip-asset-id", voiceover_text: "See results.", duration_seconds: 8 }]
})
});cURL — Browse Voices
# 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", ... }] }