# SET — API Specification

**Version** 0.1 · maggio '26
**Stack** Next.js 14 (App Router) · Postgres (Neon) · Cloudinary · Vercel Edge
**Auth** Clerk (sessions) · API tokens for agency white-label

---

## 0 · Conventions

- All endpoints under `/api/v1/`
- JSON in/out unless noted (file uploads: multipart)
- Timestamps: ISO 8601 UTC
- IDs: ULID strings
- Errors: `{ error: { code, message, details? } }` with HTTP status
- Rate limits: 100 req/min per IP for public, 600 req/min per user for authed

### Entity inheritance · multi-segment

SET is a single multi-tenant platform. The base `tenant` entity extends to 4 concrete types: `agency`, `festival`, `club`, `venue`. **Primary entities differ per tenant type but all share core schema**:

```
tenant (base) {
  id: ulid PK
  type: enum('agency','festival','club','venue')
  slug: text (unique, URL path: set.fm/{slug})
  design_system_id: ulid FK
  owner_user_id: ulid FK
  status: enum('draft','live','archived')
  created_at, updated_at: timestamptz
  members: tenant_member[]  -- shared join
}
```

Each tenant type has its own primary entity tree:

- `agency` → `artist[]` → `release[] · rider[] · gallery_item[]`
- `festival` → `edition[]` → `lineup_slot[] · stage[]` (v1.1 roadmap — M3+)
- `club` → `resident[] · night[]` → `room[]` (v1.1 roadmap — M9+)
- `venue` → `event[]` (v1.2 roadmap — M12+, self-serve)

API endpoints below are **v1.0 = agency only · production-ready M1**. Festival/club/venue endpoints land in v1.1 / v1.2 as the data model extension ships (see `17-set-product-spec.md` §2-bis Segment configuration matrix).

---

## 1 · Public surface

These power the `/[slug]/[artist]` EPK render and aggregate marketing pages. ISR + edge cache.

### `GET /api/v1/epk/:agencySlug/:artistSlug`
Returns the full EPK payload for public render. Public, no auth. Edge-cached 60s.

**Response 200**
```json
{
  "artist": {
    "id": "01J...", "slug": "nira", "name": "NIRA",
    "tagline": "TECHNO · hypnotic dub · Berlin × Roma",
    "bio": "...", "styleTags": ["techno","hypnotic","dub"],
    "homeBase": "Berlin / Roma",
    "available": { "from": "2026-10-01", "regions": ["EU","UK","JP"] }
  },
  "music": { "featured": {...}, "releases": [...] },
  "live": { "upcoming": [...], "highlights": [...] },
  "press": { "quotes": [...], "logos": [...], "gallery": [...] },
  "logistics": { "feeTier": "GATED", "rider": { "version": "v4.3", "downloadUrl": "..." } },
  "contacts": [...],
  "stats": { "openCount": 47, "lastUpdated": "2026-05-11T..." },
  "agency": { "name": "AFG Records", "designSystem": "afg-v1" }
}
```

### `GET /api/v1/epk/:agencySlug/:artistSlug/rider/:type`
Signed-URL redirect to Cloudinary asset. Logs download event.

### `POST /api/v1/epk/:agencySlug/:artistSlug/track-open`
Beacon endpoint. Public.
```json
{ "viewerId": "anon-cookie", "referrer": "...", "country": "DE", "dwell": 192 }
```

### `POST /api/v1/epk/:agencySlug/:artistSlug/booking-inquiry`
Public form submit. Spam-protected (turnstile token).
```json
{
  "promoter": "Marcel Dettmann presents",
  "email": "...", "city": "Berlin · DE",
  "eventDate": "2026-07-12", "ask": "€ 6500",
  "message": "..."
}
```

### `POST /api/v1/epk/:agencySlug/:artistSlug/fee-gate`
Submits agency credentials → grants short-lived `agency-view-token` cookie that unlocks fee tier on the public page.

---

## 2 · Agency surface (auth gated)

All require `Authorization: Bearer <token>` with `agency:read` or `agency:write` scope.

### `GET /api/v1/agency/:id/roster?status=live&sort=opens&limit=50`
Multi-roster overview for dashboard.

**Response 200**
```json
{
  "agency": { "id":"...", "name":"AFG Records", "tier":"mid", "rosterLimit":40 },
  "artists": [
    {
      "id":"...", "slug":"nira", "name":"NIRA", "code":"A — TCH",
      "status":"live", "opens30d":47, "inquiriesOpen":3, "riderVersion":"v4.3",
      "avatar":"https://res.cloudinary.com/..."
    }
  ],
  "total": 28
}
```

### `GET /api/v1/agency/:id/inquiries?status=hot&limit=20`
Booking inquiry pipeline.

### `GET /api/v1/agency/:id/analytics?from=2026-04-13&to=2026-05-13`
Aggregate metrics: opens, dwell, countries, conversion to inquiry, by-artist breakdown.

### `PATCH /api/v1/inquiries/:id`
Update inquiry status: `new` → `acknowledged` → `negotiating` → `confirmed` / `declined`.

---

## 3 · Editor surface (artist + agency)

Powers desktop editor + mobile app. Scope: `artist:write` (self) or `agency:write` (any roster artist).

### `GET /api/v1/artists/:id`
Full artist record with section blocks (draft + published states).

### `PATCH /api/v1/artists/:id/section/:sectionId`
Block-level updates. Autosave.
```json
{ "blocks": [ { "id":"A.02", "type":"wordmark", "content":"NIRA" } ] }
```
Returns `{ version:"v4.4-draft", savedAt: "..." }`. Diff is stored; published version unchanged.

### `POST /api/v1/artists/:id/publish`
Commits draft → live, increments version, invalidates ISR for the slug.
```json
{ "fromVersion":"v4.3", "toVersion":"v4.4", "summary":"Updated bio + new portrait" }
```

### `GET /api/v1/artists/:id/versions?limit=20`
Version history for the version pane.

### `POST /api/v1/artists/:id/rider/upload`
Multipart upload. Server validates, stores, increments rider version. Returns Cloudinary URL.

### `POST /api/v1/artists/:id/ai-rewrite`
Calls Claude Haiku 4.5 for AI bio/tagline polish. Returns suggestion only; editor applies on user confirm.
```json
{ "blockId":"A.06", "instruction":"polish", "model":"haiku-4-5" }
```

---

## 4 · Integrations

OAuth callbacks + webhooks for music platforms and booking software.

| Endpoint | Purpose |
|---|---|
| `GET /api/v1/oauth/spotify/callback` | Spotify Web API token exchange → sync monthly listeners, top tracks |
| `GET /api/v1/oauth/soundcloud/callback` | SoundCloud OAuth → sync latest upload to featured mix |
| `GET /api/v1/oauth/bandcamp/callback` | Bandcamp → embed releases |
| `GET /api/v1/oauth/google-calendar/callback` | Google Calendar sync for `available` window |
| `POST /api/v1/webhooks/gigwell` | Gigwell booking confirmations push → mark inquiry as `confirmed` |
| `POST /api/v1/webhooks/stagent` | Same, Stagent |

---

## 5 · Data model (short form)

### v1.0 · Agency (production-ready M1)

```
agency        (id, name, slug, tier, rosterLimit, designSystemId, ownerId, createdAt)
artist        (id, agencyId, slug, name, code, status, lastPublishedVersion)
section       (id, artistId, type, order, draft jsonb, published jsonb, lockedFields)
block         (denormalized inside section.draft / .published — for fast render)
rider         (id, artistId, type, version, fileUrl, createdBy, createdAt)
inquiry       (id, artistId, agencyId, promoter, eventDate, city, ask, status, conversation jsonb)
event-open    (artistId, viewerCookie, country, referrer, dwell, ts)  -- columnar
contact       (id, artistId, role, name, email, scope)
designSystem  (id, tokens jsonb, fonts jsonb, photographyDirection)
user          (id, email, role, agencyId, artistId)
```

### v1.1 · Festival (M3–M9 roadmap)

```
festival      (id, name, slug, designSystemId, ownerId, foundingYear, status, createdAt)
edition       (id, festivalId, slug, editionNumber, startDate, endDate, city, country, status, createdAt)
stage         (id, editionId, name, capacity, isMain, schedule jsonb, createdAt)
lineup_slot   (id, editionId, stageId, artistId, slotStart, slotEnd, position, setType, isHeadline, status, createdAt)
sponsor       (id, festivalId, tier, name, logoUrl, visibility jsonb, contractStart, contractEnd, value)
press_kit_item (id, festivalId, type, version, fileUrl, language, scope)
```

### v1.1 · Club (M9–M12 roadmap)

```
club          (id, name, slug, designSystemId, ownerId, foundedYear, address, capacity, status, createdAt)
room          (id, clubId, name, capacity, defaultGenre, status, createdAt)
resident      (id, clubId, artistId, residencyStart, frequencyPerYear, primaryRoom, status, createdAt)
night         (id, clubId, roomId, date, slotStart, slotEnd, series, ticketPrice, status, lineup jsonb)
night_dj      (id, nightId, artistId, slotOrder, setType, isHeadline)
press_archive (id, clubId, type, source, url, dateCovered, language)
```

### v1.2 · Venue Lite (M12+ self-serve)

```
venue         (id, name, slug, designSystemId, ownerId, tier, address, status, createdAt)
event         (id, venueId, slug, title, date, slotStart, slotEnd, ticketUrl, status, description, lineup jsonb)
```

**Migration rule**: tenant base table holds shared columns. Type-specific entities live in their own tables, linked via FK to the tenant base. RLS policy: each user role has read/write scope by `tenant_id + type`.

---

## 6 · Build sequence (12 weeks)

| Week | Milestone |
|---|---|
| 1-2 | Auth + agency provisioning + design system bootstrap |
| 3-4 | Editor MVP (sections, autosave, publish flow) |
| 5-6 | Public EPK render + ISR + edge cache |
| 7-8 | Analytics pipeline + inquiry routing |
| 9-10 | Music integrations OAuth |
| 11 | Agency dashboard + permissions |
| 12 | Mobile app shell (Capacitor wrapping editor) + polish |

---

## 7 · Non-functional

- p95 EPK render < 800ms at maturity
- WCAG 2.2 AA baseline
- prefers-reduced-motion respected
- All media via Cloudinary fetch (WebP/AVIF auto)
- Font: Geist self-hosted woff2, preloaded display weight
- Rate limit, turnstile on all public POSTs
- All inquiry conversations encrypted at rest (Postgres pgcrypto)
