426 lines
14 KiB
Markdown
426 lines
14 KiB
Markdown
---
|
||
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).
|