Public API v1
Veedio Public API v1
Everything the web app does, available to a program: the same script writer, the same pipeline, the same credit deposit and the same refund on failure. Six endpoints, one of which makes videos.
- https://www.veedio.co/api/v1
- Authorization: Bearer vd_live_…
- 60 req/min
Authentication
Every request carries an API key. Create one in Settings → API keys; the plaintext is shown once and only its SHA-256 hash is stored, so there is no way to recover it later — mint a new one and revoke the old. A key looks like vd_live_ followed by 43 characters of base64url.
Four headers are accepted, so a script written for another vendor's API usually works with a one-line change.
Headers
-H "Authorization: Bearer vd_live_..." # preferred
-H "x-api-key: vd_live_..."
-H "x-veedio-api-key: vd_live_..."
-H "key: vd_live_..."A missing, malformed, unknown or revoked key all answer the same way — 401 with a WWW-Authenticate: Bearer header — because saying which would tell someone whether a guessed key exists.
A key is an identity, not a scope. It can create videos, spend credits and publish to connected accounts on the owner's behalf. Treat it like a password: environment variable, never a repository, never a browser. Keys themselves are managed from the app with a session, never with a key — a leaked credential must not be able to mint its replacement.
Errors
Every failure has the same body. Branch on code; show message. The message may be reworded, the code will not.
Error body
{
"error": {
"code": "insufficient_credits",
"message": "You are out of credits. Upgrade your plan to keep generating."
}
}| Status | code | Means |
|---|---|---|
| 400 | invalid_request | The body failed validation. The message names the field. |
| 401 | unauthorized | No key, or a key that is unknown or revoked. |
| 402 | insufficient_credits | Not enough credits for the deposit. Nothing was charged; top up or move up a plan in the app, then retry. |
| 402 | plan_required | The account has no plan, and rendering starts on one. Nothing was charged; pick a plan in the app, then retry. |
| 404 | not_found | No such video — or it is not yours. |
| 409 | conflict | The video is not ready, has no rendered MP4 yet, is updating after an edit, or is already live on that platform. |
| 429 | rate_limited | Too many requests. See Retry-After. |
| 500 | internal_error | Ours. Retrying is safe. |
| 503 | generation_unavailable | Paid edits are paused while an upstream provider refuses us. Nothing was charged; retry in a few minutes. New videos are not refused: they queue and finish automatically. |
Rate limits
Per API key, sliding window: 60 requests a minute and 1,000 an hour. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining; a 429 carries Retry-After in seconds.
A refused request is not counted, so backing off works rather than extending the penalty. These bound chatter, not spend — the real limit on anything expensive is your credit balance.
Credits
A generation costs 3 credits plus a charge per scene that follows what fills it: 2 for a still or stock footage, 13 for a scene the video model sets in motion. A 5-scene short is 13 credits with stills and 138 animated.
The real cost is not knowable when you ask — the script does not exist yet — so a deposit is charged before the job is queued and settled the moment the scenes are written. creditsEstimated on the create response is the deposit; creditsSpent on the video is what was actually charged. A generation that fails and is finally abandoned is refunded in full. Publishing is free. Regenerating a scene's still or stock visual is free; a new AI-motion take is free once per video, then charged at that scene's per-scene price and refunded if it fails.
Endpoints
Six endpoints
POST/api/v1/videos
Start a generation. Answers 202 Accepted, not 201: the row exists, the video does not yet. Poll GET /api/v1/videos/{id} until status is ready or failed — a few minutes.
Body
- workflowstringRequired
- The discriminator. It decides what source must contain and how much of the writing we do.
- prompt-to-videoA brief. We write the script, generate the visuals and cut it.
- script-to-videoYour script, narrated close to verbatim. You control every sentence.
- idea-to-videoA bare idea, expanded into an angle and then a script.
- article-to-videoA link, read and turned into a short: an article, a video, a post, a thread or a PDF.
- source.textstringRequired
- Up to 5,000 characters. The brief, the idea or the script, depending on the workflow. Required for every workflow except article-to-video.
- source.urlstring
- Up to 2,000 characters, http(s). Required for article-to-video. The page is fetched and read — an article, a YouTube transcript, a post on X, a Reddit thread, a LinkedIn post or a PDF — and the script is held to what it says.
- titlestringdefault Untitled video
- Up to 120 characters. Shown in the app, and used as the caption when you publish without one.
- formatstringdefault story
- Which kind of video. Added September 2026; leave it out and the request is exactly what it was before. A `clip` takes prompt-to-video or idea-to-video (or just a photoUrl) and a clip length for targetSeconds — 5, 8, 10, 15 — is charged its exact price up front, is refunded if its take fails, and is on Starter and up. It answers 400 while Clip mode is switched off on the deployment.
- storyA narrated script cut into scenes. Every workflow and field on this page.
- clipOne short AI shot with its own sound. See sound and tier.
- chatA fake text-message conversation, bubble by bubble on a phone (added October 2026). prompt-to-video or idea-to-video writes it; script-to-video with participants and messages takes it as given. See voiceMode. 9:16 only; priced up front; answers 400 while chat videos are switched off.
- redditA Reddit post card, then the story read out with pop captions over a background loop (added October 2026). script-to-video reads source.text word for word; article-to-video reads a public Reddit post's own text; prompt-to-video writes one. See layout and backgroundId. 9:16 only; priced up front; answers 400 while Reddit videos are switched off.
- soundstringdefault native
- For a clip: where its sound comes from. Ignored for a story.
- nativeThe video model records the sound, dialogue included. No music unless musicTrack is sent.
- musicThe shot, silent, under musicTrack.
- voiceoverA line written for the shot, narrated in voice, over musicTrack.
- tierstringdefault standard
- For a clip: which video model makes the shot, and so its price. Ignored for a story.
- fastThe cheapest, up to 8 seconds (5 is shot as 6).
- standardUp to 15 seconds.
- cinematicUp to 15 seconds, the richest look. Pro and up.
- photoAttestedbooleandefault false
- For a clip from a photoUrl: true confirms the photo is of you, or of someone who gave you permission to use it. A photo clip without it answers 400 before anything is charged, and a photo that looks like it shows anyone under 18 is refused whatever it says. Ignored for a story.
- voiceModestringdefault voices
- For a chat: whether the messages are read out. Ignored for every other format.
- voicesEach person's messages in their own voice (see participants). Base price plus narration.
- silentNobody reads them; each message lands with a ding. The base price alone.
- participantsarray
- For a chat: exactly two people, `[{"name": "Mom", "side": "left", "voiceId": "bella"}, {"name": "Me", "side": "right", "voiceId": "josh"}]` — one each side (`right` is the phone's owner), different names, a catalog voice each. With messages, the thread is made as given; without, the writer names them and the voices are kept by side.
- messagesarray
- For a chat from script-to-video: 2 to 40 messages, `[{"from": "Mom", "text": "why is there a llama", "delaySec": 0.8}]` — each from one of the participants, at most 280 characters, with the pause before it (0-4 seconds). Priced by the length it will run.
- layoutstringdefault full
- For a Reddit story: where the background loop goes. Ignored for every other format.
- fullThe loop fills the frame; the card and captions sit over it.
- splitThe loop in the bottom half; the card and captions above it.
- backgroundIdstring
- For a Reddit story: which background loop, by id — one of the deployment's owned loops, or a generated one (aurora, sunset, ocean, ember) when its renderer draws them. Left out, one is picked for the video. An unknown id answers 400.
- presetstring
- A tool page's slug — `top-5-video-generator`, `storytime-video-generator`, `explainer-video-generator` and 187 others at https://www.veedio.co/tools. The page's format brief is put in front of your source before the script is written, so the video comes out in that shape. The brief itself is resolved here from the slug and is never accepted as text; an unknown slug answers 400. Each page's own brief is quoted verbatim at https://www.veedio.co/tools/{slug}/llms.txt.
- voicestringdefault auto
- The narrator. Scene length is this voice's real duration, not an estimate. All five speak every one of the 29 languages — set `language` and the same voice reads it.
- rachelWarm, conversational
- adamDeep, documentary
- bellaMature, reassuring, confident
- joshCasual, young male
- elliSoft, storytelling
- liamEnergetic social media creator
- jessicaPlayful, bright, warm
- lauraEnthusiastic, quirky
- charlieConfident, hyped
- willRelaxed optimist
- chrisCharming, down-to-earth
- rogerLaid-back, casual
- riverRelaxed, neutral, informative
- matildaKnowledgeable, upbeat explainer
- aliceClear, engaging educator
- lilyVelvety, cinematic
- georgeWarm, captivating storyteller
- danielSteady broadcaster
- brianDeep, resonant, comforting
- ericSmooth, trustworthy
- captionStylestringdefault bold-center
- Captions are word-timed from the voice model's own character alignment and burned into the MP4. `none` renders the video without them.
- bold-centerBig white text in the lower third
- popA few words at a time, the spoken word in yellow, lower third
- highlightWord-by-word yellow highlight, lower third
- bounceOne big word at a time, each popping in, lower third
- karaokeThe line fills in as it's spoken, lower third
- neonA few words at a time, the spoken word big and green, lower third
- boxedClean sentence case on a dark box, lower third
- subtleSmall caption near the bottom
- noneNo captions
- musicTrackstringdefault uplifting
- A licensed track, ducked under the narration and loudness-normalised with it. `none` leaves the voice alone.
- noneSilence
- scoreOriginal music composed for this video
- upliftingBright, motivational
- lofiCalm, chill
- cinematicTense, epic
- trapPunchy, modern
- visualStylestringdefault auto
- Appended to the image prompt for every scene, so the whole video holds one look. Applies to generated frames; see mediaType for stock footage.
- cinematicCinematic
- 3d3D render
- animeAnime
- documentaryDocumentary
- retroRetro
- toyToy bricks
- voxelVoxel world
- clayClaymation
- comicComic
- watercolorWatercolor
- aspectRatiostringdefault 9:16
- The render is 1080-class in every ratio, with no watermark on any plan.
- 9:16Vertical — TikTok, Reels, Shorts
- 1:1Square — Feed posts
- 16:9Landscape — YouTube
Request
Response
202 Accepted
{
"id": "8f1c0f2e-8a4d-4f2b-9e2c-2f1a0b7c9d31",
"status": "queued",
"creditsEstimated": 138,
"url": "https://www.veedio.co/videos/8f1c0f2e-8a4d-4f2b-9e2c-2f1a0b7c9d31"
}Status codes
- 202queued
- 400invalid_request
- 401unauthorized
- 402insufficient_credits, or plan_required with no plan
- 429rate_limited
- 503generation_unavailable
One endpoint, many workflows: new ones arrive as new workflow values rather than as new URLs, so a client written against this shape keeps working. Each workflow has its own page — start with prompt-to-video.
GET/api/v1/videos/{id}
Status and scenes. Scenes fill in as they are produced, so a client can show progress rather than a spinner.
Request
Response
200 OK
{
"id": "8f1c0f2e-8a4d-4f2b-9e2c-2f1a0b7c9d31",
"status": "ready",
"renderStatus": "idle",
"format": "story",
"title": "Why most indie launches fail",
"workflow": "prompt-to-video",
"createdAt": "2026-09-06T09:12:44.000Z",
"videoUrl": "https://.../final.mp4",
"thumbnailUrl": "https://.../thumb.jpg",
"durationSeconds": 31.4,
"creditsSpent": 12,
"error": null,
"url": "https://www.veedio.co/videos/8f1c0f2e-8a4d-4f2b-9e2c-2f1a0b7c9d31",
"scenes": [
{
"position": 0,
"status": "ready",
"text": "Most indie founders launch to nobody.",
"imageUrl": "https://.../5c1e0a9d-.../image-4b7e21c09f3a.jpg",
"clipUrl": null,
"audioUrl": "https://.../5c1e0a9d-.../audio-a02d6f1e88c4.mp3",
"durationSeconds": 4.2
}
]
}status is one of queued, processing, ready, failed. videoUrl is null until the render finishes; error says why when the status is failed. renderStatus is an edit's re-render of a finished video — idle, queued, rendering or failed — and the video stays ready while it runs, with videoUrl the previous cut until the new one lands. Poll no more than once every few seconds — generation takes minutes and the rate limit is per key.
- 200ok
- 401unauthorized
- 404not_found
GET/api/v1/videos
Your videos, newest first.
Query
- limitintegerdefault 20
- How many to return. Caps at 100. Newest first.
Request
cURL
curl "https://www.veedio.co/api/v1/videos?limit=10" \
-H "Authorization: Bearer $VEEDIO_API_KEY"Response
200 OK
{
"videos": [
{
"id": "8f1c0f2e-...",
"status": "ready",
"title": "Why most indie launches fail",
"url": "https://www.veedio.co/videos/8f1c0f2e-..."
}
]
}POST/api/v1/videos/{id}/publish
Publish a finished video to connected accounts. 202: each platform gets its own upload job, so a TikTok rejection does not hold up YouTube.
Body
- platformsstring[]Required
- Where to publish. At least one.
- tiktokTikTok
- instagramInstagram Reels
- youtubeYouTube Shorts
- captionstring
- Applies to every platform. Defaults to the video's title. Up to 2,200 characters.
- forcebooleandefault false
- Publish again to a platform this video is already live on. Leave it off when retrying a call you are unsure landed — a second post is visible to the creator's audience.
Request
Response
202 Accepted
{
"publications": [
{
"platform": "tiktok",
"status": "pending",
"url": null
}
]
}Refusals
- The video must be ready and have a rendered MP4 — otherwise 409.
- The platform must already be connected in Settings — otherwise 400, naming it.
- The video must not already be live on that platform — otherwise 409.
That last one is what makes this call safe to retry: an agent that is not sure its request landed can send it again without putting a second copy on the creator's account. Publishing costs no credits.
POST/api/v1/credits/estimate
Quote a generation, and read the balance, without spending anything. The endpoint ignores fields it does not use, so you can send the same body you would send to POST /api/v1/videos.
Body
- sceneCountinteger
- Without it you get the default quote, which is what the deposit assumes — the response says so with "estimated": true.
- formatstringdefault story
- As on POST /api/v1/videos. A story is quoted as it always was. A clip is quoted exactly from targetSeconds, sound and tier — the price creating it charges — and answers 400 while Clip mode is switched off, rather than a story's price.
Request
Response
200 OK
{
"credits": 165,
"sceneCount": 6,
"estimated": false,
"breakdown": {
"base": 3,
"perScene": 27
},
"clipSeconds": null,
"upperBound": false,
"balance": 188
}GET/api/v1/me
Whose key this is, their plan and their balance. The cheapest way to check a key works.
Request
cURL
curl https://www.veedio.co/api/v1/me -H "Authorization: Bearer $VEEDIO_API_KEY"Response
200 OK
{
"email": "creator@example.com",
"plan": "pro",
"planName": "Pro",
"credits": 188,
"creditsRenewAt": "2026-10-01T00:00:00.000Z"
}A complete flow
Quote, create, wait, publish. The CLI is these four steps behind one command.
bash
export VEEDIO_API_KEY=vd_live_...
# 1. What will it cost?
curl -s -X POST https://www.veedio.co/api/v1/credits/estimate \
-H "Authorization: Bearer $VEEDIO_API_KEY" | jq .credits
# 2. Start it.
id=$(curl -s -X POST https://www.veedio.co/api/v1/videos \
-H "Authorization: Bearer $VEEDIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"workflow":"prompt-to-video","source":{"text":"why indie launches fail"}}' \
| jq -r .id)
# 3. Wait for it.
until [ "$(curl -s https://www.veedio.co/api/v1/videos/$id \
-H "Authorization: Bearer $VEEDIO_API_KEY" | jq -r .status)" = "ready" ]; do
sleep 10
done
# 4. Ship it.
curl -s -X POST https://www.veedio.co/api/v1/videos/$id/publish \
-H "Authorization: Bearer $VEEDIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"platforms":["tiktok"]}'Or: veedio create --prompt "…" --wait --json — see the CLI reference.
Before you build, read what this API cannot do: no file posted up the wire (text and URLs only), no face swap or voice cloning, nothing longer than three minutes, no 4K, and no completion webhook yet.