ArtCraft Tips

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

SettingValue
Production base URLhttps://api.storyteller.ai
Auth headerAuthorization: Bearer artcraft_api_…
CookiesNot 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

  1. Open the web app

    Sign in at app.getartcraft.com.

  2. Go to Account → Settings → API Keys

    This section only appears if API access is enabled for your account.

  3. 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

FieldRequiredNotes
modelYesModel identifier, for example seedance_2p0
idempotency_tokenYesA fresh UUID for every request; reusing one is rejected as a duplicate
promptNoText prompt
duration_secondsNoClip length, for example 6
aspect_ratioNowide_sixteen_by_nine, tall_nine_by_sixteen, or square
resolutionNoFor example seven_twenty_p or ten_eighty_p
negative_promptNoWhat to avoid
generate_audioNoBoolean

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:

FieldTypeAccepted formats
start_frame_image_urlURLJPEG, PNG, GIF, WebP
end_frame_image_urlURLJPEG, PNG, GIF, WebP
reference_image_urlsArray of URLsJPEG, PNG, GIF, WebP
reference_video_urlsArray of URLsMP4 only
reference_audio_urlsArray of URLsWAV, MP3, AAC, OGG, FLAC, and more

Rules to keep in mind:

  • URLs must start with http:// or https:// 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:

StatusMeaningWhat to do
pending, started, attempt_failedStill in progressKeep polling
complete_successFinishedRead the result URL
complete_failure, deadFailedCheck inputs and try again
cancelled_by_user, cancelled_by_systemCancelledStart 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

ResponseMeaningFix
400Invalid request, such as a bad URL, a WebM passed as a reference video, or both a URL and a media token for the same inputRead the error message and fix the field it names
401 UnauthorizedMissing or invalid API key, or the key's owner is bannedCheck the header format and the key
402 Payment RequiredNot enough credits or balanceTop 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.