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