1. What You Can Do with the Dayvid API
Dayvid (https://dayvid.ai) is a music-to-video creation platform, and this API lets you drive it end to end. Give it an audio track (a Suno song, a Udio track, a SoundCloud track, or any MP3/WAV you own) and you can:
- Create a full music video: one POST returns a rendered MP4 with auto-transcribed, time-aligned subtitles in a chosen style, AI-generated scene imagery driven by the lyrics (or a curated video-loop / your own static background), vertical (Reels/Shorts/TikTok) or horizontal (YouTube) framing, and an optional outro (section 4)
- Cut one song into several short highlight clips, each a standalone ready-to-post MP4 (section 11)
- Manage your projects: list, search, and inspect them, including render state and download URLs (section 10)
- Publish the result to social platforms like YouTube in one call, custom thumbnail included (section 12)
The API exposes the same "Express" one-shot pipeline that powers the web app. One POST submits a job; you receive an email when the render finishes and can fetch the signed download URL via a single status request.
2. Account + API Token Setup
You need two things before any curl call works: an account and a personal API token.
2.1. Create the account
- Open https://dayvid.ai
- Click Sign in (top right)
- Choose Continue with Google and complete the OAuth consent screen
- You land on the dashboard
2.2. Subscribe to an API-capable plan
The Free and Lite tiers do NOT include API access. See section 3 for the rule. Pick Start, Pro, or King from the pricing page: https://dayvid.ai/pricing
2.3. Mint a token
- Go to Profile: https://dayvid.ai/profile
- Scroll to the API tokens card
- Click Create token, give it a label (e.g.
cli-laptop), confirm - Pick an expiration. The dialog defaults to 90 days; choose "Never" explicitly if you want a token that does not expire on its own (full lifecycle rules in section 17).
- Copy the
dvy_...value SHOWN ONCE. Store it in a secret manager. We never display it again. - Export it for the rest of this guide:
export DAYVID_TOKEN="dvy_xxxxxxxxxxxxxxxxxxxxxxxx"
export DAYVID_BASE="https://dayvid.ai"
All requests below send Authorization: Bearer $DAYVID_TOKEN.
3. Plans That Can Use the API + The /me Health Check
API access is part of paid Dayvid plans. Free and Lite subscribers can mint a token but every endpoint except /me will reject the request with a 402 telling them to upgrade.
| Plan | Can call the API | Monthly credits |
|---|---|---|
| Free | No | (trial only) |
| Lite | No | 700 |
| Start | Yes | 1,500 |
| Pro | Yes | 4,000 |
| King | Yes | 10,000 |
Upgrade at https://dayvid.ai/pricing.
GET /api/v1/me is the recommended health check. It returns your current plan, your remaining credits, and confirms that your token is valid. It is open to every authenticated caller (including Free/Lite) so you can detect an upgrade requirement before submitting.
curl -s "$DAYVID_BASE/api/v1/me" \
-H "Authorization: Bearer $DAYVID_TOKEN" | jq
Response:
{
"userId": "uuid",
"token": { "id": "uuid", "name": "cli-laptop" },
"subscription": {
"status": "active",
"plan": { "slug": "start", "name": "Start", "apiAccess": true },
"currentPeriodEnd": "2026-06-22T00:00:00.000Z"
},
"credits": 1483
}
The boolean inside subscription.plan tells you whether the current plan can call the API. Always confirm it is true and that credits is at least the cost of one run (a typical Express job reserves around 100 credits) before submitting.
Budgeting a run: there is no dry-run estimate endpoint today. The exact reservation for your submitted options comes back in the submit response as estimateSnapshot (see 4.5), and the job is rejected with 402 insufficient_credits (including needed and balance) before anything is charged if you cannot afford it. As a rule of thumb, cost grows with track length, with visualStyle: "moving" (and higher animationStop tiers), and with resolutionPreset: "2K"; videoLoop and static + your own image are the cheapest paths because nothing is AI-generated.
4. The Express Submit Flow
Producing a video is two phases:
- Ingest audio so the server has a stable storage path it can read
- Submit settings to
POST /api/v1/express. You receive ajobIdimmediately; the render completes in 10-40 minutes and we notify you by email.
Two ways to ingest audio:
- Paste a public link from Suno, Udio, or SoundCloud as
audioUrl - Upload your own audio via a signed URL, then pass the resulting
storagePath
/api/v1/express accepts exactly one of audioStoragePath or audioUrl.
4.1. Path A: Suno link
Paste the share or song URL from Suno. Both shapes work:
https://suno.com/s/<short>(the "Copy link" share URL)https://suno.com/song/<uuid>(the canonical song page)
curl -s -X POST "$DAYVID_BASE/api/v1/express" \
-H "Authorization: Bearer $DAYVID_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"settings": {
"projectName": "Suno demo",
"audioUrl": "https://suno.com/s/E6uWAXuAvVlQyJR6",
"aspectRatio": "9:16",
"brandId": null,
"assetStrategy": "regenerate",
"visualStyle": "moving",
"animationStop": 2,
"subtitlePresetName": "Punchy",
"generationPresetId": "default",
"delivery": "auto-render"
}
}' | jq
4.2. Path B: Udio link
Paste the song URL from Udio: https://www.udio.com/songs/<id>.
curl -s -X POST "$DAYVID_BASE/api/v1/express" \
-H "Authorization: Bearer $DAYVID_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"settings": {
"audioUrl": "https://www.udio.com/songs/uXYZabcDEF",
"aspectRatio": "16:9",
"brandId": null,
"assetStrategy": "regenerate",
"visualStyle": "moving",
"animationStop": 2,
"subtitlePresetName": "Glow",
"generationPresetId": "ghibli",
"delivery": "auto-render"
}
}' | jq
4.3. Path C: SoundCloud link
Paste the track URL: https://soundcloud.com/<artist>/<track>.
curl -s -X POST "$DAYVID_BASE/api/v1/express" \
-H "Authorization: Bearer $DAYVID_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"settings": {
"audioUrl": "https://soundcloud.com/artist-name/track-name",
"aspectRatio": "9:16",
"brandId": null,
"assetStrategy": "regenerate",
"visualStyle": "moving",
"animationStop": 2,
"subtitlePresetName": "Punchy",
"delivery": "auto-render"
}
}' | jq
4.4. Path D: Custom audio upload (MP3, WAV, FLAC, M4A, etc.)
POST /api/v1/audio-uploads returns a one-time signed PUT URL for Supabase Storage. The bytes never touch the Dayvid API handler. The request body needs only fileName (with an extension). Accepted types: audio/mpeg, audio/mp3, audio/wav, audio/x-wav, audio/mp4, audio/aac, audio/flac, audio/ogg. Maximum size: 100 MB.
Step 1: request the signed URL.
FILE="/path/to/song.mp3"
curl -s -X POST "$DAYVID_BASE/api/v1/audio-uploads" \
-H "Authorization: Bearer $DAYVID_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "fileName": "song.mp3" }' | tee /tmp/upload.json | jq
SIGNED_URL=$(jq -r .signedUploadUrl /tmp/upload.json)
STORAGE_PATH=$(jq -r .storagePath /tmp/upload.json)
Step 2: PUT the bytes to the signed URL.
curl -s -X PUT "$SIGNED_URL" \
-H "Content-Type: audio/mpeg" \
--data-binary "@$FILE"
Step 3: submit using audioStoragePath.
curl -s -X POST "$DAYVID_BASE/api/v1/express" \
-H "Authorization: Bearer $DAYVID_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"settings\": {
\"audioStoragePath\": \"$STORAGE_PATH\",
\"aspectRatio\": \"9:16\",
\"brandId\": null,
\"assetStrategy\": \"regenerate\",
\"visualStyle\": \"moving\",
\"animationStop\": 2,
\"subtitlePresetName\": \"Punchy\",
\"generationPresetId\": \"default\",
\"delivery\": \"auto-render\"
}
}" | jq
4.5. Submit response
All paths return the same 202 payload:
{
"projectId": "uuid",
"jobId": "uuid",
"estimateSnapshot": {
"total": 95,
"reservedAtSubmit": 100,
"marginCredits": 5,
"breakdown": [...]
},
"pollUrl": "/api/v1/jobs/uuid"
}
reservedAtSubmit is how many credits we lock the moment the job is enqueued. Any unused credits are refunded when the job completes.
4.6. Custom static background image (bring your own still)
By default a static visual style generates the background image with AI. To use your OWN image as the static background instead, upload it first and reference its storage path on submit. Two steps, mirroring the audio upload (4.4) and the YouTube thumbnail (12.4) flows — they all use the same general-purpose image endpoint.
Step 1: upload the image. POST /api/v1/image-uploads returns a one-time signed PUT URL. The request body needs only fileName (with an extension); the bytes never touch the API handler.
IMG="/path/to/background.png"
curl -s -X POST "$DAYVID_BASE/api/v1/image-uploads" \
-H "Authorization: Bearer $DAYVID_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "fileName": "background.png" }' | tee /tmp/img.json | jq
IMG_SIGNED_URL=$(jq -r .signedUploadUrl /tmp/img.json)
BG_PATH=$(jq -r .storagePath /tmp/img.json)
curl -s -X PUT "$IMG_SIGNED_URL" \
-H "Content-Type: image/png" \
--data-binary "@$IMG"
Step 2: submit with visualStyle: "static", staticImageSource: "upload", and backgroundImageStoragePath.
curl -s -X POST "$DAYVID_BASE/api/v1/express" \
-H "Authorization: Bearer $DAYVID_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"settings\": {
\"audioStoragePath\": \"$STORAGE_PATH\",
\"aspectRatio\": \"9:16\",
\"brandId\": null,
\"visualStyle\": \"static\",
\"staticImageSource\": \"upload\",
\"backgroundImageStoragePath\": \"$BG_PATH\",
\"subtitlePresetName\": \"Punchy\",
\"delivery\": \"auto-render\"
}
}" | jq
generationPresetId is not needed here: in upload mode no image is generated, so the preset is ignored. Because the AI background generation is skipped, this run reserves fewer credits than a generated static run (only transcription carries cost) — you will see a smaller estimateSnapshot.
Background image requirements (distinct from the YouTube thumbnail caps in 12.4):
- It must be an image you uploaded — referencing a path that is not yours returns
400 invalid_settings(background path is not owned by the requester). - It must already be uploaded — if the PUT has not landed yet,
400 invalid_settings(background image not found). - It must be
≤ 25 MBand one ofimage/jpeg,image/png,image/webp(webp IS accepted here, unlike YouTube thumbnails), else400 invalid_settings(background image too large/background image type not supported).
Combination errors (rejected before any work): staticImageSource: "upload" without backgroundImageStoragePath → 400 static_image_source_path_missing; a backgroundImageStoragePath without staticImageSource: "upload" → 400 background_path_requires_upload_source; either field on a non-static visualStyle → 400 static_image_source_requires_static_style.
5. Settings Reference
Every Express submit accepts the fields below. They mirror the controls in the Dayvid web app's Express form, so anything you can do there you can do here.
5.1. projectName (optional, string)
Display name for the project. If omitted, the server uses Untitled Express and the auto-namer rewrites it from the transcript after first segment processes.
5.2. audioStoragePath XOR audioUrl (required, one of)
See section 4. Exactly one must be present.
5.3. outputLanguage (optional, string)
BCP-47 code like en or pt-BR. When omitted, transcription auto-detects.
5.4. aspectRatio (required, "9:16" | "16:9")
9:16: vertical, for Reels / Shorts / TikTok16:9: horizontal, for YouTube and landscape players
Optional when you supply presetId (see 5.16): the preset's saved aspect ratio is used as the fallback. If the preset has no aspect ratio AND you omit it on the request, you get 400 aspect_ratio_required.
5.5. brandId (required, string | null)
UUID of one of your brands. When set, the brand's visual-style text and reference images steer scene generation, and brand fonts become available for subtitles (see section 9 for the full setup). Use null to skip brand injection. Get the UUID from your brand's page at https://dayvid.ai/brands, or programmatically via GET /api/v1/brands (see 8).
5.6. assetStrategy (optional, "reuse" | "regenerate", default "regenerate")
Only relevant when brandId is set. Without a brand, the field is ignored and treated as regenerate.
regenerate: always synthesize fresh AI images for every scene. Costs more credits.reuse: when scenes resemble images already in your brand catalog, recycle them. Cheaper, faster.
5.7. visualStyle (required, "moving" | "static" | "videoLoop")
moving: scene images are animated (Ken Burns / parallax). Most cinematic.static: a single still image is the whole background. By default it is AI-generated; setstaticImageSource(5.7a) to supply your own image instead. Lowest credit cost.videoLoop: ignore generated imagery; use a curated MP4 loop background. RequiresvideoLoopPresetSlug.
5.7a. staticImageSource (optional, "generated" | "upload", default "generated")
Only meaningful when visualStyle === "static". "generated" (the default when omitted) brainstorms and renders an AI cover from the lyrics. "upload" uses your own image, supplied via backgroundImageStoragePath (5.7b), and skips AI background generation entirely (cheaper). Sending this field on a non-static visualStyle returns 400 static_image_source_requires_static_style.
5.7b. backgroundImageStoragePath (required when staticImageSource === "upload")
Storage path of an image you uploaded via POST /api/v1/image-uploads (see 4.6 for the full flow). Must be owned by you ({userId}/ prefix), already uploaded, ≤ 25 MB, and one of image/jpeg, image/png, image/webp. Ignored unless staticImageSource === "upload"; sending it without that source returns 400 background_path_requires_upload_source, and sending "upload" without this path returns 400 static_image_source_path_missing. See 4.6 for all error codes and requirements.
5.8. videoLoopPresetSlug (required when visualStyle === "videoLoop")
Curated background catalog:
| Slug | Vibe |
|---|---|
train |
First-person train ride |
snow-pine |
Snowy pine forest |
three-moons |
Cosmic three-moon sky |
nyc-taxi |
New York taxi window POV |
submersible |
Deep-sea submersible window |
dancefloor |
Neon dancefloor |
5.9. animationStop (required only when visualStyle === "moving", 0 | 1 | 2 | 3 | 4)
Animation tier for the moving pipeline: how many scene shots get animated into video clips. 0 = no animation, higher tiers animate progressively more shots (and cost more credits). Required for "moving" (omitting it, or sending null, returns 400 invalid_settings with path: "settings.animationStop"). The "static" and "videoLoop" styles skip the animate stage entirely, so the field is ignored there: omit it or send null.
5.10. subtitlePresetName (required when subtitleStyleOverride is omitted)
Case-sensitive. One of: Basic, Shadow, Glow, White, Base Blue, Gold, Elegant, Opacity, Playful, Punchy, Focus, Spotlight.
Mutex with subtitleStyleOverride (see 5.13): send subtitlePresetName OR subtitleStyleOverride, never both. Sending both returns 400 subtitle_source_conflict. Sending neither returns 400 subtitle_source_missing.
The "neither" rule relaxes when you supply presetId (see 5.16): the preset's saved subtitle style fills the slot, so you can omit both. If you do send one of them with presetId, your explicit choice wins over the preset's style. Sending both is still rejected with 400 subtitle_source_conflict.
5.11. generationPresetId (optional, string, default "default")
Visual style for AI scene generation. One of: default, ghibli, pixar, anime, sketch-color, sketch-bw, lego, sci-fi, retro-cartoon, pixel-art, anime-realism, fantasy, movie, kids-book, minecraft, new-yorker-cartoon, 1950s-ad, renaissance-fresco, modern-noir, expressive-ink, cosmic-baroque, epic-lineburst, claymation, photography, illustration.
Ignored (and not validated) when no AI image is generated: visualStyle === "videoLoop", or visualStyle === "static" with staticImageSource === "upload". Omit it in those cases.
5.12. visualGuidelines (optional, string, max 3000 chars)
Freeform instructions injected into the image prompt: characters, palette hints, locations. Example: "Anime protagonist with red hoodie, tokyo neon streets, rainy".
5.13. subtitleStyleOverride (optional, object | null)
Full custom subtitle style. Replaces the preset entirely — there is no field-level merge. When you supply this object, omit subtitlePresetName (see 5.10 mutex rule). The renderer reads every field of this object verbatim; missing optional fields stay missing in the rendered output (no silent inheritance from any preset).
Prototype it visually: the subtitle style playground edits this exact JSON shape with a live caption preview and the same validation this endpoint applies, then gives you a copy-ready snippet.
Required fields (11)
| Field | Type | Range / Enum |
|---|---|---|
fontFamily |
string | One of the names from the font catalog below. Case-sensitive. |
fontSize |
number | 8 - 200 |
textColor |
string | CSS color, ≤ 64 chars |
outlineColor |
string | CSS color, ≤ 64 chars |
outlineWidth |
number | 0 - 40 |
shadowColor |
string | CSS color, ≤ 64 chars |
shadowDistance |
number | 0 - 40 |
shadowBlur |
number | 0 - 80 |
animationIn |
object | { type, durationMs } — see ElementAnimation below |
animationOut |
object | { type, durationMs } — see ElementAnimation below |
vttDisplayMode |
enum | "highlight" | "word" | "reveal" |
Optional fields (16)
| Field | Type | Range / Enum |
|---|---|---|
backgroundColor |
string | CSS color, ≤ 64 chars |
textTransform |
enum | "none" | "uppercase" | "lowercase" |
fontWeight |
number | 100 - 900 |
x |
number | 0 - 100 (% horizontal position) |
y |
number | 0 - 100 (% vertical position) |
maxWidth |
number | 10 - 100 |
highlightColor |
string | CSS color, ≤ 64 chars |
highlightBackgroundColor |
string | CSS color, ≤ 64 chars |
highlightFontSize |
number | 8 - 200 |
highlightFontWeight |
number | 100 - 900 |
opacity |
number | 0 - 100 |
highlightOpacity |
number | 0 - 100 |
glow |
number | 0 - 100 |
highlightGlow |
number | 0 - 100 |
unrevealedOpacity |
number | 0 - 100 |
wordsPerPage |
number | 1 - 20 |
ElementAnimation shape
{ "type": "fade", "durationMs": 200 }
type is one of: fade, slide, scale, none, vapor, blur, bounce, flip, zoom, wipe, spin, reveal, dissolve, stagger. durationMs is 0 - 10000. Optional direction (when applicable): "up" | "down" | "left" | "right".
Font catalog (fontFamily values)
fontFamily is validated against three sets. Names not in any set return 422 font_not_available with the full list of valid names in the response body. Case-sensitive.
Built-in Google Fonts (13, always available):
Lexend, Roboto, Poppins, Montserrat, Open Sans, Oswald, Bangers, Anton, DynaPuff, Bebas Neue, Libre Baskerville, Roboto Slab, Inter.
System fonts (9, available on the renderer):
Arial, Georgia, Impact, Courier New, Verdana, Times New Roman, Comic Sans MS, Trebuchet MS, Palatino.
Brand custom fonts: when you submit a brandId you own, any fonts uploaded to that brand from the web UI (https://dayvid.ai/brands/<brandId>, Fonts section) are also accepted. The 422 font_not_available response lists them under availableBrand so you can discover what is connected.
Example: Punchy as a starting point, with cyan text and a thicker outline
Note that subtitlePresetName is omitted (mutex). The override below is the canonical Punchy style (src/components/flows/shared/subtitles/state/subtitlePresets.ts, as of 2026-05) with two fields tweaked.
curl -s -X POST "$DAYVID_BASE/api/v1/express" \
-H "Authorization: Bearer $DAYVID_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"settings": {
"audioUrl": "https://suno.com/s/E6uWAXuAvVlQyJR6",
"aspectRatio": "9:16",
"brandId": null,
"assetStrategy": "regenerate",
"visualStyle": "moving",
"animationStop": 2,
"generationPresetId": "default",
"delivery": "auto-render",
"subtitleStyleOverride": {
"fontFamily": "Bebas Neue",
"fontSize": 108,
"textColor": "#00bfff",
"outlineColor": "#000000",
"outlineWidth": 8,
"shadowColor": "#000000",
"shadowDistance": 0,
"shadowBlur": 0,
"animationIn": { "type": "scale", "durationMs": 200 },
"animationOut": { "type": "fade", "durationMs": 150 },
"vttDisplayMode": "reveal",
"fontWeight": 700,
"wordsPerPage": 3
}
}
}' | jq
Re-render behavior
On re-render of an existing project (e.g. via POST /api/v1/render after an Express submit), the renderer reads the subtitle style from the project's stored slot, not from a subtitleStyleOverride you might send. Submit the override on the original /api/v1/express call; subsequent renders of that project will reuse it.
Source of truth
Field ranges and the canonical preset list live in src/lib/services/express/schemas.ts and src/components/flows/shared/subtitles/state/subtitlePresets.ts. The 422 response always returns the current font catalog, so when in doubt copy the names from there rather than this doc.
5.14. delivery (required, "auto-render" | "review-first")
auto-render: pipeline runs to completion, returns a finished MP4review-first: pipeline stops after asset generation; you must explicitly callPOST /api/v1/renderto produce the MP4
5.15. resolutionPreset (optional, "1080p" | "2K", default "1080p")
Final render resolution.
1080p(default): 1920x1080 (16:9) or 1080x1920 (9:16). The historical default. Use this for Reels / Shorts / TikTok / standard YouTube.2K: 2560x1440 (16:9) or 1440x2560 (9:16). Higher fidelity, sharper text and AI imagery.
Credit cost goes up with 2K when the run actually generates AI images — that is, visualStyle: "moving" or visualStyle: "static" with the default (generated) cover. The estimateSnapshot.reservedAtSubmit in the submit response reflects the higher tier automatically. Runs that don't generate imagery (videoLoop, or static with staticImageSource: "upload") cost the same in 1080p and 2K.
Omit the field to keep the historical 1080p behavior. Sending anything other than "1080p" or "2K" returns 400 invalid_settings.
5.16. presetId (optional, UUID)
Apply a preset you saved from the Dayvid web app's Single Track creator. A preset captures a re-usable "look" so you don't repeat the same overlays / outro / subtitle style on every submit.
What the preset supplies
When presetId is set, the preset fills the slots below as the BASE; any explicit field you send on the same request OVERRIDES the preset for that slot.
- Overlays and outro: the layers you saved (logo badges, banners, an outro card, etc.) are added to the render.
- Background animation: the in/out animation saved on the preset.
- Subtitle style (base): the subtitle styling saved on the preset fills
subtitle.style. SendingsubtitlePresetNameorsubtitleStyleOverrideon the same request overrides it. - Aspect ratio (fallback): used if you omit
aspectRatioon the request.
The preset never overrides visualStyle or resolutionPreset — those always come from the request.
Fields that become OPTIONAL under presetId
aspectRatio— falls back to the preset's saved aspect ratio. Either source has to provide one; otherwise400 aspect_ratio_required.subtitlePresetName/subtitleStyleOverride— the preset supplies the subtitle style. If you DO send one, it overrides. Sending both still returns400 subtitle_source_conflict.
How to get a preset UUID
You create and manage presets in the web app: open the Single Track creator, set up the look you want (subtitles, overlays, outro, aspect ratio), then click "Save as preset" on the preset card. The web app lists every preset you saved on that same picker.
There is no public listing endpoint yet. If you need the UUID for API integration today, contact Dayvid support with the preset name and we'll send it back. A GET /api/v1/presets listing endpoint is on the roadmap; once it lands, this section will show the curl call.
Errors
404 preset_not_found— the UUID doesn't exist OR the preset isn't yours. Same response for both; we don't confirm whether somebody else's preset exists.400 preset_wrong_type— the UUID is a preset you own, but it belongs to a different project type (only Single Track presets work on/api/v1/express). Body:{ "expected": "single-track", "actual": "<type>" }.400 preset_element_forbidden— the preset references an asset that isn't yours. Rare; usually means the preset row was hand-edited. Body:{ "forbidden": [<paths>] }.400 preset_asset_unavailable— the preset references an asset that has been deleted from storage. Re-save the preset in the web app to refresh it. Body:{ "missing": [<paths>] }.
Notes
presetIdandIdempotency-Keyplay well together: any preset-related error happens BEFORE the idempotency key is committed, so a failed retry with a corrected request reuses the key cleanly.presetIdis independent ofbrandId: you can combine them. The preset supplies the overlay/outro/subtitle look;brandIdstill steers scene generation whenassetStrategy === "reuse".
6. Job Status and Download
A full Express run takes between 10 and 40 minutes depending on track length, visual style, and provider load. Do NOT busy-poll the job in a tight loop. We email you when the render finishes (success or failure), and the email links straight to the download page on the web app.
There are no webhooks or callbacks today: the two completion signals are the email and polling GET /api/v1/jobs/:id. If your integration needs push-style notifications, poll on a relaxed schedule (every few minutes).
Job status is one of:
| Value | Meaning |
|---|---|
pending |
Queued, not started yet. |
processing |
Running. progress (0-100) and progressMessage tell you which stage it is in. |
completed |
Finished. downloadUrl is present (see below). |
failed |
Terminal error. userError carries a human-readable reason. Cancelled jobs also land here. |
partial |
Finished with some outputs missing (collection runs like Music Highlights, see section 11). |
Telling "slow" from "stuck": while status is processing, a moving progress or a changing progressMessage between polls means the run is alive, just slow. Only failed is a dead end. If a job sits at the same progress far beyond the 40-minute ceiling, contact support with the jobId instead of re-submitting (a re-submit reserves credits again).
If your integration needs the MP4 URL programmatically, query the job once you receive the email notification, or schedule a status check well after submit:
JOB_ID="paste-from-submit-response"
curl -s "$DAYVID_BASE/api/v1/jobs/$JOB_ID" \
-H "Authorization: Bearer $DAYVID_TOKEN" | jq
When status === "completed", the response includes downloadUrl (a short-lived signed URL):
{
"id": "uuid",
"status": "completed",
"progress": 100,
"progressMessage": "Render finished",
"userError": null,
"resultPath": "user-id/renders/abc.mp4",
"publicMetadata": { "durationMs": 187_000 },
"downloadUrl": "https://...supabase.co/...signed..."
}
downloadUrl is short-lived — re-query the same job id to get a fresh one.
If you absolutely need to poll without waiting for the email, do so at most once every 30 seconds. The rate limit is 60 GET/min per user, but most of that headroom is for transient retries, not for tight loops over a job that takes tens of minutes.
6.1. Re-rendering a project: POST /api/v1/render
Starts a new render of a project that already exists (created via /v1/express or in the web app). Use it after editing the project in the web editor, or to retry a render.
curl -s -X POST "$DAYVID_BASE/api/v1/render" \
-H "Authorization: Bearer $DAYVID_TOKEN" \
-H "Content-Type: application/json" \
-d "{ \"projectId\": \"$PROJECT_ID\" }" | jq
Returns 202 with { jobId, projectId, statusUrl, message }. Poll statusUrl exactly like any other job (section 6). Re-rendering an already-rendered project is allowed and produces a new MP4; the project detail endpoint (10.7) always points at the latest finished render.
Errors: 400 missing projectId, 404 project not found or not yours (same response in both cases). Rate limit: 10 POST/min per user.
7. Cancelling a Job
curl -s -X PATCH "$DAYVID_BASE/api/v1/jobs/$JOB_ID" \
-H "Authorization: Bearer $DAYVID_TOKEN" | jq
Returns { "success": true }. Cancellation refunds reserved credits. Rate limit: 10 PATCH/min per user.
8. Discovering Your Brands
GET /api/v1/brands lists every brand owned by the caller. Use it once after token setup to grab the brandId values you pass into POST /api/v1/express (when you want brand-font subtitles, see 5.13) and POST /api/v1/publish (where brandId is required). The same response tells you which brand fonts you can drop into subtitleStyleOverride.fontFamily and which social platforms each brand has connected.
curl -s "$DAYVID_BASE/api/v1/brands" \
-H "Authorization: Bearer $DAYVID_TOKEN"
Returns 200 with a bare JSON array (no envelope), ordered by most recently updated. Up to 100 brands are returned; pagination is not currently supported.
[
{
"id": "0a48e9b7-9b3e-4e3a-9c2f-1f9d8d2e7c10",
"name": "Acme Records",
"createdAt": "2026-04-12T08:14:22.000Z",
"url": "https://dayvid.ai/brands/0a48e9b7-9b3e-4e3a-9c2f-1f9d8d2e7c10",
"fonts": ["Acme Display", "Acme Body"],
"integrations": {
"youtube": { "connected": true },
"instagram": { "connected": false }
}
}
]
8.1. Field reference
| Field | Meaning |
|---|---|
id |
The UUID to pass as brandId in other endpoints. |
name |
Human-readable brand label (not unique, may collide across users). |
createdAt |
ISO-8601 timestamp the brand was created. The array is sorted by a different (server-internal) updated_at key, not by this one. |
url |
The brand's page on the Dayvid web app. Open this in a browser to manage the brand — rename it, update the logo, connect OAuth, upload fonts, etc. Treat it as opaque: there is no "API surface" reachable from this URL. |
fonts |
Brand-uploaded fonts available for subtitleStyleOverride.fontFamily. Strings; case-sensitive. Built-in Google Fonts and system fonts listed in 5.13 are always available even when this list is empty. |
integrations.<provider>.connected |
true when the brand has an active OAuth row for that provider — i.e. publishing to that provider will not return oauth_required. false when the row is missing or expired. |
8.2. Which providers appear
Only providers that are currently enabled for the caller appear in integrations. Today this typically means youtube (and Instagram once it ships to your account). If you call POST /api/v1/publish for a provider that is not listed here, the publish endpoint returns 422 platform_not_available. If it is listed but connected: false, publish returns 422 oauth_required with the same url so you can send your user to connect.
The set of enabled providers is per-user — a teammate with a different rollout flag may see a different integrations shape for the same brand.
8.3. Empty cases
- New account with no brands →
[](status 200, not 404). Create one in the web UI first. - Brand with no uploaded fonts →
"fonts": []. Built-in fonts still work insubtitleStyleOverride.fontFamily. - Brand with no connected platforms →
"integrations": { "youtube": { "connected": false } }(still emits enabled providers withconnected: false).
Rate limit: 30 GET/min per user, 60 GET/min per IP.
8.4. Listing a Brand's Assets (Reference Images)
GET /api/v1/brands/{brandId}/assets lists the reference images attached to a brand. These are the images you upload in the web UI under a brand's "Reference Images" section — each one has a name, an optional free-text description, and an optional whenToUse hint. The AI scene generators read those hints to decide which reference fits a given lyric, which is exactly what the assetStrategy: "reuse" flow (see 5.6) draws on. Use this endpoint to discover, by name, what a brand has to work with, and to fetch the actual image bytes.
BRAND_ID="0a48e9b7-9b3e-4e3a-9c2f-1f9d8d2e7c10"
curl -s "$DAYVID_BASE/api/v1/brands/$BRAND_ID/assets" \
-H "Authorization: Bearer $DAYVID_TOKEN" | jq
Returns 200 with a bare JSON array (no envelope), ordered oldest-first — the same order the web UI shows them. The list is small by construction, so there is no pagination.
[
{
"id": "5f1c2d3e-7a8b-4c9d-0e1f-2a3b4c5d6e7f",
"name": "Young Backpacker",
"description": "A wandering protagonist in a worn green jacket",
"whenToUse": "Use for travel and journey verses",
"createdAt": "2026-04-12T08:14:22.000Z",
"imageUrl": "https://...supabase.co/...signed..."
}
]
8.4.1. Field reference
| Field | Meaning |
|---|---|
id |
UUID of the reference image. |
name |
The label you gave the asset. This is the handle the AI generators match against when deciding which reference to reuse. |
description |
Optional free-text description of what the image depicts. null when not set. |
whenToUse |
Optional hint that tells the generator which scenes the asset suits. null when not set. |
createdAt |
ISO-8601 timestamp the reference was added to the brand. |
imageUrl |
A short-lived signed URL to the image bytes. Expires — re-query this endpoint to refresh it. null (rare) when the underlying object could not be signed; the rest of the array is unaffected. |
8.4.2. Empty and error cases
- Owned brand with no reference images →
[](status 200, not 404). Add some in the web UI first. - Brand id that doesn't exist, OR a brand owned by a different account →
404(no existence leak between tenants).
Rate limit: 30 GET/min per user, 60 GET/min per IP.
9. Bringing Your Own Brand: Characters & Visual Style
By default Dayvid invents the visuals for every scene from scratch. A brand lets you steer that, so the same recurring characters and visual-style direction carry across every clip. A brand is a reusable kit you build once and then reference by brandId on as many videos as you like.
You set a brand up in the web UI, not through the API. The API reads brands you already created; there is no endpoint to create a brand or upload images. Do the one-time setup at https://dayvid.ai/brands, then the API consumes it.
9.1. Step 1: Create the brand (web UI)
Open https://dayvid.ai/brands and click New Brand. That drops you into the brand editor. The fields that actually shape an API-generated music video are:
- Brand Voice / Visual Style: a free-text field (Markdown supported) where you describe the brand's tone and visual style. This is fed to the scene generator and is your strongest lever on how the video looks.
- Reference Images: your recurring characters and assets (its own step below).
- Brand Fonts: link custom fonts. Once linked they can be used in subtitles, but only when you name them in
subtitleStyleOverride.fontFamily(see 5.13);GET /api/v1/brandslists them underfonts. They are not auto-applied to captions.
The editor also lets you set a Name and brand colors and logo. For videos generated through this API, focus on the visual-style text, reference images, and fonts above.
9.2. Step 2: Upload your reference images (characters and assets)
In the brand editor, the Reference Images section is where you upload the characters, props, or style references you want to reuse. When you add an image you fill in:
- Name (required): a short label, e.g.
Main character,Logo icon,Mascot. This is the handle the AI matches against when it decides which reference to pull into a scene. - When to use (optional but recommended): a plain-language hint for which scenes the asset suits, e.g.
Use in intro scenes. Per the in-app tooltip, this is what helps the AI decide when to use the image while generating scenes or backgrounds.
There is also a Description field, but you do not write it on upload: Dayvid auto-generates a description of the image (you can edit it later in the editor). So at upload time you mainly provide a clear Name and, ideally, a When to use hint.
9.3. Step 3: Reference the brand in your submit
Grab the brand's UUID (from its page at https://dayvid.ai/brands, or programmatically via GET /api/v1/brands, see 8) and pass it as brandId. Set assetStrategy: "reuse" to reuse your stored reference images as-is where they match a scene, versus "regenerate" which re-styles them to the scene (see 9.4 and 5.6):
curl -s -X POST "$DAYVID_BASE/api/v1/express" \
-H "Authorization: Bearer $DAYVID_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"settings": {
"audioUrl": "https://suno.com/s/E6uWAXuAvVlQyJR6",
"aspectRatio": "9:16",
"brandId": "your-brand-uuid",
"assetStrategy": "reuse",
"visualStyle": "moving",
"animationStop": 2,
"subtitlePresetName": "Gold",
"delivery": "auto-render"
}
}'
You can confirm what a brand has to work with before submitting by listing its reference images with GET /api/v1/brands/{brandId}/assets (see 8.4).
9.4. How Dayvid decides when to use each asset
You do not assign assets to scenes by hand. When brandId is set, the scene generator sees your reference images and your visual-style text and, scene by scene, links characters or objects in the lyrics to the references that fit, matching against each asset's name and "when to use" hint (plus its auto-generated description).
assetStrategy then decides how a matched reference is used:
"reuse": drop in your stored reference image as-is, no regeneration."regenerate"(the default): use your reference image as a guide to generate an adapted variant in the video's style, rather than the original file.
So your reference images and visual-style direction inform the result in both modes (whenever brandId is set); the difference is whether the exact stored image is reused or re-styled to match the scene. Scenes with no matching reference are generated from scratch.
Tip: clearer "when to use" hints lead to better matching. If an asset shows up in scenes you did not intend, tighten its hint in the web UI and re-render.
10. Listing Your Projects
GET /api/v1/projects returns the caller's projects, newest first, with cursor pagination. Use it to recover the projectId your script forgot to persist, build a CLI dashboard, or discover which renders are already done so you can fan out /v1/publish over them.
10.1. Minimal call
curl -s "$DAYVID_BASE/api/v1/projects" \
-H "Authorization: Bearer $DAYVID_TOKEN" | jq
Response (200):
{
"items": [
{
"id": "0a48e9b7-9b3e-4e3a-9c2f-1f9d8d2e7c10",
"name": "Hung Up On You",
"type": "single-track",
"createdAt": "2026-05-20T13:42:11.000Z",
"updatedAt": "2026-05-20T14:05:33.000Z",
"aspectRatio": "9:16",
"brandId": "8c1e...",
"status": "completed",
"hasCompletedRender": true,
"url": "https://dayvid.ai/projects/0a48e9b7-9b3e-4e3a-9c2f-1f9d8d2e7c10"
}
],
"nextCursor": "eyJ1IjoiMjAyNi0wNS0yMFQxNDowNTozMy4wMDBaIiwiaSI6IjBhNDhlOWI3LTliM2UtNGUzYS05YzJmLTFmOWQ4ZDJlN2MxMCJ9"
}
10.2. Query parameters
All optional. Combine freely.
| Param | Type | Notes |
|---|---|---|
limit |
int | 1 – 100. Default 50. |
cursor |
string | Opaque token from a prior response's nextCursor. Treat as opaque — do not parse or modify. |
status |
enum | draft | processing | completed | failed. Filter by derived per-project status (see 10.3). |
type |
enum | single-track | music-highlights. Filter by project kind. |
aspectRatio |
enum | "9:16" or "16:9". Exact match. |
q |
string | Case-insensitive substring match against name. ≤ 200 chars. Empty string is ignored. |
Example: list the next page of vertical, already-rendered projects.
curl -s -G "$DAYVID_BASE/api/v1/projects" \
--data-urlencode "status=completed" \
--data-urlencode "aspectRatio=9:16" \
--data-urlencode "limit=20" \
--data-urlencode "cursor=$PREV_CURSOR" \
-H "Authorization: Bearer $DAYVID_TOKEN" | jq
10.3. Field reference
| Field | Meaning |
|---|---|
id |
UUID of the project. Pass to /v1/render, /v1/publish, /v1/projects/:id, or look up jobs scoped to it. |
name |
Display name. May change after submit while the server processes the transcript. |
type |
single-track (one video per project) or music-highlights (several clips per project, see section 11). |
createdAt |
ISO-8601, never changes. |
updatedAt |
ISO-8601, server bump on any edit, re-render, or publish. Drives ordering. |
aspectRatio |
"9:16" | "16:9" | null. |
brandId |
UUID or null if the project was submitted without a brand. |
status |
Derived. See enum below. |
hasCompletedRender |
true once any render job for the project has reached completed. Gates /v1/publish (which returns 422 no_completed_render otherwise). |
url |
The project's page on the web app. Opaque — there is no API surface reachable from this URL. |
Status enum:
| Value | Meaning |
|---|---|
processing |
A run is in flight for this project. |
failed |
The last run failed and has not been retried. |
completed |
At least one render finished. Eligible for /v1/publish. |
draft |
Never finished a render and nothing is currently running. |
status and hasCompletedRender can both be true for the same project — e.g. a re-render is in flight (status: "processing") but a prior render already succeeded (hasCompletedRender: true).
10.4. Pagination semantics
Pages are stable under concurrent writes: a project bumped to the top while you're paginating won't reappear on a later page.
- When
nextCursorisnull, there are no more pages. - The token is opaque. Do not parse it, modify it, or rely on its structure — the encoding may change without notice.
- Passing a malformed cursor returns
400 invalid_cursor. Drop the cursor and start over. - There is no
prevCursor. To go back, restart from the beginning.
10.5. Empty cases
- Brand-new account with no projects →
{ "items": [], "nextCursor": null }(200, not 404). status=completedfor an account that has never finished a render → same empty response.- Search
qthat matches nothing → same empty response.
10.6. Errors
| Status | Code | Cause |
|---|---|---|
| 400 | invalid_query |
A query param failed validation. Body includes field and message. |
| 400 | invalid_cursor |
The cursor token is malformed or no longer decodable. Drop it and re-list from the start. |
Rate limit: 60 GET/min per user, 120 GET/min per IP.
10.7. Project detail: GET /api/v1/projects/:id
One call that answers, for a projectId you kept from months ago: which track is this, did it render, where is the MP4, and did I already publish it (and where).
curl -s "$DAYVID_BASE/api/v1/projects/$PROJECT_ID" \
-H "Authorization: Bearer $DAYVID_TOKEN" | jq
Response (200):
{
"id": "0a48e9b7-9b3e-4e3a-9c2f-1f9d8d2e7c10",
"name": "Hung Up On You",
"type": "single-track",
"status": "completed",
"aspectRatio": "9:16",
"brandId": "8c1e...",
"createdAt": "2026-05-20T13:42:11.000Z",
"updatedAt": "2026-05-20T14:05:33.000Z",
"audio": { "fileName": "hung-up-on-you.mp3", "durationMs": 183000 },
"render": {
"completed": true,
"downloadUrl": "https://...signed...mp4",
"expiresAt": "2026-06-11T15:00:00.000Z",
"completedAt": "2026-05-20T14:05:33.000Z"
},
"highlights": null,
"publishTargets": [
{
"provider": "youtube",
"status": "published",
"externalUrl": "https://youtube.com/watch?v=dQw4w9WgXcQ",
"jobId": "uuid",
"createdAt": "2026-05-21T10:00:00.000Z"
}
],
"url": "https://dayvid.ai/projects/0a48e9b7-9b3e-4e3a-9c2f-1f9d8d2e7c10"
}
Field notes:
statususes the same enum and derivation as the list endpoint (10.3) - the two never disagree.audioisnullwhen the project has no audio attached yet.audio.durationMsisnullwhen the duration is unknown (e.g. the project was assembled in the web editor rather than submitted through the API); it is always set for API-submitted projects.render.downloadUrlis a short-lived signed URL to the latest finished render;expiresAttells you when it stops working - re-fetch this endpoint for a fresh one. Allnull(completed: false) while nothing has rendered.highlightsis non-null only fortype: "music-highlights"projects and carries the same{ status, partial, progress, clips[] }shape as the dedicated results endpoint (11.2).publishTargetsflattens every publish run for this project, newest first - one entry per (run, platform). Scan it forstatus: "published"to answer "did I already publish this?" before calling/v1/publish. Empty array = never published.- A project that is missing or not yours returns
404 project_not_found(same response in both cases, so existence never leaks).
Rate limit: 60 GET/min per user, 120 GET/min per IP.
10.8. Download a project's lyrics: GET /api/v1/projects/:id/lyrics
The time-aligned transcription of the project's audio, as JSON. This is the exact data the burned-in captions are built from, so you can reuse it for karaoke views, your own subtitle files, or syncing lyrics to a CMS, without transcribing the track again.
curl -s "$DAYVID_BASE/api/v1/projects/$PROJECT_ID/lyrics" \
-H "Authorization: Bearer $DAYVID_TOKEN" | jq
Response (200) is the subtitle JSON content verbatim: an array of time-aligned entries, each with per-word timings.
[
{
"index": 1,
"startMs": 18900,
"endMs": 22880,
"text": "One man's trash is another's treasure.",
"words": [
{ "word": "One", "startMs": 18900, "endMs": 19560 },
{ "word": "man's", "startMs": 19680, "endMs": 20040 },
{ "word": "trash", "startMs": 20120, "endMs": 20880 }
]
}
]
- Timestamps are milliseconds from the start of the audio.
wordscarries the word-level timing the karaoke-style caption modes use. - Lyrics exist only after a run that transcribed the audio has completed (an Express or Music Highlights submit). Before that, the endpoint returns
404 lyrics_not_availablewith a message explaining why; re-fetch after the job completes. - A project that is missing or not yours returns
404 project_not_found(same response in both cases, so existence never leaks).
Rate limit: 60 GET/min per user, 120 GET/min per IP.
11. Music Highlights
Turn ONE song into several short, ready-to-post highlight videos. Dayvid finds the strongest moments of the track (chorus, build-up, the money line), snaps each to sentence boundaries so cuts never land mid-word, and renders each as a standalone vertical (or horizontal) MP4 with burned-in subtitles over a visual background.
This is a separate one-shot pipeline from the single-video Express endpoint. One POST submits the job; you get a jobId immediately and an email when it finishes. Because the output is a COLLECTION of clips (not one video), you fetch them from a dedicated results endpoint. Each clip is a downloadable MP4 with the subtitles already burned in.
Music Highlights is gated behind a per-account feature flag while it rolls out. If your account is not enabled yet, every Music Highlights call returns 404 feature_disabled (this is on top of needing an API-capable plan). Ask support to enable it for your account.
11.1. Submit
POST /api/v1/music-highlights/express. Provide exactly one of audioStoragePath (from /api/v1/audio-uploads, see 4.4) or audioUrl (a Suno / Udio / SoundCloud link, or a whitelisted CDN URL). Body:
{
"settings": {
"audioUrl": "https://suno.com/s/E6uWAXuAvVlQyJR6",
"targetClipCount": 3,
"maxDurationS": 55,
"aspectRatio": "9:16",
"backgroundType": "generated-moving",
"animationStop": 1
}
}
Fields:
audioStoragePathXORaudioUrl(required, one of).audioDurationMsis NOT accepted; the server probes the real duration.projectName(optional string, max 200 chars): display name for the project. If omitted, the project is namedMusic Highlights.targetClipCount(optional integer 1..4, default 3): how many clips to render. Dayvid ranks the candidate moments and renders the top N.maxDurationS(optional integer 15..90, default 55): soft cap each clip respects when snapping to sentence boundaries.aspectRatio(optional,"9:16" | "16:9", default"9:16").backgroundType(required,"generated-moving" | "video-loop"):"generated-moving"paints AI scenes and can animate the strongest shots as AI video clips;"video-loop"plays a looping stock background and requiresvideoLoopSlug.animationStop(optional0 | 1 | 2 | 3 | 4, default1): only meaningful whenbackgroundType === "generated-moving". It uses the same motion tier as/api/v1/express:0disables AI video animation,1animates the best moments, higher tiers animate progressively more shots. Forvideo-loop, omit it or sendnull.videoLoopSlug(required whenbackgroundType === "video-loop"): a catalog loop slug, e.g.train,snow-pine,three-moons,nyc-taxi,submersible,dancefloor. Omitting it returns400 video_loop_slug_required.brandId(optional UUID ornull, defaultnull): a brand you own; steers recurring characters / visual style. Must belong to you or the call returns400 brand_not_owned.assetStrategy(optional,"reuse" | "regenerate", default"regenerate"): reuse a brand's existing reference assets instead of regenerating them (only meaningful with abrandId).generationPresetId(optional string, default"default"): visual generation style preset.visualGuidelines(optional string, max 1000 chars): free-text steering for the generated visuals.subtitleEnabled(optional boolean, defaulttrue).subtitleStyle(optional object ornull): a full subtitle style override, same shape assubtitleStyleOverrideon/api/v1/express(see 5.13). When set, itsfontFamilyis validated against the font catalog; an unknown font returns422 font_not_available.language(optional two-letter ISO-639-1 code ornull, default autodetect): transcription hint. Pass a code when autodetect mislabels a track (e.g. a mostly-Portuguese song with English-sounding intro ad-libs).
Response (202):
{
"projectId": "uuid",
"jobId": "uuid",
"estimateSnapshot": { "total": 12, "reservedAtSubmit": 12, "marginCredits": 0, "breakdown": [] },
"pollUrl": "/api/v1/jobs/<jobId>",
"resultsUrl": "/api/v1/music-highlights/<projectId>"
}
Credits are reserved up front against the estimate and the unused slack is refunded when the run finishes. Poll pollUrl for status; fetch the clips from resultsUrl once it reports completed.
11.2. Fetch results
GET /api/v1/music-highlights/:projectId returns the rendered clips with per-clip signed download URLs. The single-artifact downloadUrl on the job poll can't carry several clips, so this is the collection endpoint. Poll /api/v1/jobs/:id until status === "completed", then read here (rate limit 60/min).
curl -s "$DAYVID_BASE/api/v1/music-highlights/$PROJECT_ID" \
-H "Authorization: Bearer $DAYVID_TOKEN" | jq
Response:
{
"projectId": "uuid",
"status": "processing",
"partial": false,
"progress": "2/3",
"clips": [
{
"position": 0,
"startMs": 134000,
"endMs": 176000,
"durationMs": 42000,
"selected": true,
"status": "rendered",
"downloadUrl": "https://...signed...mp4"
},
{
"position": 1,
"startMs": 201000,
"endMs": 240000,
"durationMs": 39000,
"selected": true,
"status": "processing",
"downloadUrl": null
}
]
}
status:"processing"while the run is in flight,"completed"when every selected clip rendered,"partial"when the run finished but a clip failed,"failed"on error.progress:"rendered/total"over the selected clips (e.g."2/3"). Poll on this instead of counting non-nulldownloadUrls.clips[].status: per-clip lifecycle -"rendered"(MP4 ready, downloadable even mid-run),"processing"(not rendered yet, run still going),"failed"(the run already ended and this clip never rendered - it will not appear without a new submit). This is how you tell "slow" from "stuck": a clip stuck at"processing"whilestatusis still"processing"is just slow; anything"failed"is final.partial:trueonly when the run finished with at least one clip missing.downloadUrl: a short-lived signed URL to the rendered MP4 (subtitles burned in). It expires, so re-query this endpoint to refresh.- A project that is missing, not yours, or not a Music Highlights project returns
404 project_not_found(the same response in all three cases, so existence never leaks).
11.3. Recipe: highlights from a Suno track
curl -s -X POST "$DAYVID_BASE/api/v1/music-highlights/express" \
-H "Authorization: Bearer $DAYVID_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"settings": {
"audioUrl": "https://suno.com/s/E6uWAXuAvVlQyJR6",
"targetClipCount": 3,
"maxDurationS": 45,
"backgroundType": "generated-moving",
"animationStop": 1
}
}' | tee /tmp/mh.json | jq
PROJECT_ID=$(jq -r .projectId /tmp/mh.json)
# Wait for the job email (or poll pollUrl), then download every rendered clip:
curl -s "$DAYVID_BASE/api/v1/music-highlights/$PROJECT_ID" \
-H "Authorization: Bearer $DAYVID_TOKEN" \
| jq -r '.clips[] | select(.downloadUrl) | "\(.position) \(.downloadUrl)"' \
| while read POS URL; do curl -s -o "clip_$POS.mp4" "$URL"; echo "saved clip_$POS.mp4"; done
11.4. Idempotency for music highlights
Same Idempotency-Key header as /api/v1/express. Format and replay semantics are identical. The scope is independent: a key reused across the two endpoints does NOT collide.
12. Publishing to Social Platforms
Prerequisite — connect platforms via the web UI, not the API.
Before your first
POST /api/v1/publish, sign in at https://dayvid.ai and create a brand. Each brand has its own page athttps://dayvid.ai/brands/<brandId>with a "Connected Accounts" section where you connect YouTube, Instagram, and TikTok via OAuth. Connections are scoped to the brand (unique perbrand_id, provider), so if you publish using a differentbrandIdyou have to connect it on that brand too. The API never accepts platform tokens from clients and does not expose any endpoint to start the OAuth handshake. If you call publish for a platform your brand has not connected, you receive422 oauth_requiredwithconnectUrl: https://dayvid.ai/brands/<brandId>pointing at the right page.
POST /api/v1/publish takes a project that already finished rendering (see section 6) and publishes the rendered MP4 to one or more social platforms in parallel. Returns 202 with jobId you poll the same way as any other v1 job.
12.1. Minimal request (YouTube)
curl -s -X POST "$DAYVID_BASE/api/v1/publish" \
-H "Authorization: Bearer $DAYVID_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"projectId": "<projectId from express response>",
"brandId": "<brandId you connected platforms on>",
"providers": ["youtube"],
"title": "Hung Up On You",
"description": "AI music video, made with Dayvid.",
"hashtags": ["musicvideo", "indie"],
"tags": ["music", "indie", "ai"]
}' | jq
Response (202):
{
"jobId": "uuid",
"projectId": "uuid",
"status": "processing",
"pollUrl": "/api/v1/jobs/<jobId>"
}
12.2. Multiple platforms in one call
curl -s -X POST "$DAYVID_BASE/api/v1/publish" \
-H "Authorization: Bearer $DAYVID_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"projectId": "<projectId>",
"brandId": "<brandId>",
"providers": ["youtube", "instagram", "tiktok"],
"title": "Track title",
"description": "Description seen on every platform.",
"hashtags": ["music"],
"metadata": {
"thumbnailStoragePath": "<userId>/images/...",
"youtube": { "privacyStatus": "public", "playlistIds": ["PLxxx"] },
"instagram": { "coverUrl": "https://..." },
"tiktok": { "postMode": "post" }
}
}' | jq
metadata is namespaced per platform: anything inside metadata.youtube is forwarded to YouTube, anything inside metadata.tiktok to TikTok, etc. Unknown platforms pass through validation but are rejected at runtime with platform_not_available.
12.3. Polling a publish job
curl -s "$DAYVID_BASE/api/v1/jobs/$JOB_ID" \
-H "Authorization: Bearer $DAYVID_TOKEN" | jq
Publish jobs return a different shape than express/render jobs. The discriminator is jobType: "publish" and the payload exposes targets[] (one entry per platform):
{
"id": "uuid",
"jobType": "publish",
"status": "published",
"targets": [
{
"provider": "youtube",
"status": "published",
"externalId": "dQw4w9WgXcQ",
"externalUrl": "https://youtube.com/watch?v=dQw4w9WgXcQ"
},
{
"provider": "instagram",
"status": "failed",
"error": "Token expired. Reconnect Instagram on the brand page.",
"errorCategory": "auth_expired"
}
],
"userError": null,
"startedAt": "2026-05-23T13:00:00.000Z",
"completedAt": "2026-05-23T13:02:14.000Z"
}
Overall status summarizes the run: "published" (all succeeded), "partial" (some succeeded), "failed" (none), "processing" (still in flight), "pending".
To attach a custom YouTube thumbnail, see 12.4.
Per-target status is one of pending | validating | uploading | published | draft | partial | failed. Per-target error is a user-facing string; errorCategory is one of auth_expired | quota_exceeded | generic so your client can route retries.
12.4. Custom thumbnail (YouTube)
Attach your own thumbnail instead of letting YouTube pick a frame. Two steps: upload the image, then reference its storage path on publish.
POST /api/v1/image-uploads is a general-purpose image upload that uses the same signed-URL model as audio (section 4.4): the request body needs only fileName (with an extension).
# 1. Signed URL for the image (fileName only)
curl -s -X POST "$DAYVID_BASE/api/v1/image-uploads" \
-H "Authorization: Bearer $DAYVID_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "fileName": "thumb.jpg" }' | tee /tmp/thumb.json | jq
THUMB_SIGNED=$(jq -r .signedUploadUrl /tmp/thumb.json)
THUMB_PATH=$(jq -r .storagePath /tmp/thumb.json)
# 2. PUT the bytes (the Content-Type you send becomes the object's type)
curl -s -X PUT "$THUMB_SIGNED" \
-H "Content-Type: image/jpeg" \
--data-binary "@/path/to/thumb.jpg"
# 3. Publish, referencing the path as metadata.thumbnailStoragePath
curl -s -X POST "$DAYVID_BASE/api/v1/publish" \
-H "Authorization: Bearer $DAYVID_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"projectId\": \"$PROJECT_ID\",
\"brandId\": \"$BRAND_ID\",
\"providers\": [\"youtube\"],
\"title\": \"Hung Up On You\",
\"metadata\": { \"thumbnailStoragePath\": \"$THUMB_PATH\" }
}" | jq
Thumbnail requirements:
- It must be an image you uploaded — referencing a path that is not yours returns
400 thumbnail_storage_path_forbidden. - It must already be uploaded — if the PUT has not landed yet,
422 thumbnail_not_found. - It must be
≤ 10 MBand one ofimage/jpeg,image/png,image/gif, else422 thumbnail_invalid(with areasonin the body). webp is not accepted for YouTube thumbnails.
If the thumbnail fails to apply at YouTube, the publish still succeeds and the failure comes back as a per-target warning.
metadata.thumbnailStoragePath is also used as the Instagram cover when you publish to Instagram.
12.5. Platforms
| Provider | Status |
|---|---|
youtube |
Live. |
instagram |
In review with Meta. Connects via UI but publishing may be limited until approval lands. |
tiktok |
In review with TikTok. Same caveat as Instagram. |
| Others | Planned. Calling with facebook, twitter, linkedin returns platform_not_available. |
12.6. Idempotency for publish
POST /api/v1/publish accepts the same Idempotency-Key header as the other v1 routes (see section 13). Same payload + same key within 24h returns the same jobId; different payload with the same key returns 422 idempotency_mismatch. Use this when retrying after a network hiccup.
12.7. Already-published protection (force)
Publishing is the one operation a retry can't undo: a duplicate request means a second real video on the platform, with no way to cancel it through Dayvid. So on top of Idempotency-Key (which only helps when you reuse the key), the endpoint checks the project's publish history per requested platform:
Every requested platform already has this project live →
200with the existing publish instead of dispatching a new one:{ "alreadyPublished": true, "projectId": "uuid", "targets": [ { "provider": "youtube", "status": "published", "externalUrl": "https://youtube.com/watch?v=dQw4w9WgXcQ", "jobId": "uuid", "publishedAt": "2026-05-21T10:02:14.000Z" } ], "detailUrl": "/api/v1/projects/<projectId>" }Some (not all) requested platforms already have it →
409 already_publishedwithpublishedProviders[],unpublishedProviders[], and theexisting[]entries. Either narrowprovidersto the unpublished ones or passforce.You genuinely want a duplicate (re-upload after deleting on the platform, A/B posting, etc.) → add
"force": trueto the body and the check is skipped entirely.
A platform target counts as "already published" when its upload landed (published, or draft for platforms that file uploads as drafts). Failed or in-flight targets don't count - retrying those is exactly what the endpoint is for.
13. Idempotency
POST /api/v1/express accepts an Idempotency-Key header. Format: 1-80 chars, [A-Za-z0-9_-]. If you retry with the same key within 24h:
- If the original submit completed: you get the cached response with header
Idempotency-Replay: true - If still in flight: 409
idempotency_in_progress - If the original errored: the key is released and your retry runs fresh
Use this when retrying after a network hiccup so you do not accidentally enqueue two jobs.
curl -s -X POST "$DAYVID_BASE/api/v1/express" \
-H "Authorization: Bearer $DAYVID_TOKEN" \
-H "Idempotency-Key: my-track-2026-05-22-attempt-1" \
-H "Content-Type: application/json" \
-d '{ "settings": { ... } }'
14. Error Reference
Every error returns { "error": "<code>", ...details }. That includes routing errors: a request to a path that does not exist under /api/v1/ returns 404 { "error": "not_found", "message": "No such endpoint: ..." }, and a wrong HTTP method on a real endpoint returns 405 { "error": "method_not_allowed", "allowed": [...] } with an Allow header - your client never has to parse HTML out of this API.
The codes you are likely to encounter while integrating:
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_settings |
Express body failed validation. issues[] lists each problem. |
| 400 | invalid_body |
Publish body failed validation. issues[] lists each problem. |
| 400 | thumbnail_storage_path_forbidden |
Publish metadata.thumbnailStoragePath is not under your {userId}/ prefix (see 12.4). |
| 400 | subtitle_source_conflict |
Express received both subtitlePresetName and subtitleStyleOverride. Send exactly one. |
| 400 | subtitle_source_missing |
Express received neither subtitlePresetName nor subtitleStyleOverride (and no presetId to supply it). Send one of them or set presetId (see 5.16). |
| 400 | aspect_ratio_required |
Express received neither aspectRatio nor a presetId whose preset has one saved. Set aspectRatio (5.4). |
| 400 | preset_wrong_type |
Express presetId points at a preset you own but for a different project type. Only Single Track presets work here. Body: { expected, actual } (see 5.16). |
| 400 | preset_element_forbidden |
Express presetId references at least one asset that isn't yours. Rare; usually a hand-edited preset. Body: { forbidden: [...] } (see 5.16). |
| 400 | preset_asset_unavailable |
Express presetId references at least one asset that has been deleted from storage. Re-save the preset in the web app. Body: { missing: [...] } (see 5.16). |
| 404 | preset_not_found |
Express presetId does not exist or is not yours. Same response in both cases — we don't confirm whether someone else's preset exists (see 5.16). |
| 400 | idempotency_key_invalid |
Idempotency-Key does not match the allowed format. |
| 400 | brand_not_owned |
Music Highlights brandId is not a brand you own. |
| 400 | video_loop_slug_required |
Music Highlights backgroundType: "video-loop" sent without videoLoopSlug. |
| 400 | invalid_query |
GET /v1/projects query param failed validation. Body includes field and message. |
| 400 | invalid_cursor |
GET /v1/projects cursor is malformed or no longer decodable. Drop it and re-list from the start. |
| 401 | (no body) | Missing or invalid Bearer token. |
| 402 | plan_does_not_include_apiAccess |
Your plan cannot call the API. Upgrade to Start or above. |
| 402 | insufficient_credits |
{ needed, balance }. Buy more or wait for refill. |
| 403 | action_denied |
Publish blocked (banned account, plan, or per-platform cap). reason is in the body. |
| 404 | (varies) | Project, brand, or job not found. |
| 404 | feature_disabled |
Music Highlights is not enabled for your account yet (rollout flag), or the kill switch is on. |
| 404 | project_not_found |
Project detail or Music Highlights results for a project that is missing or not yours (or, for highlights, not a highlights project). |
| 404 | lyrics_not_available |
Project lyrics requested before any run transcribed the audio. Re-fetch after a submit completes (see 10.8). |
| 404 | not_found |
The URL path itself does not exist under /api/v1/. Check for typos; message echoes the path. |
| 405 | method_not_allowed |
Wrong HTTP method on a real endpoint. allowed[] lists the supported methods. |
| 409 | idempotency_in_progress |
Same key still running. |
| 409 | already_published |
Publish: some requested platforms already carry this project. publishedProviders[], unpublishedProviders[], existing[] in body. Narrow providers or pass force: true (see 12.7). |
| 413 | audio_too_large |
Audio exceeds 100 MB. |
| 415 | audio_mime_not_supported |
Audio file format not supported (see section 4.4). |
| 422 | probe_failed |
Could not read duration from the audio file. |
| 422 | no_completed_render |
Publish called for a project that has not rendered yet. |
| 422 | thumbnail_not_found |
Publish metadata.thumbnailStoragePath points at an object that does not exist — the image PUT has not landed (see 12.4). |
| 422 | thumbnail_invalid |
Publish thumbnail is > 10 MB or not image/jpeg/png/gif. reason in body (see 12.4). |
| 422 | oauth_required |
Publish called for a platform not connected on the brand. missing[] lists which platforms; connectUrl is https://dayvid.ai/brands/<brandId> for the brand you submitted. |
| 422 | platform_not_available |
Publish called for a platform not enabled for your account or not implemented. providers[] lists which. |
| 422 | content_policy_violation |
Moderation rejected the publish (e.g. title/description). |
| 422 | font_not_available |
Express subtitleStyleOverride.fontFamily is not in the catalog for this brand. Response body lists availableBuiltin, availableSystem, availableBrand. See section 5.13. |
| 429 | quota_exceeded |
Rate limit hit. Backoff and retry. |
| 429 | action_denied (reason: RATE_LIMITED) |
Monthly publish cap or per-platform hourly cap reached. cap, used, nextAllowedAt in body. |
| 502 | audio_url_unreachable |
We could not download the linked track. |
15. Rate Limits Summary
| Endpoint | User / min | IP / min |
|---|---|---|
GET /api/v1/me |
60 | 120 |
GET /api/v1/brands |
30 | 60 |
GET /api/v1/brands/:brandId/assets |
30 | 60 |
GET /api/v1/projects |
60 | 120 |
GET /api/v1/projects/:id |
60 | 120 |
GET /api/v1/projects/:id/lyrics |
60 | 120 |
POST /api/v1/audio-uploads |
20 | 40 |
POST /api/v1/image-uploads |
20 | 40 |
POST /api/v1/express |
5 | 10 |
POST /api/v1/music-highlights/express |
5 | 10 |
GET /api/v1/music-highlights/:projectId |
60 | 120 |
POST /api/v1/render |
10 | 20 |
POST /api/v1/publish |
5 | 10 |
GET /api/v1/jobs/:id |
60 | 120 |
PATCH /api/v1/jobs/:id |
10 | 20 |
Publish also has plan-level caps (monthly publish count + per-platform hourly window) that surface as 429 action_denied with reason: "RATE_LIMITED"; see cap, used, nextAllowedAt in the body.
16. Recipes
16.1. How to create a vertical music video from a Suno song (Reels/Shorts/TikTok)
Vertical, AI-generated moving imagery, punchy subtitles, no brand:
curl -s -X POST "$DAYVID_BASE/api/v1/express" \
-H "Authorization: Bearer $DAYVID_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"settings": {
"audioUrl": "https://suno.com/s/E6uWAXuAvVlQyJR6",
"aspectRatio": "9:16",
"brandId": null,
"assetStrategy": "regenerate",
"visualStyle": "moving",
"animationStop": 2,
"subtitlePresetName": "Punchy",
"generationPresetId": "default",
"delivery": "auto-render"
}
}'
16.2. How to create a horizontal lyric video with a video-loop background (YouTube)
videoLoop skips image generation; the train MP4 plays under subtitles. Cheapest run.
curl -s -X POST "$DAYVID_BASE/api/v1/express" \
-H "Authorization: Bearer $DAYVID_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"settings": {
"audioUrl": "https://www.udio.com/songs/uXYZabcDEF",
"aspectRatio": "16:9",
"brandId": null,
"assetStrategy": "reuse",
"visualStyle": "videoLoop",
"videoLoopPresetSlug": "train",
"subtitlePresetName": "White",
"delivery": "auto-render"
}
}'
16.3. How to create an anime-styled video from a custom MP3
Combines the upload flow with an aesthetic generation preset and visual guidelines.
# 1) Get signed upload URL (FILE set to your local audio path)
curl -s -X POST "$DAYVID_BASE/api/v1/audio-uploads" \
-H "Authorization: Bearer $DAYVID_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "fileName": "song.mp3" }' > /tmp/u.json
SIGNED=$(jq -r .signedUploadUrl /tmp/u.json)
PATH_=$(jq -r .storagePath /tmp/u.json)
# 2) Upload the bytes
curl -s -X PUT "$SIGNED" -H "Content-Type: audio/mpeg" --data-binary "@$FILE"
# 3) Submit
curl -s -X POST "$DAYVID_BASE/api/v1/express" \
-H "Authorization: Bearer $DAYVID_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"settings\": {
\"audioStoragePath\": \"$PATH_\",
\"aspectRatio\": \"9:16\",
\"brandId\": null,
\"assetStrategy\": \"regenerate\",
\"visualStyle\": \"moving\",
\"animationStop\": 2,
\"subtitlePresetName\": \"Glow\",
\"generationPresetId\": \"anime\",
\"visualGuidelines\": \"Anime protagonist with red hoodie, neon Tokyo streets, rainy night\",
\"delivery\": \"auto-render\"
}
}"
16.4. How to use a saved brand kit
Fetch your brand kit UUID from its page at https://dayvid.ai/brands, then pass it as brandId. For the full walkthrough of setting up a brand and reusing your own characters, see section 9, including how assetStrategy controls whether matched reference images are reused as-is or re-styled.
curl -s -X POST "$DAYVID_BASE/api/v1/express" \
-H "Authorization: Bearer $DAYVID_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"settings": {
"audioUrl": "https://suno.com/s/E6uWAXuAvVlQyJR6",
"aspectRatio": "9:16",
"brandId": "your-brand-uuid",
"assetStrategy": "regenerate",
"visualStyle": "moving",
"animationStop": 2,
"subtitlePresetName": "Gold",
"delivery": "auto-render"
}
}'
16.5. How to review assets before rendering
Use delivery: "review-first". The Express job stops once images and timing are ready, leaving the project in a reviewable state in the web UI. When you want the MP4, call render directly with the projectId returned by submit:
curl -s -X POST "$DAYVID_BASE/api/v1/render" \
-H "Authorization: Bearer $DAYVID_TOKEN" \
-H "Content-Type: application/json" \
-d "{ \"projectId\": \"$PROJECT_ID\" }"
This returns a new jobId for the render. Poll it the same way as section 6.
16.6. How to safely retry a flaky submit
Send the same Idempotency-Key every attempt. After the first success, all subsequent retries return the cached projectId + jobId instead of creating new jobs.
KEY="track-$(date +%Y%m%d)-attempt"
for i in 1 2 3; do
curl -s -X POST "$DAYVID_BASE/api/v1/express" \
-H "Authorization: Bearer $DAYVID_TOKEN" \
-H "Idempotency-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{ "settings": { ... } }' \
-w "\nstatus:%{http_code}\n" && break
sleep 2
done
16.7. How to apply a saved preset and render in 2K
Pick a preset you already saved in the web app (see 5.16 for how to obtain the UUID). The preset supplies overlays / outro / subtitle style / aspect ratio; the request below adds a Suno track, asks for 2K, and overrides nothing else — so the rendered video looks exactly like the preset on top of the new track.
curl -s -X POST "$DAYVID_BASE/api/v1/express" \
-H "Authorization: Bearer $DAYVID_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"settings": {
"audioUrl": "https://suno.com/s/E6uWAXuAvVlQyJR6",
"brandId": null,
"assetStrategy": "regenerate",
"visualStyle": "moving",
"animationStop": 2,
"generationPresetId": "default",
"delivery": "auto-render",
"resolutionPreset": "2K",
"presetId": "11111111-1111-4111-8111-111111111111"
}
}'
Two things to notice:
aspectRatiois omitted — the preset's saved aspect ratio fills the slot. Add"aspectRatio": "9:16"to override the preset.- Neither
subtitlePresetNamenorsubtitleStyleOverrideis sent — the preset's subtitle style is used as-is. Add either field to override.
reservedAtSubmit in the response will be higher than the same submit without resolutionPreset: "2K" because AI image generation steps up to the higher tier.
17. Token Lifecycle
- When you create a token at https://dayvid.ai/profile, you choose an expiration: 30 days, 90 days, 1 year, or Never. The default in the dialog is 90 days, so pick "Never" explicitly if you want a token that does not expire on its own.
- Once a token expires, requests using it return 401 and you mint a new one.
- You can also revoke a token at any time from the same page. Revoking takes effect immediately.
- Treat tokens like passwords: store them in a secret manager, never commit them, and rotate if you suspect one leaked.