ArtCraft API Tutorial: Generate Video with the Omni API
The Omni API lets you call ArtCraft's generation models from your own code with an API key. Here is the complete flow, from key to finished video.
By ArtCraft Tips Editorial TeamLast updated 6 min read
ArtCraft is best known as a desktop studio, but it also has a developer API. The Omni API gives you programmatic access to the same generation models you use in the app, authenticated with an API key instead of a browser session. This guide follows the official Omni API documentation and walks through a complete video generation.
API access is gated
API keys are enabled per account by the ArtCraft team. If you don't see an API Keys section in your account settings, you need to ask them for access first.
Base URL and authentication
| Setting | Value |
|---|---|
| Production base URL | https://api.storyteller.ai |
| Auth header | Authorization: Bearer artcraft_api_… |
| Cookies | Not used; any session cookies are ignored |
The API lives on the storyteller.ai domain, not getartcraft.com. The official docs call this out explicitly, so it is not a mistake in your setup.
Step 1: Create an API key
Open the web app
Sign in at app.getartcraft.com.
Go to Account → Settings → API Keys
This section only appears if API access is enabled for your account.
Create a key and store it safely
The secret is shown once. Save it in a password manager or your server's secret store, never in source control.
A valid key starts with artcraft_api_ followed by 40 lowercase characters, for 53 characters in total. The API accepts Bearer, Key, or a bare key in the Authorization header; Bearer is recommended.
Step 2: Request a video
Send a POST to /v1/omni_api/generate/video. The example below animates a starting image with Seedance 2.0:
curl -s -X POST https://api.storyteller.ai/v1/omni_api/generate/video \
-H "Authorization: Bearer $ARTCRAFT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "seedance_2p0",
"prompt": "A puffin turns its head on a windy cliff edge as morning light catches its beak.",
"duration_seconds": 6,
"aspect_ratio": "wide_sixteen_by_nine",
"idempotency_token": "'"$(uuidgen)"'",
"start_frame_image_url": "https://example.com/puffin.jpg"
}'
The response contains a job token you use to fetch the result:
{
"success": true,
"inference_job_token": "jinf_xxxxxxxxxxxxxxxxxxxxxxxxx",
"all_job_tokens": ["jinf_xxxxxxxxxxxxxxxxxxxxxxxxx"]
}
Request fields
| Field | Required | Notes |
|---|---|---|
model | Yes | Model identifier, for example seedance_2p0 |
idempotency_token | Yes | A fresh UUID for every request; reusing one is rejected as a duplicate |
prompt | No | Text prompt |
duration_seconds | No | Clip length, for example 6 |
aspect_ratio | No | wide_sixteen_by_nine, tall_nine_by_sixteen, or square |
resolution | No | For example seven_twenty_p or ten_eighty_p |
negative_prompt | No | What to avoid |
generate_audio | No | Boolean |
Passing reference media by URL
You don't have to upload files first. The server downloads each URL, saves it to your account, and runs the generation with it:
| Field | Type | Accepted formats |
|---|---|---|
start_frame_image_url | URL | JPEG, PNG, GIF, WebP |
end_frame_image_url | URL | JPEG, PNG, GIF, WebP |
reference_image_urls | Array of URLs | JPEG, PNG, GIF, WebP |
reference_video_urls | Array of URLs | MP4 only |
reference_audio_urls | Array of URLs | WAV, MP3, AAC, OGG, FLAC, and more |
Rules to keep in mind:
- URLs must start with
http://orhttps://and be reachable by the server. Up to 10 redirects are followed, so CDN links work. - File types are detected from the downloaded bytes, not the extension.
- Each URL field has a media-token equivalent (for files already on your account), such as
reference_image_media_tokens. You can send one or the other for each input, never both.
Step 3: Poll for the result
Generation is asynchronous. Poll GET /v1/omni_api/job_status/job/{token} until the job finishes. To check many jobs at once, use GET /v1/omni_api/job_status/batch?tokens=…&tokens=….
The job state is at state.status.status:
| Status | Meaning | What to do |
|---|---|---|
pending, started, attempt_failed | Still in progress | Keep polling |
complete_success | Finished | Read the result URL |
complete_failure, dead | Failed | Check inputs and try again |
cancelled_by_user, cancelled_by_system | Cancelled | Start a new job if needed |
state.status.progress_percentage gives a rough 0–100 progress value. When the job succeeds, the finished file is at state.maybe_result.media_links.cdn_url. Video jobs also include still and animated preview URLs under media_links.maybe_video_previews.
Complete example in Python
This script submits a video job and polls until it finishes:
import os
import time
import uuid
import requests
BASE_URL = "https://api.storyteller.ai"
HEADERS = {"Authorization": f"Bearer {os.environ['ARTCRAFT_API_KEY']}"}
FAILED = {"complete_failure", "dead", "cancelled_by_user", "cancelled_by_system"}
job = requests.post(
f"{BASE_URL}/v1/omni_api/generate/video",
headers=HEADERS,
json={
"model": "seedance_2p0",
"prompt": "A puffin turns its head on a windy cliff edge as morning light catches its beak.",
"duration_seconds": 6,
"aspect_ratio": "wide_sixteen_by_nine",
"idempotency_token": str(uuid.uuid4()),
"start_frame_image_url": "https://example.com/puffin.jpg",
},
timeout=240,
)
job.raise_for_status()
token = job.json()["inference_job_token"]
while True:
resp = requests.get(f"{BASE_URL}/v1/omni_api/job_status/job/{token}", headers=HEADERS, timeout=30)
resp.raise_for_status()
state = resp.json()["state"]
status = state["status"]["status"]
print(status, state["status"]["progress_percentage"], "%")
if status == "complete_success":
print("Video:", state["maybe_result"]["media_links"]["cdn_url"])
break
if status in FAILED:
raise SystemExit(f"Job ended without a result: {status}")
time.sleep(5)
Complete example in JavaScript
The same flow in Node.js 18 or newer, which has fetch built in:
import { randomUUID } from "node:crypto";
const BASE_URL = "https://api.storyteller.ai";
const headers = {
Authorization: `Bearer ${process.env.ARTCRAFT_API_KEY}`,
"Content-Type": "application/json",
};
const FAILED = new Set(["complete_failure", "dead", "cancelled_by_user", "cancelled_by_system"]);
const job = await fetch(`${BASE_URL}/v1/omni_api/generate/video`, {
method: "POST",
headers,
body: JSON.stringify({
model: "seedance_2p0",
prompt: "A puffin turns its head on a windy cliff edge as morning light catches its beak.",
duration_seconds: 6,
aspect_ratio: "wide_sixteen_by_nine",
idempotency_token: randomUUID(),
reference_image_urls: ["https://example.com/puffin.jpg", "https://example.com/cliff.jpg"],
}),
}).then((r) => r.json());
while (true) {
const { state } = await fetch(
`${BASE_URL}/v1/omni_api/job_status/job/${job.inference_job_token}`,
{ headers },
).then((r) => r.json());
const status = state.status.status;
if (status === "complete_success") {
console.log("Video:", state.maybe_result.media_links.cdn_url);
break;
}
if (FAILED.has(status)) throw new Error(`Job ended without a result: ${status}`);
await new Promise((r) => setTimeout(r, 5000));
}
Generating images
A companion endpoint, POST /v1/omni_api/generate/image, follows the same pattern. It accepts image_urls (or image_media_tokens, but not both) for input images, returns a job token, and uses the same job status endpoint. For image jobs, cdn_url points to the image file, and media_links.maybe_thumbnail_template provides resizable thumbnails.
Errors and troubleshooting
| Response | Meaning | Fix |
|---|---|---|
400 | Invalid request, such as a bad URL, a WebM passed as a reference video, or both a URL and a media token for the same input | Read the error message and fix the field it names |
401 Unauthorized | Missing or invalid API key, or the key's owner is banned | Check the header format and the key |
402 Payment Required | Not enough credits or balance | Top up your account |
Cost control
API generations use the same models and pricing as the app. Seedance 2.0 costs about 16 credits per second on the official Basic plan estimate, so a 6-second clip is roughly 100 credits. Test your integration with short clips first.
The official documentation in the GitHub repository covers the video endpoints in detail and links to the full API reference for everything else. Model identifiers other than seedance_2p0 are not listed in that guide, so check the full reference before using other models.
Frequently asked questions
Does ArtCraft have an API?
Yes. The Omni API is ArtCraft's API-key-authenticated interface for generation. It mirrors the in-app generation endpoints and is documented in the storytold/artcraft GitHub repository.
How do I get an ArtCraft API key?
In the ArtCraft web app, go to Account, then Settings, then API Keys. Access is enabled per account by the ArtCraft team, so if you don't see the section, contact them to request it. The secret is shown only once.
What is the ArtCraft API base URL?
Production requests go to https://api.storyteller.ai. The official docs note that this is not a typo: the API is hosted on the storyteller.ai domain, not getartcraft.com.
Does the API use the same credits as the app?
The API documentation lists a 402 Payment Required error for insufficient credits or balance, so API generations draw on your account balance.