The InkSlop API lets you generate and publish videos programmatically, from a single script or a batch of hundreds.
Base URL: https://inkslop.com/api/proxy/api/v1
All endpoints require an API key passed as a Bearer token.
Authorization: Bearer isk_your_key_hereCreate API keys in the Dashboard → Accounts tab. Keys are prefixed with isk_ and are shown once at creation, so store them securely.
Key management requires a dashboard login (Logto session JWT). Requests authenticated with an API key get 403, so a leaked key cannot create new keys or revoke yours.
POST /api/v1/keys{ "name": "My automation" }Response:
{
"key_id": "a1b2c3...",
"name": "My automation",
"api_key": "isk_abcdef...",
"created_at": "2026-05-18T12:00:00Z"
}
api_keyis returned only once. Store it immediately.
GET /api/v1/keys[
{
"key_id": "a1b2c3...",
"name": "My automation",
"created_at": "2026-05-18T12:00:00Z",
"last_used_at": "2026-05-18T14:30:00Z",
"revoked": false
}
]DELETE /api/v1/keys/{key_id}{ "revoked": true, "key_id": "a1b2c3..." }List the YouTube and TikTok channels you have connected in the dashboard.
GET /api/v1/channels[
{
"id": 3,
"platform": "youtube",
"account_name": "My Channel",
"channel_id": "UCxxxxxxx",
"is_oauth": true
}
]Use the id field when submitting videos for auto-publish.
POST /api/v1/videos| Field | Type | Description |
|---|---|---|
script |
string | DSL script. See DSL Reference. |
video_name |
string | Display name (no extension). Optional. |
resolution |
string | "9:16" (portrait, default) or "16:9" |
estimated_duration |
float | Duration hint in seconds (default 30). Used for the credit pre-check. |
notify_on_complete |
bool | Send an email when generation finishes. |
publish |
array | Auto-publish targets, one object per channel. Optional. |
ai_generate_meta |
bool | Generate a title, description and tags from the script, used for every publish entry that has no title of its own. |
asset_files |
object | Only needed for your own uploaded files (see below). |
Assets. Library files referenced in the script (for example music/Carefree.opus or video/minecraft/BXUA2FncVPI.mp4) are found automatically, and so are your own files uploaded with POST /files: use the ref it returns, such as [image file="user/5f2c…a1.png" duration="3s"/]. You only need asset_files for files stored elsewhere.
Validation. The script is checked before anything is rendered or charged. Syntax errors, unknown tags and uploaded files that cannot be found return 400 with every problem listed:
{
"detail": {
"message": "The script is not valid. Nothing was rendered or charged.",
"errors": [{ "message": "Unknown tag [shak]. …", "line": 3, "column": null }]
}
}Parameters that have no effect (usually typos such as speeed="2") do not block the render; they are listed in warnings in the response. Use POST /scripts/validate to check a script without submitting it.
Minimal request (generate only):
{
"script": "[tts engine=\"kokoro_tts\" voice=\"af_heart\"]\nHello world!\n[/tts]",
"video_name": "hello-world",
"resolution": "9:16"
}With manual publish metadata:
{
"script": "...",
"publish": [
{
"channel_id": 3,
"title": "My Video",
"description": "A video created with InkSlop.",
"tags": ["ai", "shorts"],
"privacy": "public"
}
]
}With AI-generated metadata, to two channels:
{
"script": "...",
"ai_generate_meta": true,
"publish": [
{ "channel_id": 3, "privacy": "public" },
{ "channel_id": 5, "privacy": "public" }
]
}Publish config fields:
| Field | Type | Description |
|---|---|---|
channel_id |
int | Channel ID from GET /channels |
title |
string | Video title. Leave empty with ai_generate_meta: true. |
description |
string | Video description. |
tags |
array | List of tag strings. |
privacy |
string | public, private, or unlisted |
review_before_publish |
bool | Queue the upload for manual approval in the dashboard before it goes live. |
scheduled_at |
string | ISO 8601 time to publish at. Empty publishes as soon as the video is ready. |
playlist_ids |
array | YouTube only. Playlist IDs the video is added to after upload. |
YouTube descriptions get the credits for any Creative Commons music or footage from the library appended automatically, followed by "Made with #InkSlop" unless you turned branding off in your account. TikTok uploads use the title; privacy maps to TikTok's public, followers-only (unlisted) and private (private) settings.
Response:
{
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending",
"warnings": []
}POST /api/v1/videos/preview
GET /api/v1/previews/{task_id}Takes script, resolution and asset_files. Renders the first 30 seconds at 360p with an InkSlop watermark and free voices (paid voices are swapped for a free one, so timing is close but not exact) and AI images/videos replaced by their fallback or a placeholder. Nothing is charged. Poll the GET until status is done, then play video_url; previews are deleted after a day.
Previews count against an hourly allowance of preview time: 5 minutes on the free plan, 10 on Starter, 20 on Standard and 40 on Business. Each preview counts as its estimated narration length, at least 10 and at most 30 seconds, and stops counting an hour after it started. The response includes quota (used, limit, remaining, resets_in, in seconds). Over the allowance the call returns 429 with the same fields. Also limited to 5 a minute.
POST /api/v1/videos/translationsTakes every field of Submit a video, plus:
| Field | Type | Description |
|---|---|---|
languages |
array | Language codes, e.g. ["es", "fr", "de"] (up to 10). GET /api/v1/languages lists them. |
include_original |
bool | Also render the script as written. |
The narration, [text] values and [chapter] titles are translated and the voices switched to native ones; tags are never changed (a translation that alters them is rejected). Each language costs 1 credit for the translation plus the video. If any translation fails, or the videos cost more than the credits available, nothing is rendered and the translation credits are refunded.
{ "videos": [{ "lang": "original", "task_id": "…" }, { "lang": "es", "task_id": "…" }], "warnings": [] }GET /api/v1/videos/{task_id}| Status | Meaning |
|---|---|
pending |
Queued, not started yet |
processing |
Rendering in progress |
done |
Complete, video_url and download_url are available |
error |
Failed, check message |
Response (done):
{
"task_id": "550e8400...",
"status": "done",
"progress": 100,
"message": "Generation complete.",
"video_url": "https://inkslop.com/files/abc123",
"download_url": "https://inkslop.com/files/abc123/download",
"credits_used": 3,
"duration_seconds": 28.4,
"user_file_id": 42
}Poll every 5-10 seconds until status is done or error, or skip polling and receive a webhook instead. Publishing happens whether or not you poll.
GET /api/v1/videos?limit=20&offset=0Returns an array of video summaries, newest first: task_id, video_name, status, resolution, created_at, credits_used, user_file_id, video_url, download_url, duration_seconds. limit max is 100.
POST /api/v1/videos/{task_id}/publishThe video must have status: done. Queues an upload to one or more YouTube or TikTok channels.
{
"ai_generate_meta": false,
"channels": [
{
"channel_id": 3,
"title": "My Video",
"description": "Description here.",
"tags": ["shorts", "ai"],
"privacy": "public",
"scheduled_at": "2026-10-01T15:00:00Z",
"playlist_ids": []
}
]
}| Field | Type | Description |
|---|---|---|
channels |
array | One entry per channel. Required, must not be empty. |
channels[].channel_id |
int | Channel ID from GET /channels. The channel must be connected with OAuth. |
channels[].title, description, tags, privacy |
As for submit. | |
channels[].scheduled_at |
string | ISO 8601 time to publish at. Empty publishes immediately. |
channels[].playlist_ids |
array | YouTube only. Playlists to add the video to. |
ai_generate_meta |
bool | Generate metadata for entries without a title. |
Response:
{
"published": [
{ "platform": "youtube", "channel_id": 3, "job_id": 17, "status": "pending" }
]
}Upload your own images, footage and audio to use in scripts. Files count towards your storage quota.
POST /api/v1/filesSend the file as multipart/form-data in a field named file. Accepted: PNG, JPEG, WebP, MP4, MOV, WebM, MP3, WAV, OGG and Opus.
curl -X POST https://inkslop.com/api/proxy/api/v1/files \
-H "Authorization: Bearer isk_your_key_here" \
-F "[email protected]"{
"file_id": 42,
"ref": "user/5f2c9e...a1.png",
"file_name": "chart.png",
"content_type": "image/png",
"size_bytes": 48213,
"created_at": "2026-09-29T12:00:00+00:00"
}Use ref in scripts: [image file="user/5f2c9e...a1.png" duration="3s"/].
GET /api/v1/files?limit=50&offset=0Returns your uploaded files, newest first, in the same shape.
Check a script without rendering it or spending credits.
POST /api/v1/scripts/validate{ "script": "[tts voice=\"af_heart\" speeed=\"2\"]Hello[/tts]", "estimated_duration": 30 }{
"valid": true,
"errors": [],
"warnings": [{ "message": "[tts] does not use the parameter \"speeed\", so it has no effect. Check the spelling.", "line": 1, "column": null }],
"assets": [],
"estimated_duration_seconds": 0.4,
"estimated_credits": 1
}errors covers syntax errors (with line and column), unknown tags and uploaded files that cannot be found. A script with errors has valid: false and would be rejected by POST /videos. estimated_credits is an estimate; the charge is based on the finished video.
Instead of polling, InkSlop can POST an event to your server when a video finishes or fails, or when publishing to a channel succeeds or fails. Webhooks follow the Standard Webhooks specification, so any Standard Webhooks library can verify them.
| Type | When | data |
|---|---|---|
video.completed |
A video finished rendering | task_id, video_name, status, duration_seconds, credits_used, user_file_id, video_url, download_url |
video.failed |
A video could not be rendered | task_id, video_name, status, error |
publish.completed |
A video was published to a channel | job_id, platform (youtube/tiktok), user_file_id, channel_id, title, status, scheduled_at, and video_id + url (YouTube) or publish_id (TikTok) |
publish.failed |
Publishing failed after all retries | as publish.completed, plus error |
webhook.test |
You called the test endpoint | endpoint_id, message |
Example body:
{
"type": "video.completed",
"timestamp": "2026-09-29T12:04:10.344522+00:00",
"data": {
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"video_name": "mantis-shrimp",
"status": "done",
"duration_seconds": 14.2,
"credits_used": 1,
"user_file_id": 42,
"video_url": "https://inkslop.com/files/abc123",
"download_url": "https://inkslop.com/files/abc123/download"
}
}Webhooks are managed in the dashboard, under Dashboard → Accounts → Webhooks, not through the API. An API key cannot create, change or read webhook endpoints, so a leaked key cannot redirect your webhooks or read their signing secret.
https:// and point to a public address.whsec_...) into your server's configuration. You can show it again later from the endpoint.From the same place you can:
webhook.test event and check your server,You can have up to 10 endpoints.
Every request carries three headers:
| Header | Meaning |
|---|---|
webhook-id |
Unique message ID. The same across retries: use it to ignore duplicates. |
webhook-timestamp |
Unix time of this attempt. Reject requests more than 5 minutes old. |
webhook-signature |
v1,<base64 signature>. During a secret rotation there are two, separated by a space. |
The signature is an HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{raw body}, keyed with the base64-decoded part of your secret after whsec_. Always verify against the raw request body, before parsing the JSON.
import base64, hashlib, hmac, time
def verify(secret: str, headers: dict, raw_body: bytes) -> bool:
msg_id, ts = headers["webhook-id"], headers["webhook-timestamp"]
if abs(time.time() - int(ts)) > 300:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
expected = base64.b64encode(
hmac.new(key, f"{msg_id}.{ts}.".encode() + raw_body, hashlib.sha256).digest()
).decode()
return any(
hmac.compare_digest(sig.split(",", 1)[1], expected)
for sig in headers["webhook-signature"].split(" ")
if sig.startswith("v1,")
)Or use the official library: pip install standardwebhooks, then Webhook(secret).verify(raw_body, headers).
2xx status within 15 seconds to acknowledge a message. Do slow work after replying.Retry-After header is respected.410 Gone disables the endpoint immediately.Generate a title, description, and tags for any script using AI.
POST /api/v1/meta/generate{
"script": "...",
"platform": "youtube"
}Response:
{
"title": "5 Things Nobody Tells You About Electric Cars",
"description": "We break down the real cost, range, and daily experience of owning an EV in 2026.",
"tags": ["electric cars", "ev", "tesla", "sustainability", "auto"]
}platform can be youtube (10 tags, longer description) or tiktok (5 tags, shorter).
All errors return a JSON body with a detail field.
| Status | Meaning |
|---|---|
| 400 | Bad request, including an invalid script (detail.errors lists each problem) |
| 401 | Invalid or revoked API key |
| 402 | Insufficient credits or subscription required |
| 404 | Resource not found |
| 409 | Conflict, e.g. video not done yet |
| 422 | Request body does not match the expected shape (the detail lists each field) |
| 429 | Rate limit exceeded |
| 502 | External service unavailable (Polar, Google AI) |
Example:
{ "detail": "Insufficient credits. Balance: 0.5, required: 2.0" }Limits are per user across all API keys.
| Endpoint | Limit |
|---|---|
POST /videos |
10/min, 25/hr, 50/day |
GET /videos/{id}, GET /videos, GET /channels, GET /keys |
60/min, 500/hr, 2,000/day |
POST /videos/{id}/publish |
30/min, 300/hr, 1,000/day |
POST /files |
10/min, 25/hr, 50/day |
POST /scripts/validate, GET /files |
60/min, 500/hr, 2,000/day |
POST /meta/generate |
10/min, 60/hr |
POST /keys, DELETE /keys/{id} |
5/min, 100/hr, 200/day |
When rate-limited, the response is 429 Too Many Requests.
import time, requests
BASE = "https://inkslop.com/api/proxy/api/v1"
KEY = "isk_your_key_here"
H = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}
SCRIPT = """
[video file="video/minecraft/BXUA2FncVPI.mp4" size="content" fit="loop"]
[music file="music/Carefree.opus" volume="0.3" fit="loop"]
[tts engine="kokoro_tts" voice="af_heart" subtitles="true"]
Did you know the mantis shrimp can punch with the force of a bullet?
Their eyes have up to sixteen types of colour receptors. Humans have three.
[/tts]
[/music]
[/video]
"""
# Submit
r = requests.post(f"{BASE}/videos", headers=H, json={
"script": SCRIPT,
"video_name": "mantis-shrimp",
"resolution": "9:16",
"estimated_duration": 15,
})
r.raise_for_status()
task_id = r.json()["task_id"]
print("Task:", task_id)
# Poll until done
while True:
r = requests.get(f"{BASE}/videos/{task_id}", headers=H)
data = r.json()
print(f" {data['progress']}% - {data['message']}")
if data["status"] in ("done", "error"):
break
time.sleep(8)
if data["status"] == "done":
print("Download:", data["download_url"])
print("Credits used:", data["credits_used"])Ready to start building?