wss://api.sulphur.dev/v1/caption/{job_id}/eventsAPI reference · v1
Create captions and videos with asynchronous jobs. Use the job’s WebSocket for live updates, or read the job URL at any time.
https://api.sulphur.dev/v1
Authentication
Create a key in your account, keep it on your server, and pass it as a Bearer token.
Authorization: Bearer sk_sulphur_…
Caption keys use caption:read and caption:write. Generation keys use generation:read and generation:write.
Video generation
Text-to-video needs one create request. Supply a unique Idempotency-Key, then connect to the returned job.
# pip install httpx websockets
import asyncio, json, os, uuid
import httpx
from websockets.asyncio.client import connect
auth = {"Authorization": f"Bearer {os.environ['SULPHUR_API_KEY']}"}
job = httpx.post(
"https://api.sulphur.dev/v1/generations",
headers={**auth, "Idempotency-Key": str(uuid.uuid4())},
json={
"model": "sulphur-video-1",
"mode": "text_to_video",
"prompt": "Sunlight moving across a quiet room",
"duration_seconds": 5,
"aspect_ratio": "16:9",
"resolution": "720p",
"max_credits": 68,
},
).raise_for_status().json()
async def result():
url = f"wss://api.sulphur.dev/v1/generations/{job['id']}/events"
async with connect(url, additional_headers=auth) as socket:
async for raw in socket:
current = json.loads(raw).get("data", {}).get("job")
if current and current["status"] == "completed":
output = httpx.get(
"https://api.sulphur.dev" + current["result"]["url"],
headers=auth,
).raise_for_status().json()
return output["url"]
if current and current["status"] in {"failed", "expired", "cancelled"}:
raise RuntimeError(current["error"]["message"])
print(asyncio.run(result()))
// npm install ws
import crypto from "node:crypto";
import WebSocket from "ws";
const key = process.env.SULPHUR_API_KEY;
const auth = { Authorization: "Bearer " + key };
const response = await fetch("https://api.sulphur.dev/v1/generations", {
method: "POST",
headers: {
...auth,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
model: "sulphur-video-1",
mode: "text_to_video",
prompt: "Sunlight moving across a quiet room",
duration_seconds: 5,
aspect_ratio: "16:9",
resolution: "720p",
max_credits: 68,
}),
});
const job = await response.json();
const socket = new WebSocket(
"wss://api.sulphur.dev/v1/generations/" + job.id + "/events",
{ headers: auth },
);
socket.on("message", (raw) => {
const current = JSON.parse(raw.toString()).data?.job;
if (current?.status === "completed") {
fetch("https://api.sulphur.dev" + current.result.url, { headers: auth })
.then((result) => result.json())
.then((output) => console.log(output.url));
}
});
For image-to-video, include input_image, upload to the returned URL, then call POST /generations/{id}/submit. Use POST /generations/estimate to calculate the pay-as-you-go value first. Active subscription capacity is reserved before credits.
Captioning
Create a job, upload the video to the returned private URL, submit it, then wait for the result.
POST /captionPUT upload_urlPOST /caption/{id}/submitWS /caption/{id}/events# pip install httpx websockets
import asyncio, json, os
from pathlib import Path
import httpx
from websockets.asyncio.client import connect
API = "https://api.sulphur.dev/v1"
KEY = os.environ["SULPHUR_API_KEY"]
VIDEO = Path("example.mp4")
auth = {"Authorization": f"Bearer {KEY}"}
with httpx.Client(base_url=API, headers=auth) as client:
job = client.post("/caption", json={
"filename": VIDEO.name,
"content_type": "video/mp4",
"size_bytes": VIDEO.stat().st_size,
}).raise_for_status().json()
httpx.put(job["upload_url"], content=VIDEO.read_bytes(),
headers={"Content-Type": "video/mp4"}).raise_for_status()
client.post(f"/caption/{job['id']}/submit").raise_for_status()
async def result():
url = f"wss://api.sulphur.dev/v1/caption/{job['id']}/events"
async with connect(url, additional_headers=auth) as socket:
async for raw in socket:
current = json.loads(raw).get("data", {}).get("job")
if current and current["status"] == "completed":
return current["caption"]
if current and current["status"] in {"failed", "expired", "cancelled"}:
raise RuntimeError(current.get("error") or current["status"])
print(asyncio.run(result()))
// npm install ws
import { readFile, stat } from "node:fs/promises";
import WebSocket from "ws";
const API = "https://api.sulphur.dev/v1";
const key = process.env.SULPHUR_API_KEY;
const auth = { Authorization: "Bearer " + key };
const file = await readFile("example.mp4");
const created = await fetch(API + "/caption", {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({
filename: "example.mp4",
content_type: "video/mp4",
size_bytes: (await stat("example.mp4")).size,
}),
});
const job = await created.json();
await fetch(job.upload_url, {
method: "PUT",
headers: { "Content-Type": "video/mp4" },
body: file,
});
await fetch(API + "/caption/" + job.id + "/submit", {
method: "POST",
headers: auth,
});
const socket = new WebSocket(
"wss://api.sulphur.dev/v1/caption/" + job.id + "/events",
{ headers: auth },
);
socket.on("message", (raw) => {
const current = JSON.parse(raw.toString()).data?.job;
if (current?.status === "completed") console.log(current.caption);
});
Videos may be up to 10 MiB. Do not send your API key to the signed upload_url. Each create request makes a new job and reserves four credits.
Job updates
wss://api.sulphur.dev/v1/caption/{job_id}/eventswss://api.sulphur.dev/v1/generations/{job_id}/eventsThe first message is job.snapshot. Later messages are job.updated or a terminal event such as job.completed.
{
"type": "job.completed",
"data": { "resource": "caption", "job": { "id": "…", "status": "completed" } }
}
The job keeps running. Reconnecting sends a fresh snapshot; the matching HTTP GET endpoint returns the same durable state.
Reference
| Method | Path | Purpose |
|---|---|---|
POST | /generations | Create a video job. |
POST | /generations/{id}/submit | Submit an uploaded image. |
GET | /generations/{id} | Read a video job. |
WS | /generations/{id}/events | Watch a video job. |
GET | /generations/{id}/output | Get a temporary download URL. |
POST | /caption | Create a caption job. |
POST | /caption/{id}/submit | Submit an uploaded video. |
GET | /caption/{id} | Read a caption job. |
WS | /caption/{id}/events | Watch a caption job. |
The OpenAPI schema contains request fields, response schemas, list and cancel routes, pagination, validation rules, and error responses.
Sulphur