# 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"
```

Live HTML docs: [docs.songs2vid.com/docs/api/endpoints](https://docs.songs2vid.com/docs/api/endpoints).

## Discovery

```bash
curl "$BASE_URL/api/v1"
```

Returns the endpoint list and requirements (**no auth**). No billing routes are listed or implemented.

## 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)
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

| `type` | Formats | Max size |
|--------|---------|----------|
| `image` | JPEG, PNG, WebP, GIF | 500 MB |
| `audio` | MP3, WAV, FLAC | 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"
  }
}
```

`audioTags` is present for MP3 when tags are readable; otherwise `null`.

## Create job / render from paths (recommended)

Self-hosted unlocks per-track covers, custom watermarks/fonts, and art-track layouts. Max batch size: **100**. Watermarks are optional — nothing forces the default Songs2VID badge.

`POST /api/v1/render` is an **n8n-friendly alias** of `POST /api/v1/jobs` (identical body and response). `GET /api/v1/render` lists jobs like `GET /api/v1/jobs`.

Optional `webhookUrl` (absolute `http(s)` URL) makes Songs2VID POST JSON when items/jobs finish — preferred for n8n.

```bash
curl -X POST "$BASE_URL/api/v1/render" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "imagePath": "/uploads/.../cover.jpg",
    "webhookUrl": "https://your-n8n.example/webhook/songs2vid-complete",
    "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,
        "layout": {
          "template": "LOWER_LEFT_COVER_TEXT",
          "blurAmount": 60,
          "blurOpacity": 85,
          "textPadding": 48,
          "titleArtistGap": 12,
          "titleBold": true,
          "textOffsetX": 0,
          "textOffsetY": 0
        },
        "watermark": {
          "mode": "default",
          "fontKey": "montserrat",
          "position": "bottom-right",
          "offsetX": 24,
          "offsetY": 24
        },
        "playlistId": null
      }
    }]
  }'
```

Equivalent path: `POST $BASE_URL/api/v1/jobs` with the same JSON.

### Success response

```json
{
  "jobId": "clxxxxxxxx",
  "itemCount": 1,
  "status": "PENDING",
  "statusUrl": "/api/v1/jobs/clxxxxxxxx",
  "webhookUrl": "https://your-n8n.example/webhook/songs2vid-complete",
  "playlist": null
}
```

### Webhook payload

| `event` | Meaning |
|---------|---------|
| `job.item.completed` | One track finished (`youtubeVideoId` set) |
| `job.item.failed` | One track failed (`error` set) |
| `job.completed` | All items succeeded |
| `job.failed` | All items failed |
| `job.partial` | Mix of success and failure |

Community node: [`n8n-nodes-songs2vid`](https://www.npmjs.com/package/n8n-nodes-songs2vid) — primary source [git.atakanozban.com/Songs2VID](https://git.atakanozban.com/Songs2VID); see [docs/n8n.md](../n8n.md) and [docs.songs2vid.com/docs/n8n](https://docs.songs2vid.com/docs/n8n).

<details>
<summary>Legacy example without webhook (same metadata shape)</summary>

```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",
        "privacy": "PUBLIC",
        "categoryId": "10",
        "resolution": "1920x1080",
        "includeWatermark": false
      }
    }]
  }'
```

</details>

### Metadata fields

| Field | Type | Notes |
|-------|------|--------|
| `title` | string | YouTube video title |
| `songTitle` | string \| null | On-video song title for art-track (max **120**) |
| `artist` | string \| null | On-video artist line (max **80**) |
| `description` | string | YouTube description |
| `tags` | string | Comma-separated |
| `privacy` | string | `PUBLIC` \| `PRIVATE` \| `UNLISTED` |
| `categoryId` | string | YouTube category ID |
| `resolution` | string | See resolutions below |
| `notifySubscribers` | boolean | YouTube notify flag |
| `madeForKids` | boolean | Made for kids |
| `embeddable` | boolean | Allow embedding |
| `creativeCommons` | boolean | CC vs standard YouTube license |
| `includeWatermark` | boolean | Apply watermark settings |
| `imagePath` | string \| null | Per-track cover |
| `backgroundImagePath` | string \| null | Lower-corner templates only: separate blur-fill image (else cover is blurred) |
| `playlistId` | string \| null | Existing playlist ID |
| `layout` | object | Art-track layout |
| `watermark` | object | Watermark settings |

Snake_case aliases are accepted for layout/watermark fields.

### Resolutions

| Value | Aspect |
|-------|--------|
| `1920x1080` | 16:9 |
| `1280x720` | 16:9 |
| `854x480` | 16:9 |
| `720x720` | 1:1 |
| `640x360` | 16:9 |
| `426x240` | 16:9 |

### YouTube categories

| 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 |

### Watermark fields

`watermark.mode`: `none` | `default` | `text` | `logo`

- `none` — no overlay
- `default` — built-in Songs2VID badge PNG (`assets/watermark.png`)
- `logo` — requires prior `type=logo` upload; set `logoPath`
- `text` — custom string (max **80**); set `text`

`watermark.position`: `top-left` | `top-right` | `bottom-left` | `bottom-right` | `center`

`watermark.offsetX` / `offsetY`: `0`–`200` (default `20`)

`watermark.fontKey`: `system` | `inter` | `montserrat` | `roboto` | `oswald` | `playfair` | `custom` (styles art-track text and text watermarks)

### Art-track layouts

`metadata.layout.template` (or flat `layout_template` / `layoutTemplate`). Valid enums are also listed on `GET /api/v1` as `layoutTemplates`. Mirrored pairs used by the Layout Studio composition grid are listed under `compositionFamilies` (`side`: cover beside text; `lower`: lower corner).

| 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 |
| `LOWER_LEFT_COVER_TEXT` | Lower-left cover; `textPadding` is equal left + bottom inset (diagonal from frame corner) with title/artist to the right |
| `LOWER_RIGHT_COVER_TEXT` | Lower-right cover; `textPadding` is equal right + bottom inset (diagonal from frame corner) with title/artist to the left |

For lower-corner templates only, optional `metadata.backgroundImagePath` (or `background_image_path`) sets a separate full-frame blur fill. Upload with `type=image` first, then pass the returned path. The cover (`imagePath` / per-item `metadata.imagePath`) stays the sharp corner square. Omit the field to blur the cover itself (default). `blurAmount` / `blurOpacity` still apply to whichever image is used as the fill.

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 |
| `blurFill` / `blur_fill` | boolean | `false` | Classic letterbox only: fill bars with blurred cover (ignored for art-track templates) |
| `textPadding` / `text_padding` | 16–120 | 48 | Edge inset for cover/text. On lower-corner templates this value is applied equally on both axes (left=bottom or right=bottom) so the cover corner sits on a true diagonal from the frame corner |
| `titleArtistGap` / `title_artist_gap` | 0–64 | 10 | Space between title and artist |
| `titleBold` / `title_bold` | boolean | `true` | Bold song title (preview + FFmpeg) |
| `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.

Omit `layout.template` for classic letterbox (black-padded cover by default). Set `layout.blurFill` / `blur_fill` to `true` to fill letterbox bars with a blurred cover; then `blurAmount` / `blurOpacity` apply. Free-form cover coordinates (`x`, `y`, `coverX`, …) and layout-level `offsetX`/`offsetY` are **rejected**.

Invalid template strings return **400**:

```json
{ "error": "Invalid layout template. Refer to API documentation for valid enum values." }
```

## YouTube playlists

```bash
curl "$BASE_URL/api/v1/playlists" \
  -H "Authorization: Bearer $API_KEY"
```

```bash
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"}'
```

Or pass `createPlaylist` on a job / batch body to create a playlist and attach all videos.

## One-shot batch (small packs only)

```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={"defaults":{"privacy":"PUBLIC"},"items":[{"title":"Track One"},{"title":"Track Two"}]}'
```

Prefer two-step upload + jobs for larger packs.

## 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 statuses

| Status | Meaning |
|--------|---------|
| `PENDING` | Queued |
| `PROCESSING` | Encoding or uploading |
| `COMPLETED` | All items succeeded |
| `FAILED` | All items failed |
| `PARTIAL` | Mix of completed and failed |

### Item statuses

| Status | Meaning |
|--------|---------|
| `PENDING` | Waiting |
| `ENCODING` | FFmpeg building video |
| `UPLOADING` | Uploading to YouTube |
| `COMPLETED` | Live (`youtubeVideoId` set) |
| `FAILED` | Failed (`error` message) |

YouTube channel daily upload caps are enforced by Google, not Songs2VID.

## HTTP errors

| Status | When |
|--------|------|
| `400` | Validation error |
| `401` | Missing or invalid API key |
| `403` | YouTube not connected, or request not allowed |
| `404` | Job not found |
| `429` | API rate limit exceeded |

There is no `402` payment / credits status in OSS.
