14 KiB
sidebar_position
| sidebar_position |
|---|
| 2 |
Endpoints
Set these for the examples below:
export BASE_URL="http://localhost:3000" # local / self-hosted app
export API_KEY="s2yt_live_your_key_here"
Local API docs: with npm run dev:all (or npm run docs:dev) open http://localhost:3001/docs/api/overview.
Discovery
curl "$BASE_URL/api/v1"
Returns the endpoint list and requirements (no auth).
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 (self-hosted)
type |
Formats | Max size |
|---|---|---|
image |
JPEG, PNG, WebP, GIF | 500 MB |
audio |
MP3, WAV, FLAC (also accepts related MIME types) | 500 MB |
logo |
PNG only | 500 MB |
font |
.ttf / .otf |
10 MB |
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 from paths (recommended)
Use the exact path strings returned by upload. Add one items[] entry per track.
Self-hosted deployments unlock per-track covers, custom watermarks/fonts, and art-track layouts. Max batch size: 100.
curl -X POST "$BASE_URL/api/v1/jobs" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"imagePath": "/uploads/.../cover.jpg",
"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": true,
"imagePath": "/uploads/.../track-cover.jpg",
"layout": {
"template": "COVER_LEFT_TEXT_RIGHT",
"blurAmount": 60,
"blurOpacity": 85,
"textPadding": 48,
"titleArtistGap": 12,
"textOffsetX": 0,
"textOffsetY": 0
},
"watermark": {
"mode": "default",
"fontKey": "montserrat",
"position": "bottom-right",
"offsetX": 24,
"offsetY": 24
},
"playlistId": null
}
}]
}'
Success response
{
"jobId": "clxxxxxxxx",
"itemCount": 1,
"status": "PENDING",
"playlist": null
}
playlist is set when you pass createPlaylist (see YouTube playlists).
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) |
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.
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). For custom, upload with type=font first and set fontPath. system maps to Arial in the dashboard preview.
For a full walkthrough of composition controls, see Video editing.
Art-track layouts
metadata.layout.template (or flat layout_template / layoutTemplate):
| 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 |
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 |
textPadding / text_padding |
16–120 | 48 | Padding around cover and text |
titleArtistGap / title_artist_gap |
0–64 | 10 | Space between title and artist |
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. 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"}'
{
"playlistId": "PLxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
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.
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) |
401 |
Missing or invalid API key |
403 |
YouTube not connected, or the request is not allowed for this account |
404 |
Job not found |
429 |
API rate limit exceeded — body includes retryAfterSeconds; header Retry-After is set |
Example rate-limit body:
{
"error": "API rate limit exceeded. Try again shortly.",
"retryAfterSeconds": 42
}
Self-hosted rate limits are effectively unlimited for normal use. See API overview.