← Dayvid
Public API · v1

API Reference

Everything on this page works with two things: an HTTPS client and a bearer token. Submit a song, poll the job, download the video, publish it. No SDK required.

Base URL & auth
https://dayvid.ai/api/v1
Authorization: Bearer dvy_...

Tokens are minted in your account settings. See section 2 below.

Agent-ready docs

Skip the reading: send your AI agent instead. Copy the prompt below into Claude, ChatGPT, or Cursor and it reads /SKILL.md (the official skill file behind this page) and drives the whole API for you.

On this page

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

  1. Open https://dayvid.ai
  2. Click Sign in (top right)
  3. Choose Continue with Google and complete the OAuth consent screen
  4. 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

  1. Go to Profile: https://dayvid.ai/profile
  2. Scroll to the API tokens card
  3. Click Create token, give it a label (e.g. cli-laptop), confirm
  4. 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).
  5. Copy the dvy_... value SHOWN ONCE. Store it in a secret manager. We never display it again.
  6. 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:

  1. Ingest audio so the server has a stable storage path it can read
  2. Submit settings to POST /api/v1/express. You receive a jobId immediately; 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.

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

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

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 MB and one of image/jpeg, image/png, image/webp (webp IS accepted here, unlike YouTube thumbnails), else 400 invalid_settings (background image too large / background image type not supported).

Combination errors (rejected before any work): staticImageSource: "upload" without backgroundImageStoragePath400 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 / TikTok
  • 16: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; set staticImageSource (5.7a) to supply your own image instead. Lowest credit cost.
  • videoLoop: ignore generated imagery; use a curated MP4 loop background. Requires videoLoopPresetSlug.

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).

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 MP4
  • review-first: pipeline stops after asset generation; you must explicitly call POST /api/v1/render to 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. Sending subtitlePresetName or subtitleStyleOverride on the same request overrides it.
  • Aspect ratio (fallback): used if you omit aspectRatio on 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; otherwise 400 aspect_ratio_required.
  • subtitlePresetName / subtitleStyleOverride — the preset supplies the subtitle style. If you DO send one, it overrides. Sending both still returns 400 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

  • presetId and Idempotency-Key play 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.
  • presetId is independent of brandId: you can combine them. The preset supplies the overlay/outro/subtitle look; brandId still steers scene generation when assetStrategy === "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 in subtitleStyleOverride.fontFamily.
  • Brand with no connected platforms → "integrations": { "youtube": { "connected": false } } (still emits enabled providers with connected: 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/brands lists them under fonts. 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 nextCursor is null, 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=completed for an account that has never finished a render → same empty response.
  • Search q that 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:

  • status uses the same enum and derivation as the list endpoint (10.3) - the two never disagree.
  • audio is null when the project has no audio attached yet. audio.durationMs is null when 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.downloadUrl is a short-lived signed URL to the latest finished render; expiresAt tells you when it stops working - re-fetch this endpoint for a fresh one. All null (completed: false) while nothing has rendered.
  • highlights is non-null only for type: "music-highlights" projects and carries the same { status, partial, progress, clips[] } shape as the dedicated results endpoint (11.2).
  • publishTargets flattens every publish run for this project, newest first - one entry per (run, platform). Scan it for status: "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.

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:

  • audioStoragePath XOR audioUrl (required, one of). audioDurationMs is NOT accepted; the server probes the real duration.
  • projectName (optional string, max 200 chars): display name for the project. If omitted, the project is named Music 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 requires videoLoopSlug.
  • animationStop (optional 0 | 1 | 2 | 3 | 4, default 1): only meaningful when backgroundType === "generated-moving". It uses the same motion tier as /api/v1/express: 0 disables AI video animation, 1 animates the best moments, higher tiers animate progressively more shots. For video-loop, omit it or send null.
  • videoLoopSlug (required when backgroundType === "video-loop"): a catalog loop slug, e.g. train, snow-pine, three-moons, nyc-taxi, submersible, dancefloor. Omitting it returns 400 video_loop_slug_required.
  • brandId (optional UUID or null, default null): a brand you own; steers recurring characters / visual style. Must belong to you or the call returns 400 brand_not_owned.
  • assetStrategy (optional, "reuse" | "regenerate", default "regenerate"): reuse a brand's existing reference assets instead of regenerating them (only meaningful with a brandId).
  • 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, default true).
  • subtitleStyle (optional object or null): a full subtitle style override, same shape as subtitleStyleOverride on /api/v1/express (see 5.13). When set, its fontFamily is validated against the font catalog; an unknown font returns 422 font_not_available.
  • language (optional two-letter ISO-639-1 code or null, 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-null downloadUrls.
  • 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" while status is still "processing" is just slow; anything "failed" is final.
  • partial: true only 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 at https://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 per brand_id, provider), so if you publish using a different brandId you 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 receive 422 oauth_required with connectUrl: 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 MB and one of image/jpeg, image/png, image/gif, else 422 thumbnail_invalid (with a reason in 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 live200 with 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 it409 already_published with publishedProviders[], unpublishedProviders[], and the existing[] entries. Either narrow providers to the unpublished ones or pass force.

  • You genuinely want a duplicate (re-upload after deleting on the platform, A/B posting, etc.) → add "force": true to 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 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
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:

  • aspectRatio is omitted — the preset's saved aspect ratio fills the slot. Add "aspectRatio": "9:16" to override the preset.
  • Neither subtitlePresetName nor subtitleStyleOverride is 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.