mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-20 17:02:53 -04:00
This is a fresh start for the `artcraft` repo. The old monorepo, which included our web API gateway, job services, SRE tools, various frontends, and more has moved to `artcraft-services` You can find the full git history here: https://github.com/storytold/artcraft-services This change was motivated by the fact that the entire git history was hundreds of gigabytes in size, which is unwieldy for anyone not in the core team that doesn't care about support services outside of the core artcraft app. We wanted to keep this repo in its well-known place since we already have users and activity here. Please update your remotes to either `artcraft-services` or make a fresh clone of this repo. History can be found at the artcraft-services repo.
626 lines
24 KiB
Markdown
626 lines
24 KiB
Markdown
# ArtCraft Omni API
|
||
|
||
> # 📚 Full API reference: **<https://storyteller-docs.netlify.app/>**
|
||
>
|
||
> **Every ArtCraft API endpoint is documented there.** This guide covers only the Omni API video
|
||
> endpoints — for everything else, start at the link above.
|
||
|
||
The **Omni API** is the API-key–authenticated surface for ArtCraft generation. It mirrors the
|
||
in-app "Omni Gen" endpoints but is designed for programmatic use: you authenticate with an API key
|
||
in the `Authorization` header instead of a browser session cookie.
|
||
|
||
This guide covers **video generation from URL inputs** — passing image, video, and audio reference
|
||
URLs directly. The server downloads each URL, stores it as a media file on your account, and then
|
||
runs generation against it. You never have to upload files yourself.
|
||
|
||
> All examples send **no cookies**. The Omni API is authenticated purely by your API key. If you
|
||
> send session cookies they are ignored.
|
||
|
||
---
|
||
|
||
## Base URLs
|
||
|
||
| Environment | Base URL |
|
||
| ------------- | ------------------------------ |
|
||
| Development | `http://localhost:12345` |
|
||
| Production | `https://api.storyteller.ai` |
|
||
|
||
All paths below are relative to the base URL (e.g. `POST /v1/omni_api/generate/video`).
|
||
|
||
The examples in this guide use the **development** base URL. To run against production, swap
|
||
`http://localhost:12345` for `https://api.storyteller.ai`.
|
||
|
||
Note: Our APIs are hosted on "`api.storyteller.ai`", not "`api.getartcraft.com`". This is not a
|
||
typo. Our Cloudflare CDN URLs are `cdn-2.fakeyou.com`.
|
||
|
||
---
|
||
|
||
## Getting an API key
|
||
|
||
Create and manage API keys from the ArtCraft web app:
|
||
|
||
1. Go to **<https://app.getartcraft.com/>**
|
||
2. Click **Account → Settings → API Keys** (your account must have API keys enabled from our staff)
|
||
3. Create a key and copy it. The secret is shown **once** — store it somewhere safe.
|
||
|
||
> **Don't see the API Keys section?** API access is gated per account. Contact the Artcraft team
|
||
> to enable it.
|
||
|
||
### What an API key looks like
|
||
|
||
- Prefix: `artcraft_api_`
|
||
- Followed by 40 random lowercase Crockford-base32 characters (`0-9 a-z`, excluding `i l o u`)
|
||
- Total length: **53 characters**, e.g. `artcraft_api_3k7q9w0xv2hs5n8m4d6r1t8y0p2s4f6h8j0k2m4n6`
|
||
|
||
Treat the key like a password: never commit it to source control or paste it into logs.
|
||
|
||
---
|
||
|
||
## Authentication
|
||
|
||
Send the secret key in the `Authorization` header. Three forms are accepted:
|
||
|
||
```
|
||
Authorization: Bearer artcraft_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||
Authorization: Key artcraft_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||
Authorization: artcraft_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||
```
|
||
|
||
`Bearer` is recommended. **Do not send cookies.**
|
||
|
||
---
|
||
|
||
## Video generation — `POST /v1/omni_api/generate/video`
|
||
|
||
Generates a video and returns an inference job token you can poll for results.
|
||
|
||
### Request body
|
||
|
||
| Field | Type | Notes |
|
||
| ---------------------------- | --------------- | ----- |
|
||
| `model` | string | Generation model, e.g. `seedance_2p0`. **Required.** |
|
||
| `prompt` | string | Text prompt. |
|
||
| `duration_seconds` | integer | Length of the output video, e.g. `6`. |
|
||
| `aspect_ratio` | string | e.g. `wide_sixteen_by_nine`, `tall_nine_by_sixteen`, `square`. |
|
||
| `idempotency_token` | string (UUID) | **Required.** A fresh UUID per request; prevents accidental duplicates. |
|
||
| `negative_prompt` | string | Optional. |
|
||
| `resolution` | string | Optional, e.g. `seven_twenty_p`, `ten_eighty_p`. |
|
||
| `generate_audio` | boolean | Optional. |
|
||
| **URL inputs** (this guide) | | |
|
||
| `start_frame_image_url` | string (URL) | Starting keyframe image. |
|
||
| `end_frame_image_url` | string (URL) | Ending keyframe image. |
|
||
| `reference_image_urls` | array of URL | Reference images. |
|
||
| `reference_video_urls` | array of URL | Reference videos (`mp4`). |
|
||
| `reference_audio_urls` | array of URL | Reference audio (`wav`, `mp3`, `ogg`, …). |
|
||
| **Media-token inputs** | | |
|
||
| `start_frame_image_media_token`, `end_frame_image_media_token`, `reference_image_media_tokens`, `reference_video_media_tokens`, `reference_audio_media_tokens` | token(s) | Use these instead if you've already uploaded media. |
|
||
|
||
#### Rules for URL inputs
|
||
|
||
- Each URL field and its media-token counterpart are **mutually exclusive**. Sending both
|
||
`reference_image_urls` and `reference_image_media_tokens`, for example, returns:
|
||
`Either reference_image_media_tokens or reference_image_urls must be set, not both`.
|
||
- Every URL must start with `http://` or `https://`, otherwise:
|
||
`URL must start with http:// or https://, bad URL: <url>`.
|
||
- Redirects are followed (up to 10), so CDN/redirecting URLs work.
|
||
- The file type is detected from the downloaded bytes. Images must be a real image
|
||
(`jpeg`/`png`/`gif`/`webp`), reference videos must be `mp4`, reference audio must be a
|
||
supported audio type (`wav`, `mp3`, `aac`, `ogg`, `flac`, …).
|
||
|
||
### Response
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"inference_job_token": "jinf_xxxxxxxxxxxxxxxxxxxxxxxxx",
|
||
"all_job_tokens": ["jinf_xxxxxxxxxxxxxxxxxxxxxxxxx"]
|
||
}
|
||
```
|
||
|
||
Use `inference_job_token` to poll for the finished video. `all_job_tokens` lists every job created
|
||
(batch requests can create more than one).
|
||
|
||
---
|
||
|
||
## Examples
|
||
|
||
The examples reuse these public sample URLs:
|
||
|
||
- Images (Wikimedia Commons): an Atlantic puffin, the Matterhorn, a toco toucan
|
||
- Video: a short sample `mp4`
|
||
- Audio: a short sample `wav`
|
||
|
||
Replace `YOUR_API_KEY` with your `artcraft_api_…` secret, and generate a fresh `idempotency_token`
|
||
(UUID) for every request.
|
||
|
||
---
|
||
|
||
### Example 1 — Starting-frame Seedance (`start_frame_image_url`)
|
||
|
||
Animate outward from a single starting image.
|
||
|
||
#### bash
|
||
|
||
```bash
|
||
curl -s -X POST http://localhost:12345/v1/omni_api/generate/video \
|
||
-H "Authorization: Bearer YOUR_API_KEY" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"model": "seedance_2p0",
|
||
"prompt": "An Atlantic puffin stands on a windy cliff edge and turns its head as morning light catches its colorful beak.",
|
||
"duration_seconds": 6,
|
||
"aspect_ratio": "wide_sixteen_by_nine",
|
||
"idempotency_token": "'"$(uuidgen)"'",
|
||
"start_frame_image_url": "https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900"
|
||
}'
|
||
```
|
||
|
||
#### python
|
||
|
||
```python
|
||
import uuid
|
||
import requests
|
||
|
||
BASE_URL = "http://localhost:12345" # production: https://api.storyteller.ai
|
||
API_KEY = "YOUR_API_KEY"
|
||
|
||
resp = requests.post(
|
||
f"{BASE_URL}/v1/omni_api/generate/video",
|
||
headers={"Authorization": f"Bearer {API_KEY}"},
|
||
json={
|
||
"model": "seedance_2p0",
|
||
"prompt": "An Atlantic puffin stands on a windy cliff edge and turns its head as morning light catches its colorful beak.",
|
||
"duration_seconds": 6,
|
||
"aspect_ratio": "wide_sixteen_by_nine",
|
||
"idempotency_token": str(uuid.uuid4()),
|
||
"start_frame_image_url": "https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
|
||
},
|
||
timeout=240,
|
||
)
|
||
resp.raise_for_status()
|
||
print(resp.json())
|
||
```
|
||
|
||
#### javascript
|
||
|
||
```javascript
|
||
import { randomUUID } from "node:crypto";
|
||
|
||
const BASE_URL = "http://localhost:12345"; // production: https://api.storyteller.ai
|
||
const API_KEY = "YOUR_API_KEY";
|
||
|
||
const resp = await fetch(`${BASE_URL}/v1/omni_api/generate/video`, {
|
||
method: "POST",
|
||
headers: {
|
||
Authorization: `Bearer ${API_KEY}`,
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: JSON.stringify({
|
||
model: "seedance_2p0",
|
||
prompt:
|
||
"An Atlantic puffin stands on a windy cliff edge and turns its head as morning light catches its colorful beak.",
|
||
duration_seconds: 6,
|
||
aspect_ratio: "wide_sixteen_by_nine",
|
||
idempotency_token: randomUUID(),
|
||
start_frame_image_url:
|
||
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
|
||
}),
|
||
});
|
||
|
||
console.log(resp.status, await resp.json());
|
||
```
|
||
|
||
---
|
||
|
||
### Example 2 — Image-reference Seedance (`reference_image_urls`)
|
||
|
||
Guide the generation with one or more reference images.
|
||
|
||
#### bash
|
||
|
||
```bash
|
||
curl -s -X POST http://localhost:12345/v1/omni_api/generate/video \
|
||
-H "Authorization: Bearer YOUR_API_KEY" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"model": "seedance_2p0",
|
||
"prompt": "A whimsical journey: an Atlantic puffin perches on a cliff beneath the snow-capped Matterhorn while a brilliant toco toucan watches from a misty rainforest canopy.",
|
||
"duration_seconds": 6,
|
||
"aspect_ratio": "wide_sixteen_by_nine",
|
||
"idempotency_token": "'"$(uuidgen)"'",
|
||
"reference_image_urls": [
|
||
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
|
||
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
|
||
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900"
|
||
]
|
||
}'
|
||
```
|
||
|
||
#### python
|
||
|
||
```python
|
||
import uuid
|
||
import requests
|
||
|
||
BASE_URL = "http://localhost:12345" # production: https://api.storyteller.ai
|
||
API_KEY = "YOUR_API_KEY"
|
||
|
||
resp = requests.post(
|
||
f"{BASE_URL}/v1/omni_api/generate/video",
|
||
headers={"Authorization": f"Bearer {API_KEY}"},
|
||
json={
|
||
"model": "seedance_2p0",
|
||
"prompt": "A whimsical journey: an Atlantic puffin perches on a cliff beneath the snow-capped Matterhorn while a brilliant toco toucan watches from a misty rainforest canopy.",
|
||
"duration_seconds": 6,
|
||
"aspect_ratio": "wide_sixteen_by_nine",
|
||
"idempotency_token": str(uuid.uuid4()),
|
||
"reference_image_urls": [
|
||
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
|
||
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
|
||
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900",
|
||
],
|
||
},
|
||
timeout=240,
|
||
)
|
||
resp.raise_for_status()
|
||
print(resp.json())
|
||
```
|
||
|
||
#### javascript
|
||
|
||
```javascript
|
||
import { randomUUID } from "node:crypto";
|
||
|
||
const BASE_URL = "http://localhost:12345"; // production: https://api.storyteller.ai
|
||
const API_KEY = "YOUR_API_KEY";
|
||
|
||
const resp = await fetch(`${BASE_URL}/v1/omni_api/generate/video`, {
|
||
method: "POST",
|
||
headers: {
|
||
Authorization: `Bearer ${API_KEY}`,
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: JSON.stringify({
|
||
model: "seedance_2p0",
|
||
prompt:
|
||
"A whimsical journey: an Atlantic puffin perches on a cliff beneath the snow-capped Matterhorn while a brilliant toco toucan watches from a misty rainforest canopy.",
|
||
duration_seconds: 6,
|
||
aspect_ratio: "wide_sixteen_by_nine",
|
||
idempotency_token: randomUUID(),
|
||
reference_image_urls: [
|
||
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
|
||
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
|
||
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900",
|
||
],
|
||
}),
|
||
});
|
||
|
||
console.log(resp.status, await resp.json());
|
||
```
|
||
|
||
---
|
||
|
||
### Example 3 — Image + video reference Seedance (`reference_image_urls` + `reference_video_urls`)
|
||
|
||
Combine reference images with a reference video (and, optionally, reference audio). This is the
|
||
fullest URL-driven request.
|
||
|
||
#### bash
|
||
|
||
```bash
|
||
curl -s -X POST http://localhost:12345/v1/omni_api/generate/video \
|
||
-H "Authorization: Bearer YOUR_API_KEY" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"model": "seedance_2p0",
|
||
"prompt": "An Atlantic puffin stands on a windy cliff edge; the scene sweeps up to the snow-capped Matterhorn at sunrise before settling on a brilliant toco toucan in a misty rainforest canopy. The camera glides with the motion of the reference video, set to the mood of the reference audio.",
|
||
"duration_seconds": 6,
|
||
"aspect_ratio": "wide_sixteen_by_nine",
|
||
"idempotency_token": "'"$(uuidgen)"'",
|
||
"reference_image_urls": [
|
||
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
|
||
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
|
||
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900"
|
||
],
|
||
"reference_video_urls": ["https://download.samplelib.com/mp4/sample-5s.mp4"],
|
||
"reference_audio_urls": ["https://download.samplelib.com/wav/sample-6s.wav"]
|
||
}'
|
||
```
|
||
|
||
#### python
|
||
|
||
```python
|
||
import uuid
|
||
import requests
|
||
|
||
BASE_URL = "http://localhost:12345" # production: https://api.storyteller.ai
|
||
API_KEY = "YOUR_API_KEY"
|
||
|
||
resp = requests.post(
|
||
f"{BASE_URL}/v1/omni_api/generate/video",
|
||
headers={"Authorization": f"Bearer {API_KEY}"},
|
||
json={
|
||
"model": "seedance_2p0",
|
||
"prompt": "An Atlantic puffin stands on a windy cliff edge; the scene sweeps up to the snow-capped Matterhorn at sunrise before settling on a brilliant toco toucan in a misty rainforest canopy. The camera glides with the motion of the reference video, set to the mood of the reference audio.",
|
||
"duration_seconds": 6,
|
||
"aspect_ratio": "wide_sixteen_by_nine",
|
||
"idempotency_token": str(uuid.uuid4()),
|
||
"reference_image_urls": [
|
||
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
|
||
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
|
||
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900",
|
||
],
|
||
"reference_video_urls": ["https://download.samplelib.com/mp4/sample-5s.mp4"],
|
||
"reference_audio_urls": ["https://download.samplelib.com/wav/sample-6s.wav"],
|
||
},
|
||
timeout=240,
|
||
)
|
||
resp.raise_for_status()
|
||
print(resp.json())
|
||
```
|
||
|
||
#### javascript
|
||
|
||
```javascript
|
||
import { randomUUID } from "node:crypto";
|
||
|
||
const BASE_URL = "http://localhost:12345"; // production: https://api.storyteller.ai
|
||
const API_KEY = "YOUR_API_KEY";
|
||
|
||
const resp = await fetch(`${BASE_URL}/v1/omni_api/generate/video`, {
|
||
method: "POST",
|
||
headers: {
|
||
Authorization: `Bearer ${API_KEY}`,
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: JSON.stringify({
|
||
model: "seedance_2p0",
|
||
prompt:
|
||
"An Atlantic puffin stands on a windy cliff edge; the scene sweeps up to the snow-capped Matterhorn at sunrise before settling on a brilliant toco toucan in a misty rainforest canopy. The camera glides with the motion of the reference video, set to the mood of the reference audio.",
|
||
duration_seconds: 6,
|
||
aspect_ratio: "wide_sixteen_by_nine",
|
||
idempotency_token: randomUUID(),
|
||
reference_image_urls: [
|
||
"https://commons.wikimedia.org/wiki/Special:FilePath/Puffin_%28Fratercula_arctica%29.jpg?width=900",
|
||
"https://commons.wikimedia.org/wiki/Special:FilePath/Matterhorn_from_Domh%C3%BCtte_-_2.jpg?width=900",
|
||
"https://commons.wikimedia.org/wiki/Special:FilePath/Toco_toucan.jpg?width=900",
|
||
],
|
||
reference_video_urls: ["https://download.samplelib.com/mp4/sample-5s.mp4"],
|
||
reference_audio_urls: ["https://download.samplelib.com/wav/sample-6s.wav"],
|
||
}),
|
||
});
|
||
|
||
console.log(resp.status, await resp.json());
|
||
```
|
||
|
||
---
|
||
|
||
## Checking job status
|
||
|
||
Generation is asynchronous. The generate call returns an `inference_job_token`; poll the job-status
|
||
endpoint until the job reaches a terminal state, then read the result URL.
|
||
|
||
### Endpoints
|
||
|
||
- **Single job:** `GET /v1/omni_api/job_status/job/{token}`
|
||
- **Batch (many jobs):** `GET /v1/omni_api/job_status/batch?tokens=jinf_aaa&tokens=jinf_bbb`
|
||
|
||
Both require the same `Authorization: Bearer …` header (no cookies).
|
||
|
||
### Response shape
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"state": {
|
||
"job_token": "jinf_xxxxxxxxxxxxxxxxxxxxxxxxx",
|
||
"request": {
|
||
"inference_category": "video_generation",
|
||
"maybe_prompt_token": "prompt_xxxxxxxxxxxxxxxxxxxxxxxxx",
|
||
"maybe_model_type": "seedance_2p0",
|
||
"maybe_model_token": null
|
||
},
|
||
"status": {
|
||
"status": "pending",
|
||
"maybe_first_started_at": null,
|
||
"maybe_failure_category": null,
|
||
"progress_percentage": 0
|
||
},
|
||
"maybe_result": null,
|
||
"created_at": "2026-06-24T04:06:14Z",
|
||
"updated_at": "2026-06-24T04:06:14Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
- `state.status.status` — the job state. Keep polling while it's `pending`, `started`, or
|
||
`attempt_failed`. **Terminal** states are `complete_success`, `complete_failure`, `dead`,
|
||
`cancelled_by_user`, and `cancelled_by_system`.
|
||
- `state.status.progress_percentage` — an integer 0–100.
|
||
- `state.maybe_result` — `null` until the job succeeds. On `complete_success` it contains the
|
||
finished media; the playable/downloadable URL is **`state.maybe_result.media_links.cdn_url`**.
|
||
For video, `media_links.maybe_video_previews` also carries still/animated preview URLs.
|
||
|
||
The batch endpoint returns `{ "success": true, "job_states": [ … ] }` where each entry is the same
|
||
`state` payload as above.
|
||
|
||
### Where the result video / image lives
|
||
|
||
When `state.status.status` is `complete_success`, the finished media is under `state.maybe_result`:
|
||
|
||
- **`state.maybe_result.media_links.cdn_url`** — the primary, playable/downloadable URL of the
|
||
finished asset. **This is the field you want.** For a video job it's the `.mp4`; for an image job
|
||
it's the image file (`.jpg` / `.png` / etc.).
|
||
- `state.maybe_result.entity_token` — the `m_…` media-file token for the result.
|
||
- `state.maybe_result.media_links.maybe_video_previews` — **video jobs only**. Contains a `still`
|
||
(poster `.jpg`) and an `animated` (`.gif`) preview URL, plus `{WIDTH}` thumbnail templates. This
|
||
is `null` for image jobs.
|
||
- `state.maybe_result.media_links.maybe_thumbnail_template` — **image jobs only**. A URL template;
|
||
replace `{WIDTH}` with a pixel width to fetch a resized thumbnail. `null` for video jobs.
|
||
|
||
**Example — completed video job:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"state": {
|
||
"job_token": "jinf_9fcn2qv4dv3vvbrrxbnmfgen06v",
|
||
"request": {
|
||
"inference_category": "video_generation",
|
||
"maybe_prompt_token": "prompt_q53tp4rh97qh8q159p21tazzt",
|
||
"maybe_model_type": "seedance_2p0",
|
||
"maybe_model_token": null
|
||
},
|
||
"status": {
|
||
"status": "complete_success",
|
||
"maybe_first_started_at": "2026-06-24T04:07:11Z",
|
||
"maybe_failure_category": null,
|
||
"progress_percentage": 100
|
||
},
|
||
"maybe_result": {
|
||
"entity_type": "media_file",
|
||
"entity_token": "m_ss682134tx19yhqs5y0nbnd0bd59a6",
|
||
"media_links": {
|
||
"cdn_url": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4",
|
||
"maybe_thumbnail_template": null,
|
||
"maybe_video_previews": {
|
||
"still": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4-thumb.jpg",
|
||
"animated": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4-thumb.gif",
|
||
"still_thumbnail_template": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4-thumb-{WIDTH}.jpg",
|
||
"animated_thumbnail_template": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4-thumb-{WIDTH}.gif"
|
||
}
|
||
},
|
||
"maybe_successfully_completed_at": "2026-06-24T04:17:30Z"
|
||
},
|
||
"created_at": "2026-06-24T04:06:14Z",
|
||
"updated_at": "2026-06-24T04:17:30Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
→ The video is at
|
||
`https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/8/p/n/0/4/8pn04v7q3vfhf60wy173f6awcwkre1qj/artcraft_8pn04v7q3vfhf60wy173f6awcwkre1qj.mp4`
|
||
(example, not a live URL).
|
||
|
||
**Example — completed image job** (from `POST /v1/omni_api/generate/image`):
|
||
|
||
```json
|
||
{
|
||
"maybe_result": {
|
||
"entity_type": "media_file",
|
||
"entity_token": "m_3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6",
|
||
"media_links": {
|
||
"cdn_url": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/3/k/q/9/w/3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6/artcraft_3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6.jpg",
|
||
"maybe_thumbnail_template": "https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/3/k/q/9/w/3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6/artcraft_3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6.jpg-thumb-{WIDTH}.jpg",
|
||
"maybe_video_previews": null
|
||
},
|
||
"maybe_successfully_completed_at": "2026-06-24T04:09:02Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
→ The image is at
|
||
`https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev/media/3/k/q/9/w/3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6/artcraft_3kq9w0xv2hs5n8m4d6r1t8y0p2s4f6.jpg`
|
||
(example, not a live URL).
|
||
|
||
### Poll until complete
|
||
|
||
#### bash
|
||
|
||
```bash
|
||
JOB_TOKEN="jinf_xxxxxxxxxxxxxxxxxxxxxxxxx"
|
||
|
||
while :; do
|
||
resp=$(curl -s "http://localhost:12345/v1/omni_api/job_status/job/$JOB_TOKEN" \
|
||
-H "Authorization: Bearer YOUR_API_KEY")
|
||
status=$(echo "$resp" | python3 -c "import sys,json;print(json.load(sys.stdin)['state']['status']['status'])")
|
||
echo "status=$status"
|
||
case "$status" in
|
||
complete_success)
|
||
echo "$resp" | python3 -c "import sys,json;print(json.load(sys.stdin)['state']['maybe_result']['media_links']['cdn_url'])"
|
||
break ;;
|
||
complete_failure|dead|cancelled_by_user|cancelled_by_system)
|
||
echo "job ended without a result: $status"; break ;;
|
||
esac
|
||
sleep 5
|
||
done
|
||
```
|
||
|
||
#### python
|
||
|
||
```python
|
||
import time
|
||
import requests
|
||
|
||
BASE_URL = "http://localhost:12345" # production: https://api.storyteller.ai
|
||
API_KEY = "YOUR_API_KEY"
|
||
JOB_TOKEN = "jinf_xxxxxxxxxxxxxxxxxxxxxxxxx"
|
||
|
||
TERMINAL_FAILURES = {"complete_failure", "dead", "cancelled_by_user", "cancelled_by_system"}
|
||
|
||
while True:
|
||
resp = requests.get(
|
||
f"{BASE_URL}/v1/omni_api/job_status/job/{JOB_TOKEN}",
|
||
headers={"Authorization": f"Bearer {API_KEY}"},
|
||
timeout=30,
|
||
)
|
||
resp.raise_for_status()
|
||
state = resp.json()["state"]
|
||
status = state["status"]["status"]
|
||
print("status:", status, state["status"]["progress_percentage"], "%")
|
||
|
||
if status == "complete_success":
|
||
print("result URL:", state["maybe_result"]["media_links"]["cdn_url"])
|
||
break
|
||
if status in TERMINAL_FAILURES:
|
||
print("job ended without a result:", status)
|
||
break
|
||
time.sleep(5)
|
||
```
|
||
|
||
#### javascript
|
||
|
||
```javascript
|
||
const BASE_URL = "http://localhost:12345"; // production: https://api.storyteller.ai
|
||
const API_KEY = "YOUR_API_KEY";
|
||
const JOB_TOKEN = "jinf_xxxxxxxxxxxxxxxxxxxxxxxxx";
|
||
|
||
const TERMINAL_FAILURES = new Set([
|
||
"complete_failure", "dead", "cancelled_by_user", "cancelled_by_system",
|
||
]);
|
||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||
|
||
while (true) {
|
||
const resp = await fetch(`${BASE_URL}/v1/omni_api/job_status/job/${JOB_TOKEN}`, {
|
||
headers: { Authorization: `Bearer ${API_KEY}` },
|
||
});
|
||
const { state } = await resp.json();
|
||
const status = state.status.status;
|
||
console.log("status:", status, state.status.progress_percentage, "%");
|
||
|
||
if (status === "complete_success") {
|
||
console.log("result URL:", state.maybe_result.media_links.cdn_url);
|
||
break;
|
||
}
|
||
if (TERMINAL_FAILURES.has(status)) {
|
||
console.log("job ended without a result:", status);
|
||
break;
|
||
}
|
||
await sleep(5000);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Notes & troubleshooting
|
||
|
||
- **Always send a fresh `idempotency_token`** (UUID). Reusing one is rejected as a duplicate.
|
||
- **No cookies.** The endpoint authenticates only via the `Authorization` header.
|
||
- **URL must be `http(s)`** and reachable by the server; redirects are followed.
|
||
- **Type mismatches** (e.g. a `.webm` passed as a reference video, which requires `mp4`) are
|
||
rejected with a `400` describing the unpermitted type.
|
||
- **`401 Unauthorized`** — missing/invalid API key, or the key's owner is banned.
|
||
- **`402 Payment required`** — insufficient credits/balance.
|
||
- There is a companion image endpoint, `POST /v1/omni_api/generate/image`, which accepts
|
||
`image_urls` (mutually exclusive with `image_media_tokens`) following the same rules.
|