Files
songs2vid/website/docs/api/endpoints.md
T
Atakan Doğan ÖzbanandCursor 56880256e5 Enlarge watermark badge for preview/encode parity and document video title fields.
Fix double-scaled badge preview, bump default watermark to 42% frame width, and update OSS docs/API for title vs songTitle separation and shared typography.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 18:28:07 +02:00

426 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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` | 0100 | 55 | Background `boxblur` intensity |
| `blurOpacity` / `blur_opacity` | 0100 | 100 | Blurred fill vs black |
| `textPadding` / `text_padding` | 16120 | 48 | Padding around cover and text |
| `titleArtistGap` / `title_artist_gap` | 064 | 10 | Space between title and artist |
| `textOffsetX` / `text_offset_x` | 120120 | 0 | Shift text block horizontally |
| `textOffsetY` / `text_offset_y` | 120120 | 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 edition/plan does not allow API features |
| `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).