Endpoints
Set these for the examples below:
export BASE_URL="https://songs2vid.com" # cloud SaaS (Developer+)
# export BASE_URL="http://localhost:3000" # local / self-hosted
export API_KEY="s2yt_live_your_key_here"
Auth on every protected call: -H "Authorization: Bearer $API_KEY" (no X-API-Key header).
Local docs: npm run docs:dev → http://localhost:3001/docs/api/overview. Automation: n8n integration.
Discovery
curl "$BASE_URL/api/v1"
Returns the endpoint list and requirements (no auth).
Manage API keys
Developer+ only. Auth: session cookie or Bearer API key. UI: Dashboard → Settings.
# List
curl "$BASE_URL/api/v1/user/api-keys" \
-H "Authorization: Bearer $API_KEY"
# Create (name optional; key shown once)
curl -X POST "$BASE_URL/api/v1/user/api-keys" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"n8n production"}'
{
"apiKey": "s2yt_live_…",
"prefix": "s2yt_live_…",
"name": "n8n production",
"message": "Copy this key now. It will not be shown again."
}
# Revoke one / all
curl -X DELETE "$BASE_URL/api/v1/user/api-keys?id=KEY_ID" \
-H "Authorization: Bearer $API_KEY"
curl -X DELETE "$BASE_URL/api/v1/user/api-keys" \
-H "Authorization: Bearer $API_KEY"
| Status | Body |
|---|---|
401 | Missing/invalid key when not signed in |
403 | { "error": "REST API access requires the Developer & Automation plan." } |
Upload a file (two-step)
curl -X POST "$BASE_URL/api/v1/upload" \
-H "Authorization: Bearer $API_KEY" \
-F "file=@cover.jpg" \
-F "type=image"
curl -X POST "$BASE_URL/api/v1/upload" \
-H "Authorization: Bearer $API_KEY" \
-F "file=@track1.mp3" \
-F "type=audio"
# Optional: PNG watermark logo
curl -X POST "$BASE_URL/api/v1/upload" \
-H "Authorization: Bearer $API_KEY" \
-F "file=@logo.png" \
-F "type=logo"
# Optional: custom font (.ttf / .otf, max 10 MB) for art-track / text watermark
curl -X POST "$BASE_URL/api/v1/upload" \
-H "Authorization: Bearer $API_KEY" \
-F "file=@Brand.ttf" \
-F "type=font"
Request fields
| Field | Required | Notes |
|---|---|---|
file | Yes | Multipart file |
type | Yes | image | audio | logo | font |
Allowed files
Limits depend on plan (cloud). Self-hosted uses the high OSS ceilings.
type | Formats | Typical max (Developer) |
|---|---|---|
image | JPEG, PNG, WebP, GIF | 50 MB |
audio | MP3, WAV, FLAC, AAC, OGG, M4A | 500 MB |
logo | PNG only | 50 MB |
font | .ttf / .otf | 10 MB |
Free cloud accounts: audio MP3 only, ≤50 MB, encode 192 kbps. Paid plans encode 320 kbps AAC (bitrate is not a request field).
Response
{
"path": "/uploads/.../track.mp3",
"filename": "track.mp3",
"size": 4123456,
"audioTags": {
"title": "Song Title",
"artist": "Artist Name",
"album": "Album",
"genre": "Electronic",
"year": "2024"
}
}
| Field | Notes |
|---|---|
path | Absolute path on the server — pass this into job create |
filename | Original filename |
size | Bytes |
audioTags | Present for MP3 when tags are readable; otherwise null. Fields may be omitted when missing in the file |
Upload the shared cover once (type=image), each audio (type=audio), optionally a PNG logo (type=logo), and optionally a custom font (type=font) for art-track typography and/or text watermarks.
Create job / render (recommended)
Use the exact path strings returned by upload. Add one items[] entry per track.
POST /api/v1/render is an n8n-friendly alias of POST /api/v1/jobs (identical body and response). GET /api/v1/render likewise lists jobs like GET /api/v1/jobs.
Cloud Developer unlocks per-track covers, custom watermarks/fonts, art-track layouts, and priority queue. Max batch size: 10 (Developer) / 50 (Enterprise) / 100 (self-hosted).
curl -X POST "$BASE_URL/api/v1/render" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"imagePath": "/uploads/.../cover.jpg",
"webhookUrl": "https://your-n8n.example/webhook/songs2vid-complete",
"items": [{
"audioPath": "/uploads/.../track.mp3",
"audioFilename": "track.mp3",
"metadata": {
"title": "My Artist - My Track (Official Audio)",
"songTitle": "My Track",
"artist": "My Artist",
"description": "",
"tags": "electronic",
"privacy": "PUBLIC",
"categoryId": "10",
"resolution": "1920x1080",
"notifySubscribers": true,
"madeForKids": false,
"embeddable": true,
"creativeCommons": false,
"includeWatermark": false,
"imagePath": "/uploads/.../track-cover.jpg",
"layout": {
"template": "LOWER_LEFT_COVER_TEXT",
"blurAmount": 60,
"blurOpacity": 85,
"textPadding": 48,
"titleArtistGap": 12,
"titleBold": true,
"textOffsetX": 0,
"textOffsetY": 0
},
"watermark": {
"mode": "none",
"fontKey": "montserrat",
"position": "bottom-right",
"offsetX": 24,
"offsetY": 24
},
"playlistId": null
}
}]
}'
Equivalent path: POST $BASE_URL/api/v1/jobs with the same JSON.
Top-level job fields
| Field | Required | Notes |
|---|---|---|
imagePath | Yes | Shared cover path from upload |
items | Yes | ≥1 track |
webhookUrl | No | Absolute http(s) URL — Songs2VID POSTs JSON on item/job terminal states (n8n) |
createPlaylist | No | { title, description?, privacy? } — creates a playlist and attaches all videos |
Success response
{
"jobId": "clxxxxxxxx",
"itemCount": 1,
"status": "PENDING",
"statusUrl": "/api/v1/jobs/clxxxxxxxx",
"webhookUrl": "https://your-n8n.example/webhook/songs2vid-complete",
"playlist": null
}
Poll with GET $BASE_URL + statusUrl. playlist is set when you pass createPlaylist (see YouTube playlists).
Create-job errors
| Status | When |
|---|---|
400 | Validation (missing files, bad layout, invalid webhookUrl, …) |
401 | Missing/invalid API key |
403 | Not on Developer+ · YouTube not connected · Pro-only feature (code: "PREMIUM_REQUIRED") |
402 | Monthly quota / credits exhausted |
429 | Rate limit |
Example Producer/Free API attempt:
{ "error": "REST API access requires the Developer & Automation plan." }
Example bad webhook:
{ "error": "Invalid webhookUrl. Provide an absolute http(s) URL for n8n callbacks." }
Metadata fields
| Field | Type | Notes |
|---|---|---|
title | string | YouTube video title. If empty, falls back to ${artist} - ${songTitle} when those are set |
songTitle | string | null | On-video song / track title for art-track layouts (max 120) |
artist | string | null | On-video artist line for art-track layouts (max 80) |
description | string | YouTube description |
tags | string | Comma-separated (quoted tags supported) |
privacy | string | PUBLIC | PRIVATE | UNLISTED |
categoryId | string | YouTube category ID (see below) |
resolution | string | One of the supported values (see below) |
notifySubscribers | boolean | YouTube upload notify flag |
madeForKids | boolean | COPPA / made for kids |
embeddable | boolean | Allow embedding |
creativeCommons | boolean | CC license vs standard YouTube |
includeWatermark | boolean | Apply watermark settings |
imagePath | string | null | Per-track cover (overrides job imagePath) |
backgroundImagePath | string | null | Lower-corner templates only: separate blur-fill image (else cover is blurred) |
playlistId | string | null | Existing playlist ID |
layout | object | Art-track layout (see below) |
watermark | object | Watermark settings (see below) |
Snake_case aliases are accepted for layout/watermark fields (e.g. blur_amount, layout_template).
Resolutions
| Value | Aspect |
|---|---|
1920x1080 | 16:9 |
1280x720 | 16:9 |
854x480 | 16:9 |
720x720 | 1:1 |
640x360 | 16:9 |
426x240 | 16:9 |
Self-hosted allows all of these. Cloud Free tops out at 720p (1280x720); Developer/Producer allow 1080p.
YouTube categories
Pass categoryId as a string ID. Common values:
| ID | Name |
|---|---|
1 | Film & Animation |
2 | Autos & Vehicles |
10 | Music |
15 | Pets & Animals |
17 | Sports |
19 | Travel & Events |
20 | Gaming |
22 | People & Blogs |
23 | Comedy |
24 | Entertainment |
25 | News & Politics |
26 | Howto & Style |
27 | Education |
28 | Science & Technology |
29 | Nonprofits & Activism |
Official reference: YouTube Data API — VideoCategories.
Watermark fields
watermark.position: top-left | top-right | bottom-left | bottom-right | center.
watermark.mode: none | default | text | logo
default— built-in Songs2VID badge PNG (assets/watermark.png), scaled to ~42% of frame width (matches Layout Studio preview)logo— requires priortype=logoupload; setlogoPath(same width scaling)text— custom string (max 80); settext
watermark.offsetX / offsetY: 0–200 (default 20) — pixels from the chosen anchor.
watermark.fontKey: system | inter | montserrat | roboto | oswald | playfair | custom. Styles art-track song title / artist and text watermarks (same .ttf files in preview and FFmpeg). system uses bundled Arimo in preview and encode.
For a full walkthrough of composition controls, see Video editing.
Art-track layouts
metadata.layout.template (or flat layout_template / layoutTemplate). Valid enums are also listed on GET /api/v1 as layoutTemplates. Mirrored pairs used by the Layout Studio composition grid are listed under compositionFamilies (side: cover beside text; lower: lower corner).
| Enum | Description |
|---|---|
COVER_LEFT_TEXT_RIGHT | Cover left, title & artist right |
COVER_TOP_TEXT_BOTTOM | Cover top, title & artist below |
COVER_RIGHT_TEXT_LEFT | Cover right, title & artist left |
CENTERED_COMPACT | Centered cover + text stack |
LOWER_LEFT_COVER_TEXT | Lower-left cover; textPadding is equal left + bottom inset (diagonal from frame corner) with title/artist to the right |
LOWER_RIGHT_COVER_TEXT | Lower-right cover; textPadding is equal right + bottom inset (diagonal from frame corner) with title/artist to the left |
For lower-corner templates only, optional metadata.backgroundImagePath (or background_image_path) sets a separate full-frame blur fill. Upload with type=image first, then pass the returned path. The cover (imagePath / per-item metadata.imagePath) stays the sharp corner square. Omit the field to blur the cover itself (default). blurAmount / blurOpacity still apply to whichever image is used as the fill.
Optional fine-tuning (clamped; camelCase or snake_case):
| Field | Range | Default | Purpose |
|---|---|---|---|
blurAmount / blur_amount | 0–100 | 55 | Background boxblur intensity |
blurOpacity / blur_opacity | 0–100 | 100 | Blurred fill vs black |
blurFill / blur_fill | boolean | false | Classic letterbox only: fill bars with blurred cover (ignored for art-track templates) |
textPadding / text_padding | 16–120 | 48 | Edge inset for cover/text. On lower-corner templates this value is applied equally on both axes (left=bottom or right=bottom) so the cover corner sits on a true diagonal from the frame corner |
titleArtistGap / title_artist_gap | 0–64 | 10 | Space between title and artist |
titleBold / title_bold | boolean | true | Bold song title (same in preview + FFmpeg) |
textOffsetX / text_offset_x | −120–120 | 0 | Shift text block horizontally |
textOffsetY / text_offset_y | −120–120 | 0 | Shift text block vertically |
Also set metadata.songTitle (max 120) and metadata.artist (max 80) for the on-video text lines. metadata.title remains the YouTube title (see Video editing — Video title, song title, and artist).
Omit layout.template (or use classic letterbox) when you only want a black-padded cover. Set layout.blurFill / blur_fill to true to fill letterbox bars with a blurred cover; then blurAmount / blurOpacity apply. Free-form cover coordinates (x, y, coverX, …) and layout-level offsetX/offsetY are rejected (use textOffsetX/textOffsetY instead; watermark offsets stay under watermark).
Invalid template strings return 400:
{ "error": "Invalid layout template. Refer to API documentation for valid enum values." }
YouTube playlists
List existing playlists, create a new one, or create one inline when starting a job. Pass playlistId in item metadata / batch defaults, or use createPlaylist to make a playlist and attach all videos to it.
Privacy may be public, unlisted, or private. If playlist permission was just added, sign out and sign in again so OAuth includes youtube.force-ssl.
YouTube playlist API reference: Playlists: insert.
# List playlists
curl "$BASE_URL/api/v1/playlists" \
-H "Authorization: Bearer $API_KEY"
# Create a playlist
curl -X POST "$BASE_URL/api/v1/playlists" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"My Album","description":"From Songs2VID","privacy":"unlisted"}'
{
"playlist": {
"id": "PLxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"title": "My Album",
"itemCount": 0,
"privacy": "unlisted"
}
}
Or create one inline with a job / batch request:
{
"createPlaylist": {
"title": "My Album",
"description": "Uploaded via Songs2VID",
"privacy": "private"
},
"defaults": { "privacy": "PUBLIC" },
"items": [{ "title": "Track One" }]
}
One-shot batch (small packs only)
Upload one cover image and a few audio files in a single multipart request. Not recommended for large batches — use two-step if you see FormData parse errors.
Batch create does not accept webhookUrl today. Prefer POST /api/v1/render with paths if you need n8n callbacks.
curl -X POST "$BASE_URL/api/v1/jobs/batch" \
-H "Authorization: Bearer $API_KEY" \
-F "image=@cover.jpg" \
-F "audio=@track1.mp3" \
-F "audio=@track2.mp3" \
-F 'metadata={"createPlaylist":{"title":"My Album","privacy":"unlisted"},"defaults":{"privacy":"PUBLIC"},"items":[{"title":"Track One"},{"title":"Track Two"}]}'
Optional metadata JSON supports defaults applied to every item and per-item overrides in items. Item order should match the order of audio files.
When item metadata is omitted, batch defaults include privacy PUBLIC, resolution 1920x1080, and watermark off unless overridden in defaults. Per-item title is the YouTube title (falls back from the audio filename when omitted). Pass songTitle and artist in items[] or defaults when using art-track layouts.
Multipart tips
- Do not set
Content-Typemanually for multipart; the client must include the boundary - In Postman: Body → form-data; each audio field key must be exactly
audio(type File) - If a file field shows a warning, re-select the file from disk
Poll job status
curl "$BASE_URL/api/v1/jobs/JOB_ID" \
-H "Authorization: Bearer $API_KEY"
curl "$BASE_URL/api/v1/jobs?limit=10" \
-H "Authorization: Bearer $API_KEY"
GET /api/v1/jobs accepts limit (default 20, max 100).
Job response
{
"id": "clxxxxxxxx",
"status": "PROCESSING",
"createdAt": "2026-07-25T12:00:00.000Z",
"completedAt": null,
"items": [
{
"id": "clitemxxx",
"audioFilename": "track.mp3",
"title": "My Track",
"description": "",
"tags": "electronic",
"privacy": "PUBLIC",
"categoryId": "10",
"resolution": "1920x1080",
"status": "ENCODING",
"youtubeVideoId": null,
"error": null
}
]
}
Job statuses
| Status | Meaning |
|---|---|
PENDING | Queued; worker has not started |
PROCESSING | At least one item is encoding or uploading |
COMPLETED | All items succeeded |
FAILED | All items failed |
PARTIAL | Mix of completed and failed items |
Item statuses
| Status | Meaning |
|---|---|
PENDING | Waiting in the queue |
ENCODING | FFmpeg is building the video |
UPLOADING | Uploading to YouTube |
COMPLETED | Live on YouTube (youtubeVideoId set) |
FAILED | Failed (error contains a message) |
Pipeline notes
- Each item is encoded, then uploaded; the local MP4 is removed after a successful upload
- Queue retries: 2 attempts with exponential backoff (5s base)
- Worker concurrency: 2 items in parallel
- On item failure, reserved allowance for that item is released
YouTube upload limits (channel daily caps, etc.) are enforced by Google, not Songs2VID. See YouTube Data API — Quota and compliance.
HTTP errors
| Status | When |
|---|---|
400 | Validation error (bad file type, invalid layout, missing fields, bad JSON, invalid webhookUrl) |
401 | Missing or invalid API key — "Missing API key. Use Authorization: Bearer <your_api_key>" / "Invalid API key" |
402 | Quota / credits exhausted on job create |
403 | YouTube not connected · not on Developer+ · Pro-only feature (PREMIUM_REQUIRED) |
404 | Job not found |
429 | API rate limit exceeded — body includes retryAfterSeconds; header Retry-After is set |
Exact Developer-gate message:
{ "error": "REST API access requires the Developer & Automation plan." }
Example rate-limit body:
{
"error": "API rate limit exceeded. Try again shortly.",
"retryAfterSeconds": 42
}
Cloud default rate limit: 60 requests / minute / API key. Self-hosted is effectively unlimited for normal use. See API overview and n8n.