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, Gemini, 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 (see The Express Submit Flow)
  • Cut one song into several short highlight clips, each a standalone ready-to-post MP4 (see Music to Reels)
  • Manage your projects: list, search, and inspect them, including render state and download URLs (see Listing Your Projects)
  • Publish the result to social platforms like YouTube in one call, custom thumbnail included (see Publishing to Social Platforms)

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, Plan & Token Setup

You need three things before any curl call works: an account, an API-capable plan, and a personal API token. The /me health check at the end confirms all three are in place.

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. Pick an API-capable plan

API access is part of paid Dayvid plans. The Free and Lite tiers do NOT include API access: you can mint a token, but every endpoint except GET/api/v1/me rejects the request with a 402 telling you 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

Pick Start, Pro, or King and upgrade at 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 Token Lifecycle).
  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:
bash
export DAYVID_TOKEN="dvy_xxxxxxxxxxxxxxxxxxxxxxxx"
export DAYVID_BASE="https://dayvid.ai"

All requests below send Authorization: Bearer $DAYVID_TOKEN.

2.4. The /me health check

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.

bash
curl -s "$DAYVID_BASE/api/v1/me" \
  -H "Authorization: Bearer $DAYVID_TOKEN" | jq

Response:

json
{
  "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,
  "limits": {
    "publish": {
      "limit": 10,
      "used": 3,
      "scheduledPending": 2,
      "remaining": 5,
      "resetsAt": "2026-06-22T00:00:00.000Z",
      "perPlatformHourly": 5
    }
  }
}

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.

limits.publish is the per-billing-period publish quota, so you can size a batch before hitting action_denied on release. limit is your plan's allowance, used counts publishes that already fired this period, and scheduledPending counts posts still queued (scheduled) that will consume it when they fire (posts already publishing are counted in used, not here). remaining already subtracts both (max(0, limit - used - scheduledPending)), so it is the real number of NEW publishes you can still queue this period - schedule at most that many. The counter resets at resetsAt (your billing period end). perPlatformHourly is a separate burst cap: at most that many publishes per platform per rolling hour, regardless of monthly headroom.

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.

3. Recipes

3.1. How to create a vertical music video from a Suno song (Reels/Shorts/TikTok)

Vertical, AI-generated moving imagery, punchy subtitles, no brand:

bash
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"
    }
  }'

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

bash
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"
    }
  }'

3.3. How to create an anime-styled video from a custom MP3

Combines the upload flow with an aesthetic generation preset and visual guidelines.

bash
# 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\"
    }
  }"

3.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 Bringing Your Own Brand, including how assetStrategy controls whether matched reference images are reused as-is or re-styled.

bash
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"
    }
  }'

3.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:

bash
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 Job Status and Download.

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

bash
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

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

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

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

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

bash
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. You upload the bytes straight to it, so they never pass through the API. 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.

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

bash
curl -s -X PUT "$SIGNED_URL" \
  -H "Content-Type: audio/mpeg" \
  --data-binary "@$FILE"

Step 3: submit using audioStoragePath.

bash
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:

json
{
  "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); you upload the bytes straight to it, not through the API.

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

bash
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 renames it from the transcript once processing starts.

5.2. audioStoragePath XOR audioUrl (required, one of)

See The Express Submit Flow. 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 Bringing Your Own Brand 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 Discovering Your Brands).

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: 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 don't animate at all, 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

json
{ "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 (as of 2026-05) with two fields tweaked.

bash
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

The API itself is the source of truth: the canonical preset list is in 5.10, and the 422 font_not_available 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 is malformed. 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 to Reels, see Music to Reels).

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:

bash
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):

json
{
  "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.

bash
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 (see Job Status and Download). 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

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

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

json
[
  {
    "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 recent activity, not by this field.
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, so another account may have 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.

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

json
[
  {
    "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):

bash
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

bash
curl -s "$DAYVID_BASE/api/v1/projects" \
  -H "Authorization: Bearer $DAYVID_TOKEN" | jq

Response (200):

json
{
  "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.

bash
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 Music to Reels).
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).

bash
curl -s "$DAYVID_BASE/api/v1/projects/$PROJECT_ID" \
  -H "Authorization: Bearer $DAYVID_TOKEN" | jq

Response (200):

json
{
  "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",
      "clipPosition": null,
      "createdAt": "2026-05-21T10:00:00.000Z"
    }
  ],
  "scheduledPosts": [
    {
      "id": "uuid",
      "status": "scheduled",
      "scheduledAt": "2026-06-20T18:00:00.000Z",
      "providers": ["youtube"],
      "title": "Hung Up On You",
      "description": null,
      "hashtags": [],
      "tags": [],
      "brandId": "8c1e...",
      "projectId": "0a48e9b7-9b3e-4e3a-9c2f-1f9d8d2e7c10",
      "queueId": null,
      "clipPosition": null,
      "releasedJobId": null,
      "error": null,
      "createdAt": "2026-06-15T09:00:00.000Z",
      "updatedAt": "2026-06-15T09: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. Each entry carries clipPosition: the 0-based clip for a Music to Reels publish (see Music to Reels), or null for a single-track publish - so you can tell which clip a target came from without cross-referencing job ids.
  • scheduledPosts is the future side of the timeline: this project's posts that still matter, earliest first. It is a curated view - it hides canceled posts (you dropped them) and published posts (those are already in publishTargets), and keeps everything pending or needing attention (scheduled, publishing, draft, and failed). failed stays on purpose: a post that failed before dispatch has no publishTarget, so this is the only place you would see it. Empty array = nothing pending for this project. Need every state (including canceled/published) for one project? Use GET/api/v1/scheduled-posts?projectId=<id> (12.9), which applies no status filter by default.
  • provider vs providers is a cardinality difference, not a rename. A publishTargets[] entry is one already-fanned-out destination, so it carries a single provider and a per-platform status. A scheduledPosts[] entry is one pre-fan-out row that targets providers[] (an array) with a single overall status. When it releases, that one scheduled post becomes N publish targets (one per provider, each with its own status). So a scheduled post has no singular provider, and a publish target has no providers[] - don't expect either.
  • Reconciling future to past. When a scheduled post is released, its releasedJobId is set to the publish job it produced - the same value you see as a publishTargets[].jobId. So scheduledPost.releasedJobId === publishTarget.jobId is the deterministic join between "what I queued" and "what went out" (1 scheduled post to N targets), with clipPosition identifying the clip on both sides. No heuristic matching needed.
  • 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.

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

json
[
  {
    "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. words carries 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 to Reels submit). Before that, the endpoint returns 404 lyrics_not_available with 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 to Reels

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 to Reels is generally available (you still need an API-capable plan, like every /api/v1 endpoint). The endpoints below use the music-highlights path, which is the feature's internal name.

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:

json
{
  "settings": {
    "audioUrl": "https://suno.com/s/E6uWAXuAvVlQyJR6",
    "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 to Reels.
  • targetClipCount (optional integer, or omit / null for Auto — the default): how many clips to render. Leave it out (or send null) for Auto, and Dayvid picks the best number for the song. To force a specific count, pass an integer from 1; the effective maximum scales with the track's length (longer songs allow more, up to 8).
  • 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):

json
{
  "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).

bash
curl -s "$DAYVID_BASE/api/v1/music-highlights/$PROJECT_ID" \
  -H "Authorization: Bearer $DAYVID_TOKEN" | jq

Response:

json
{
  "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 to Reels 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

bash
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 per brand and platform, so if you publish using a different brandId you have to connect that platform 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 Job Status and Download) 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)

bash
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):

json
{
  "jobId": "uuid",
  "projectId": "uuid",
  "status": "processing",
  "pollUrl": "/api/v1/jobs/<jobId>"
}

12.2. Multiple platforms in one call

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

Publishing a Music to Reels clip. A Music to Reels project (see Music to Reels) has no single rendered MP4; it produces several clips, each its own video. To publish one, add clipPosition (the 0-based position from the GET/api/v1/music-highlights/<projectId> results) to the same request; you publish one clip per call:

bash
curl -s -X POST "$DAYVID_BASE/api/v1/publish" \
  -H "Authorization: Bearer $DAYVID_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "projectId":    "<music-highlights projectId>",
    "brandId":      "<brandId>",
    "clipPosition": 0,
    "providers":    ["youtube"],
    "title":        "Best moment"
  }' | jq

clipPosition is required for music-highlights projects (422 clip_position_required otherwise) and must not be sent for single-track projects (422 clip_position_not_supported). A clip that has not rendered yet returns 422 clip_not_rendered. The already-published protection (12.7) is scoped per clip, so publishing clip 1 to a platform is not blocked by having published clip 0 there.

12.3. Polling a publish job

bash
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):

json
{
  "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 (4.4): the request body needs only fileName (with an extension).

bash
# 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 Idempotency). 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:

    json
    {
      "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.

12.8. Scheduling a publish (delivery)

By default POST/api/v1/publish publishes immediately. The optional delivery field defers it instead. Every pre-dispatch check from the immediate path still runs (ownership, completed render, thumbnail validation, the already-published protection, oauth_required / platform_not_available, and your monthly publish quota): scheduling only moves when the upload happens, not whether it is allowed. The only check that is skipped is the per-platform hourly burst cap, which is meaningless when you are not publishing right now.

delivery takes one of three values:

  • "now" (default): publish immediately. Responds 202 with jobId / pollUrl, exactly as the examples above.
  • "schedule": publish at an explicit time. Requires scheduledAt, an ISO 8601 instant with an offset or Z, in the future.
  • "queue": publish into the next free slot of one of your posting queues. Requires queueId.

Both deferred modes respond 200 with the scheduled post instead of a job:

bash
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"],
    "title":       "Hung Up On You",
    "delivery":    "schedule",
    "scheduledAt": "2030-01-01T18:00:00Z"
  }' | jq
json
{
  "scheduledPostId": "uuid",
  "projectId": "uuid",
  "status": "scheduled",
  "scheduledAt": "2030-01-01T18:00:00.000Z",
  "queueId": null
}

When the slot arrives, Dayvid publishes the scheduled post through the very same pipeline as an immediate publish, so the per-platform outcome shows up in GET/api/v1/projects/<projectId> like any other publish. (There is no separate "scheduled job" to poll at submit time; the publish job appears when the post is released.)

Queues. A queue is a reusable posting schedule (e.g. "weekdays at 9am and 6pm") that you create and manage in the Dayvid web app under https://dayvid.ai/queues. Open a queue there and its id is the last path segment of the URL (https://dayvid.ai/queues/<queueId>). Pass that as queueId and Dayvid drops the post into the queue's next unoccupied slot:

bash
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"],
    "title":     "Hung Up On You",
    "delivery":  "queue",
    "queueId":   "<queueId from the queue page URL>"
  }' | jq

The response carries the resolved scheduledAt (the slot that was assigned) and the queueId.

Field contract. The discriminator and its field travel together: scheduledAt is only accepted with delivery: "schedule", and queueId only with delivery: "queue". Sending one without the matching delivery is rejected with 400 invalid_body (rather than silently publishing now), so a typo like passing scheduledAt but forgetting delivery fails loudly. A past scheduledAt is 422 scheduled_at_in_past; an unknown or paused queue is 404 queue_not_found / 422 queue_inactive; a queue with no free future slot is 422 no_available_slot.

12.9. Listing your scheduled posts

bash
curl -s "$DAYVID_BASE/api/v1/scheduled-posts?status=scheduled" \
  -H "Authorization: Bearer $DAYVID_TOKEN" | jq

Lists the posts you scheduled with delivery: "schedule" or "queue" (12.8) across all projects, earliest first. The optional status query narrows to one lifecycle state (draft, scheduled, publishing, published, failed, canceled); omit it to get every state. An unknown value is 400 invalid_status and the response lists the allowed set. The optional projectId query scopes to one project (a non-UUID is 400 invalid_project_id); combine it with status to filter both. This is the full-control counterpart to the scheduledPosts[] embedded in the project detail (10.7): the embedded array is curated (hides canceled/published), whereas ?projectId= here returns every state. The response is a plain JSON array:

json
[
  {
    "id": "uuid",
    "status": "scheduled",
    "scheduledAt": "2030-01-01T18:00:00.000Z",
    "providers": ["youtube"],
    "title": "Hung Up On You",
    "description": null,
    "hashtags": [],
    "tags": [],
    "brandId": "uuid",
    "projectId": "uuid",
    "queueId": null,
    "clipPosition": null,
    "releasedJobId": null,
    "error": null,
    "createdAt": "...",
    "updatedAt": "..."
  }
]

releasedJobId is null until the slot arrives; once Dayvid releases the post it carries the publish job id, which you poll at GET/api/v1/jobs/<jobId> for the per-platform outcome (the same job the immediate path returns). That same id appears as a publishTargets[].jobId on the project detail (10.7), so releasedJobId === jobId is the deterministic join from a scheduled post to what actually went out. Rate limit: 30 GET/min per user.

12.10. Canceling a scheduled post

bash
curl -s -X DELETE "$DAYVID_BASE/api/v1/scheduled-posts/<id>" \
  -H "Authorization: Bearer $DAYVID_TOKEN" | jq

Cancels a scheduled post by its id (the scheduledPostId from 12.8, or any id from the list above) so it never publishes. Soft cancel: the post stays visible as canceled, nothing is hard-deleted. Responds 200 with { "id": "...", "status": "canceled" }.

Only a post still armed can be canceled. One that Dayvid already started releasing, or that already published or failed, is 409 scheduled_post_not_cancelable with its current status. The cancel is atomic, so it never interrupts a publish already in flight. An unknown id, or one that is not yours, is 404 scheduled_post_not_found. Rate limit: 20 DELETE/min per user.

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.

bash
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 scheduled_at_required Publish delivery: "schedule" sent without scheduledAt (see 12.8).
400 scheduled_at_invalid Publish scheduledAt is not a valid date (see 12.8).
400 queue_id_required Publish delivery: "queue" sent without queueId (see 12.8).
400 brand_not_owned Music to Reels brandId is not a brand you own.
400 video_loop_slug_required Music to Reels 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 to Reels is temporarily disabled at the service level. Retry later or contact support.
404 project_not_found Project detail or Music to Reels results for a project that is missing or not yours (or, for highlights, not a highlights project).
404 queue_not_found Publish delivery: "queue": the queueId is unknown or not yours (see 12.8).
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 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 scheduled_at_in_past Publish delivery: "schedule": scheduledAt is in the past (see 12.8).
422 queue_inactive Publish delivery: "queue": the queue is paused (see 12.8).
422 no_available_slot Publish delivery: "queue": the queue has no free future slot (see 12.8).
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 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. 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.