openapi: 3.1.0
info:
  title: TracklistFinder API
  version: 1.0.0
  description: |
    Synchronous API for extracting and retrieving timestamped YouTube tracklists.

    ## Quick start

    Create an API key from your dashboard after your API plan is active, then submit a YouTube URL:

    ```
    curl https://tracklistfinder.com/api/v1/tracklists \
      -H 'Authorization: Bearer tlf_live_REPLACE_WITH_YOUR_KEY' \
      -H 'Content-Type: application/json' \
      -H 'Idempotency-Key: import-42' \
      --data '{"youtube_url":"https://www.youtube.com/watch?v=dQw4w9WgXcQ"}'
    ```

    The synchronous request returns a normalized tracklist with a public UUID, video metadata, timestamps, and track positions in seconds.

    ## Authentication

    Send your key as a bearer token in the `Authorization` header. Keys are shown once, may overlap during rotation, and can be revoked immediately from your dashboard.

    ## Usage & limits

    One successful new extraction consumes one unit. Cache hits, idempotent retries, no-tracklist results, and provider failures are free. Check `RateLimit-*` response headers and `GET /usage`.

    The API key currently includes up to 500 successful API extractions per month. Need more? Contact sales@tracklistfinder.com.

    Request bodies are limited to 16 KiB, YouTube URLs to 2,048 bytes, and idempotency keys to 255 bytes.

    ## Errors and retries

    Errors use a stable JSON envelope containing `error.code`, a safe human-readable `message`, the support-friendly `request_id`, and field errors when applicable. Retry transient `429` and `502` responses with backoff. Respect `Retry-After`, and reuse the same idempotency key only with the same input.

    ## Versioning and support

    Version 1 receives backward-compatible additions. Breaking changes require a new URL version and at least 90 days' deprecation notice.

    Do not use the service to violate copyright, privacy, YouTube terms, or applicable law. Contact support@tracklistfinder.com and include the response request ID when asking about a failed call.
servers:
  - url: https://tracklistfinder.com/api/v1
security:
  - bearerAuth: []
paths:
  /tracklists:
    get:
      operationId: listTracklists
      summary: List account-owned API results
      description: Returns tracklists this API account has created or previously retrieved through the API, newest first.
      parameters:
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
        - in: query
          name: offset
          schema: { type: integer, minimum: 0, default: 0 }
      responses:
        "200":
          description: A page of tracklists
          headers:
            RateLimit-Limit: { $ref: "#/components/headers/RateLimit-Limit" }
            RateLimit-Remaining: { $ref: "#/components/headers/RateLimit-Remaining" }
            RateLimit-Reset: { $ref: "#/components/headers/RateLimit-Reset" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TracklistListResponse" }
        "401": { $ref: "#/components/responses/Error" }
        "403": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/Error" }
    post:
      operationId: createTracklist
      summary: Extract a tracklist
      description: A successful new extraction consumes one monthly unit from the authenticated API key. Cache hits and idempotent replays are free.
      parameters:
        - in: header
          name: Idempotency-Key
          schema: { type: string, maxLength: 255 }
          description: Account-scoped retry key. Reuse with different input returns 409.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [youtube_url]
              properties:
                youtube_url: { type: string, format: uri, maxLength: 2048 }
            example: { youtube_url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ" }
      responses:
        "200": { $ref: "#/components/responses/Tracklist", description: Idempotent replay }
        "201": { $ref: "#/components/responses/Tracklist" }
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }
        "403": { $ref: "#/components/responses/Error" }
        "409": { $ref: "#/components/responses/Error" }
        "415": { $ref: "#/components/responses/Error" }
        "413": { $ref: "#/components/responses/Error" }
        "422": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/Error" }
        "502": { $ref: "#/components/responses/Error" }
        "503": { $ref: "#/components/responses/Error" }
  /tracklists/{id}:
    get:
      operationId: getTracklist
      summary: Retrieve an account-owned API result
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200": { $ref: "#/components/responses/Tracklist" }
        "401": { $ref: "#/components/responses/Error" }
        "403": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/Error" }
  /usage:
    get:
      operationId: getUsage
      summary: Get the current billing period's allowance and consumption for the authenticated API key
      responses:
        "200":
          description: Current usage
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    required: [period_start, period_end, allowance, consumed, remaining]
        "401": { $ref: "#/components/responses/Error" }
        "403": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/Error" }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: tlf_live
  headers:
    RateLimit-Limit: { schema: { type: integer } }
    RateLimit-Remaining: { schema: { type: integer } }
    RateLimit-Reset: { schema: { type: integer }, description: Unix timestamp }
  responses:
    Tracklist:
      description: A normalized tracklist
      headers:
        RateLimit-Limit: { $ref: "#/components/headers/RateLimit-Limit" }
        RateLimit-Remaining: { $ref: "#/components/headers/RateLimit-Remaining" }
        RateLimit-Reset: { $ref: "#/components/headers/RateLimit-Reset" }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/TracklistResponse" }
    Error:
      description: Stable error envelope
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorResponse" }
  schemas:
    TracklistResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [id, type, status, youtube, tracks, created_at]
          properties:
            id: { type: string, format: uuid }
            type: { const: tracklist }
            status: { enum: [succeeded] }
            youtube: { type: object }
            tracks:
              type: array
              items:
                type: object
                properties:
                  title: { type: string }
                  artist: { type: [string, "null"] }
                  timestamp: { type: [string, "null"] }
                  seconds: { type: [integer, "null"] }
            created_at: { type: string, format: date-time }
    TracklistListResponse:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/TracklistResponse/properties/data" }
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, request_id]
          properties:
            code: { type: string }
            message: { type: string }
            request_id: { type: string }
            fields: { type: object }
