openapi: 3.1.0
info:
  title: djstrich API
  description: Booking platform API for DJs, promoters, and booking agencies.
  version: 1.0.0
servers:
  - url: https://djstrich.de
    description: Production
tags:
  - name: v1
    description: Promoter API v1 — the stable, versioned surface for driving a
      promoter organisation from your own systems. Authenticated with an API
      key; every request is scoped to the organisation that key belongs to.
components:
  securitySchemes:
    apiKeyAuth:
      type: http
      scheme: bearer
      description: "Promoter API key, sent as `Authorization: Bearer djs_live_…`.
        Org-scoped and created by an organisation owner under Promoter → profile
        → API access. Only a SHA-256 hash is stored, so the token is shown
        exactly once at creation. Valid on `/api/v1/**` only; it is not accepted
        anywhere else on the site."
  responses:
    V1Unauthorized:
      description: Missing, invalid, revoked or expired API key. The body is the v1
        error envelope, whose `code` is one of missing_api_key, invalid_api_key,
        revoked_api_key, expired_api_key.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/V1Error"
    V1NotFound:
      description: The resource does not exist, or belongs to another organisation.
        These are deliberately indistinguishable — the API never confirms that
        another promoter's records exist.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/V1Error"
    V1RateLimited:
      description: Too many requests. Retry after the number of seconds in `Retry-After`.
      headers:
        Retry-After:
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/V1Error"
  schemas:
    V1Error:
      type: object
      description: Every /api/v1/** error uses this envelope. `code` is stable and
        safe to branch on; `message` is human-readable English and may be
        reworded.
      properties:
        error:
          type: object
          properties:
            code:
              type: string
            message:
              type: string
          required:
            - code
            - message
      required:
        - error
    WebhookEnvelope:
      type: object
      description: The JSON body djstrich POSTs to a registered webhook endpoint.
        Verify it with the `X-DJStrich-Signature` header before trusting it.
      properties:
        id:
          type: string
          format: uuid
          description: Unique per event. Shared across endpoints, so two receivers can
            dedupe.
        type:
          type: string
          enum:
            - booking.confirmed
            - booking.declined
            - booking.cancelled
            - booking.status_changed
            - booking.requested
            - invite.accepted
            - event.lineup_complete
            - event.is_beginning
            - event.has_ended
        created_at:
          type: string
          format: date-time
        data:
          type: object
          properties:
            booking_request_id:
              type: string
            status:
              type: string
            previous_status:
              type: string
              nullable: true
            changed_by:
              type: string
              enum:
                - dj
                - promoter
            event_set_slot_id:
              type: string
              nullable: true
            cancellation_reason:
              type: string
              nullable: true
            dj:
              type: object
            event:
              type: object
      required:
        - id
        - type
        - created_at
        - data
    Error:
      type: object
      properties:
        error:
          type: string
        message:
          type: string
        details:
          type: object
      required:
        - error
    SuccessResponse:
      type: object
      properties:
        ok:
          type: boolean
      required:
        - ok
    BookingProposal:
      type: object
      description: A structured counter-offer on a booking request. Budgets are whole
        EUR, matching the booking's own budget_min / budget_max.
      properties:
        id:
          type: string
        booking_request_id:
          type: string
        proposal_by:
          type: string
          enum:
            - dj
            - promoter
        proposal_status:
          type: string
          enum:
            - pending
            - accepted
            - declined
            - superseded
        proposed_budget_min:
          type: integer
          nullable: true
        proposed_budget_max:
          type: integer
          nullable: true
        proposed_set_time:
          type: string
          nullable: true
        created_by_user_id:
          type: string
        created_by_name:
          type: string
        created_at:
          type: string
        responded_at:
          type: string
          nullable: true
        responded_by_user_id:
          type: string
          nullable: true
paths:
  /api/v1/events:
    get:
      summary: List your organisation's events
      description: Every event belonging to the organisation the API key was issued
        for, newest first. There is no organisation parameter — the key
        determines the scope, so a key can never read another promoter's events.
      tags:
        - v1
      security:
        - apiKeyAuth: []
      responses:
        "200":
          description: Events for the key's organisation
          content:
            application/json:
              schema:
                type: object
                properties:
                  events:
                    type: array
                    items:
                      type: object
        "401":
          $ref: "#/components/responses/V1Unauthorized"
        "429":
          $ref: "#/components/responses/V1RateLimited"
  /api/v1/events/{eventId}/slots:
    get:
      summary: List an event's line-up slots
      description: >-
        The set slots of one event, including whether each is already filled and
        whether it is currently an open call. This is how you discover the
        `slot_id` values that `POST /api/v1/slots/{slotId}/book` and `POST
        /api/v1/djs/invite` take.


        An event belonging to a different organisation returns 404, not 403 —
        the API does not confirm that another promoter's records exist.
      tags:
        - v1
      security:
        - apiKeyAuth: []
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Event summary plus its slots
          content:
            application/json:
              schema:
                type: object
                properties:
                  event:
                    type: object
                    properties:
                      id:
                        type: string
                      name:
                        type: string
                      event_date:
                        type: string
                      is_archived:
                        type: boolean
                  slots:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        start_time:
                          type: string
                          nullable: true
                        end_time:
                          type: string
                          nullable: true
                        set_length_minutes:
                          type: integer
                          nullable: true
                        is_filled:
                          type: boolean
                        external_dj_name:
                          type: string
                          nullable: true
                        booking_request_id:
                          type: string
                          nullable: true
                        is_open_call:
                          type: boolean
                        genres:
                          type: array
                          items:
                            type: string
        "401":
          $ref: "#/components/responses/V1Unauthorized"
        "404":
          $ref: "#/components/responses/V1NotFound"
        "429":
          $ref: "#/components/responses/V1RateLimited"
    post:
      summary: Create line-up slots on an event
      description: >-
        Adds 1–50 new set slots to an event. Use this when an event was created
        without a line-up, or to extend one — `create_event` does not generate
        slots by itself.


        The batch is all or nothing: every slot is validated before anything is
        written, so a single bad `event_floor_id` rejects the whole call and
        leaves the event untouched. Slots come back in the same shape `GET`
        returns, so the response can be used directly to pick a `slot_id` for
        `POST /api/v1/slots/{slotId}/book`.


        An event belonging to a different organisation returns 404, not 403.
      tags:
        - v1
      security:
        - apiKeyAuth: []
      parameters:
        - name: eventId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                slots:
                  type: array
                  description: 1–50 slots to create
                  items:
                    type: object
                    properties:
                      slot_label:
                        type: string
                        description: Line-up label, e.g. "Opening" or "Headline"
                      start_time:
                        type: string
                        nullable: true
                        description: HH:MM
                      end_time:
                        type: string
                        nullable: true
                        description: HH:MM
                      set_length_minutes:
                        type: integer
                        nullable: true
                        maximum: 1440
                      genres:
                        type: array
                        items:
                          type: string
                        description: Folded into the event's music direction, and into the floor's own
                          genres when event_floor_id is set
                      is_open_call:
                        type: boolean
                        description: "Marks the slot as an open call. On its own this does not publish
                          anything — DJs can only apply once the event also has
                          applications_open: true (set via update_event)."
                      event_floor_id:
                        type: string
                        format: uuid
                        nullable: true
                        description: Must be a floor of THIS event; anything else rejects the whole
                          batch
              required:
                - slots
            example:
              slots:
                - slot_label: Opening
                  start_time: 23:00
                  end_time: 01:00
                  set_length_minutes: 120
                  genres:
                    - Techno
                - slot_label: Headline
                  start_time: 01:00
                  end_time: 03:00
                  set_length_minutes: 120
                  is_open_call: true
      responses:
        "201":
          description: The created slots
          content:
            application/json:
              schema:
                type: object
                properties:
                  slots:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        slot_label:
                          type: string
                        start_time:
                          type: string
                          nullable: true
                        end_time:
                          type: string
                          nullable: true
                        set_length_minutes:
                          type: integer
                          nullable: true
                        is_filled:
                          type: boolean
                        is_open_call:
                          type: boolean
                        event_floor_id:
                          type: string
                          nullable: true
                        genres:
                          type: array
                          items:
                            type: string
        "401":
          $ref: "#/components/responses/V1Unauthorized"
        "404":
          $ref: "#/components/responses/V1NotFound"
        "409":
          description: The event is archived
        "422":
          description: slots empty / longer than 50, or an event_floor_id from another event
        "429":
          $ref: "#/components/responses/V1RateLimited"
  /api/v1/djs:
    get:
      summary: Search DJ profiles
      description: Search the public DJ catalogue by name, city, country, genre and
        availability date. Returns approved, publicly listed profiles only. Use
        the `id` of a result as `dj_profile_id` when booking a slot.
      tags:
        - v1
      security:
        - apiKeyAuth: []
      parameters:
        - name: q
          in: query
          description: Free-text artist name search
          schema:
            type: string
        - name: city
          in: query
          description: Home-base city, or "worldwide"
          schema:
            type: string
        - name: country
          in: query
          description: ISO country code (DE, AT, CH, GB, NL, TH)
          schema:
            type: string
        - name: genres[]
          in: query
          description: Repeatable genre filter
          schema:
            type: array
            items:
              type: string
        - name: date
          in: query
          description: Availability date (YYYY-MM-DD). Excludes DJs who have blocked that
            day.
          schema:
            type: string
        - name: sort
          in: query
          schema:
            type: string
            enum:
              - best_match
              - newest
              - alpha
        - name: limit
          in: query
          description: Page size, default 24, max 48
          schema:
            type: integer
        - name: cursor
          in: query
          description: Pagination cursor from the previous response
          schema:
            type: string
      responses:
        "200":
          description: Matching DJ profiles
          content:
            application/json:
              schema:
                type: object
        "401":
          $ref: "#/components/responses/V1Unauthorized"
        "429":
          $ref: "#/components/responses/V1RateLimited"
  /api/v1/slots/{slotId}/book:
    post:
      summary: Offer a slot to a DJ who is on djstrich
      description: >-
        Creates a booking REQUEST for the given slot and notifies the DJ. It
        does not assign them: the slot stays open until the DJ accepts, and only
        their acceptance fills it. This is the same contract the web app has,
        and it is deliberate — a promoter cannot put a DJ's name on a public
        line-up without their consent.


        The event and organisation are derived server-side from `slotId`; any
        values you send for them are ignored. A slot belonging to another
        organisation returns 404.


        For a DJ who does not have a profile yet, use `POST /api/v1/djs/invite`
        instead.


        Rate limit: booking creation is additionally capped per user per 24
        hours (the same cap the web app applies). Exceeding it returns 429 with
        `retryAfterSeconds`.
      tags:
        - v1
      security:
        - apiKeyAuth: []
      parameters:
        - name: slotId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                dj_profile_id:
                  type: string
                  format: uuid
                  description: From GET /api/v1/djs
                message:
                  type: string
                  description: Message to the DJ, 1–500 characters
                set_time:
                  type: string
                  nullable: true
                  description: HH:MM
                set_length_minutes:
                  type: integer
                  nullable: true
                budget_min:
                  type: number
                  nullable: true
                budget_max:
                  type: number
                  nullable: true
              required:
                - dj_profile_id
                - message
            example:
              dj_profile_id: 8f14e45f-ceea-467a-9c8b-1f5c9c9c1a11
              message: Freitag, 2 Uhr Slot im Hauptraum. 90 Minuten, Techno.
              set_time: 02:00
              set_length_minutes: 90
      responses:
        "201":
          description: Booking request created; the DJ has been notified
          content:
            application/json:
              schema:
                type: object
        "401":
          $ref: "#/components/responses/V1Unauthorized"
        "404":
          $ref: "#/components/responses/V1NotFound"
        "409":
          description: The slot already holds a confirmed booking
        "422":
          description: Validation error (missing dj_profile_id or message, message length)
        "429":
          $ref: "#/components/responses/V1RateLimited"
  /api/v1/djs/invite:
    post:
      summary: Invite a DJ who is not on djstrich yet
      description: >-
        Creates a claimable invite link and tags the given slots with the DJ's
        name.


        This does NOT create a DJ profile. The profile comes into existence when
        the invitee opens the link and signs up, at which point the slots they
        were tagged on are attached to their new profile automatically and an
        `invite.accepted` webhook fires. Until then the slots show the name as
        an external tag.


        All slots must belong to your organisation; otherwise the call returns
        404.
      tags:
        - v1
      security:
        - apiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  description: The DJ's name as it should appear on the line-up
                slot_ids:
                  type: array
                  description: 1–50 slot ids from GET /api/v1/events/{eventId}/slots
                  items:
                    type: string
                    format: uuid
                locale:
                  type: string
                  enum:
                    - de
                    - en
                  description: Language of the invite landing page. Defaults to English.
              required:
                - name
                - slot_ids
            example:
              name: Nadia Kraft
              slot_ids:
                - 3c1e0b9a-7c1e-4f0a-9c1e-0b9a7c1e4f0a
              locale: de
      responses:
        "201":
          description: Invite created
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  token:
                    type: string
                  url:
                    type: string
                    description: Send this to the DJ
                  status:
                    type: string
        "401":
          $ref: "#/components/responses/V1Unauthorized"
        "404":
          $ref: "#/components/responses/V1NotFound"
        "422":
          description: Missing name, or slot_ids empty / longer than 50
        "429":
          $ref: "#/components/responses/V1RateLimited"
  /api/v1/bookings:
    get:
      summary: List booking requests (polling feed)
      description: >-
        Every booking request of your organisation, most recently updated first.
        This is the polling alternative to webhooks: keep the highest
        `updated_at` you have seen and pass it as `updated_since` on the next
        call, then follow `next_cursor` until it is null.


        Pagination is keyset-based on `(updated_at, id)`, so rows cannot be
        skipped or repeated when data changes between pages.
      tags:
        - v1
      security:
        - apiKeyAuth: []
      parameters:
        - name: updated_since
          in: query
          description: ISO 8601 timestamp; only rows updated at or after this
          schema:
            type: string
            format: date-time
        - name: status
          in: query
          description: Filter to one booking status (e.g. confirmed, cancelled)
          schema:
            type: string
        - name: limit
          in: query
          description: Page size, default 50, max 100
          schema:
            type: integer
        - name: cursor
          in: query
          description: The `next_cursor` from the previous response
          schema:
            type: string
      responses:
        "200":
          description: A page of booking requests
          content:
            application/json:
              schema:
                type: object
                properties:
                  bookings:
                    type: array
                    items:
                      type: object
                  next_cursor:
                    type: string
                    nullable: true
        "400":
          description: Invalid cursor or updated_since
        "401":
          $ref: "#/components/responses/V1Unauthorized"
        "429":
          $ref: "#/components/responses/V1RateLimited"
  /api/v1/webhooks:
    get:
      summary: List your webhook endpoints
      description: "Read-only. Registering and deleting endpoints is dashboard-only,
        on purpose: every API key is minted by an org owner, so a leaked key
        could otherwise register an attacker-controlled endpoint that keeps
        receiving your bookings even after the key is revoked. Keeping writes
        behind a browser session means revoking a key actually stops the leak."
      tags:
        - v1
      security:
        - apiKeyAuth: []
      responses:
        "200":
          description: Registered endpoints and the available event types
          content:
            application/json:
              schema:
                type: object
                properties:
                  endpoints:
                    type: array
                    items:
                      type: object
                  event_types:
                    type: array
                    items:
                      type: string
        "401":
          $ref: "#/components/responses/V1Unauthorized"
        "403":
          description: The key was not issued by an organisation owner
