--- sidebar_position: 2 --- # Endpoints Set these for the examples below: ```bash 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](http://localhost:3001/docs/api/overview). ## Discovery ```bash curl "$BASE_URL/api/v1" ``` Returns the endpoint list and requirements (**no auth**). ## Upload a file (two-step) ```bash curl -X POST "$BASE_URL/api/v1/upload" \ -H "Authorization: Bearer $API_KEY" \ -F "file=@cover.jpg" \ -F "type=image" ``` ```bash curl -X POST "$BASE_URL/api/v1/upload" \ -H "Authorization: Bearer $API_KEY" \ -F "file=@track1.mp3" \ -F "type=audio" ``` ```bash # Optional: PNG watermark logo curl -X POST "$BASE_URL/api/v1/upload" \ -H "Authorization: Bearer $API_KEY" \ -F "file=@logo.png" \ -F "type=logo" ``` ```bash # 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 ```json { "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**. ```bash 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 ```json { "jobId": "clxxxxxxxx", "itemCount": 1, "status": "PENDING", "playlist": null } ``` `playlist` is set when you pass `createPlaylist` (see [YouTube playlists](#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](https://developers.google.com/youtube/v3/docs/videoCategories/list). ### 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 prior `type=logo` upload; set `logoPath` (same width scaling) - `text` — custom string (max **80**); set `text` `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](../video-editing.md). ### 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](../video-editing.md#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**: ```json { "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](https://developers.google.com/youtube/v3/docs/playlists/insert). ```bash # List playlists curl "$BASE_URL/api/v1/playlists" \ -H "Authorization: Bearer $API_KEY" ``` ```bash # 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"}' ``` ```json { "playlistId": "PLxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } ``` Or create one inline with a job / batch request: ```json { "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. ```bash 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-Type` manually 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 ```bash curl "$BASE_URL/api/v1/jobs/JOB_ID" \ -H "Authorization: Bearer $API_KEY" ``` ```bash 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 ```json { "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](https://developers.google.com/youtube/v3/guides/quota_and_compliance_audits). ## 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: ```json { "error": "API rate limit exceeded. Try again shortly.", "retryAfterSeconds": 42 } ``` Self-hosted rate limits are effectively unlimited for normal use. See [API overview](./overview.md).