openapi: 3.0.3
info:
  title: Goboblo Partner API
  version: "1.0.0"
  description: >
    Server-to-server API for booking partners. Your backend calls Goboblo with a
    secret key to read availability, create and cancel bookings, and read the menu
    for the locations your key is allowed to see. Goboblo owns the booking itself
    (table plan, double-booking protection, guest confirmation email).
    This spec is handwritten and kept in sync with the v1 contract.
  contact:
    name: Goboblo Partner Support
    email: help@goboblo.com
servers:
  - url: https://api.goboblo.com/v1/partner-api
    description: Production
security:
  - bearerAuth: []
  - apiKeyHeader: []
tags:
  - name: Availability
  - name: Bookings
  - name: Menu
paths:
  /locations/{locationId}/availability:
    get:
      tags: [Availability]
      summary: Read available time slots for a day
      description: >
        Requires scope `availability:read`. Designed for sub-1-second responses.
        Micro-cached ~8 seconds per locationId|date|partySize. If the location is
        closed that day, `slots` is an empty array.
      operationId: getAvailability
      parameters:
        - $ref: "#/components/parameters/LocationId"
        - name: date
          in: query
          required: true
          description: Date in YYYY-MM-DD.
          schema:
            type: string
            format: date
            example: "2026-07-20"
        - name: partySize
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
            example: 2
      responses:
        "200":
          description: Availability for the requested day.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AvailabilityResponse"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
  /locations/{locationId}/availability/batch:
    post:
      tags: [Availability]
      summary: Read availability across a date range and party sizes in one call
      description: >
        Requires scope `availability:read`. A whole date range multiplied by several
        party sizes in one call. Limits: max 92 days (endDate >= startDate),
        1-12 party sizes, each size 1-50. Outside the limits returns 400. Duplicate
        party sizes are merged. Days with no availability are omitted from `days`.
      operationId: getAvailabilityBatch
      parameters:
        - $ref: "#/components/parameters/LocationId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BatchRequest"
      responses:
        "200":
          description: Availability grouped per (day, party size).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BatchResponse"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
  /locations/{locationId}/bookings:
    post:
      tags: [Bookings]
      summary: Create a booking
      description: >
        Requires scope `bookings:write`. Goes through the same booking engine as the
        guest widget (concurrency-safe). Send an idempotencyKey so a retry never
        creates a duplicate: the same key against the same location returns the same
        booking. The booking is marked source=partner and channel=partner:<slug>.
      operationId: createBooking
      parameters:
        - $ref: "#/components/parameters/LocationId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateBookingRequest"
      responses:
        "201":
          description: Booking created (or, on idempotent replay, the existing booking).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BookingCreated"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
  /bookings/{bookingId}:
    get:
      tags: [Bookings]
      summary: Read booking status
      description: >
        Requires scope `bookings:read`. Returns no extra guest PII beyond what you
        sent in yourself. A booking your key cannot see returns 404.
      operationId: getBooking
      parameters:
        - $ref: "#/components/parameters/BookingId"
      responses:
        "200":
          description: The booking.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BookingStatus"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
  /bookings/{bookingId}/cancel:
    post:
      tags: [Bookings]
      summary: Cancel a booking
      description: >
        Requires scope `bookings:write`. Idempotent: an already-cancelled booking
        returns { "ok": true }. A booking in a final state that cannot be cancelled
        returns 400.
      operationId: cancelBooking
      parameters:
        - $ref: "#/components/parameters/BookingId"
      responses:
        "200":
          description: Cancelled (or already cancelled).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OkResponse"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
  /locations/{locationId}/menu:
    get:
      tags: [Menu]
      summary: Read the restaurant's menu
      description: Requires scope `menu:read`.
      operationId: getMenu
      parameters:
        - $ref: "#/components/parameters/LocationId"
      responses:
        "200":
          description: The menu.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MenuResponse"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "Authorization: Bearer gbp_..."
    apiKeyHeader:
      type: apiKey
      in: header
      name: x-goboblo-key
      description: Equivalent alternative to the Bearer header.
  parameters:
    LocationId:
      name: locationId
      in: path
      required: true
      description: Goboblo location UUID.
      schema:
        type: string
        format: uuid
        example: "00000000-0000-0000-0000-000000000010"
    BookingId:
      name: bookingId
      in: path
      required: true
      schema:
        type: string
        example: "b1a2c3d4"
  responses:
    BadRequest:
      description: Invalid input (e.g. wrong date format, invalid partySize).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Missing or invalid key.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Forbidden:
      description: Key lacks the scope, or may not see the location.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Booking or location not found (or not visible to the key).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Conflict:
      description: The time/table was just taken, or the location is closed.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    TooManyRequests:
      description: Per-key rate limit exceeded.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    ServiceUnavailable:
      description: The Partner API is not enabled yet (operational state) - try again later.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
  schemas:
    Error:
      type: object
      description: >
        Standard NestJS error shape. The message string is human-readable and may be
        localized - branch on statusCode.
      properties:
        statusCode:
          type: integer
          example: 403
        message:
          type: string
          example: "saknar scope: bookings:write"
        error:
          type: string
          example: "Forbidden"
      required: [statusCode, message]
    Slot:
      type: object
      properties:
        slot:
          type: string
          format: date-time
          example: "2026-07-20T17:00:00.000Z"
        available:
          type: boolean
          example: true
        remainingCovers:
          type: integer
          example: 12
      required: [slot, available, remainingCovers]
    AvailabilityResponse:
      type: object
      properties:
        locationId:
          type: string
          format: uuid
        date:
          type: string
          format: date
        timezone:
          type: string
          example: "Europe/Stockholm"
        slots:
          type: array
          items:
            $ref: "#/components/schemas/Slot"
      required: [locationId, date, timezone, slots]
    BatchRequest:
      type: object
      properties:
        startDate:
          type: string
          format: date
          example: "2026-07-20"
        endDate:
          type: string
          format: date
          example: "2026-08-18"
        partySizes:
          type: array
          minItems: 1
          maxItems: 12
          items:
            type: integer
            minimum: 1
            maximum: 50
          example: [2, 4, 6]
      required: [startDate, endDate, partySizes]
    DayAvailability:
      type: object
      properties:
        date:
          type: string
          format: date
        partySize:
          type: integer
        slots:
          type: array
          items:
            $ref: "#/components/schemas/Slot"
      required: [date, partySize, slots]
    BatchResponse:
      type: object
      properties:
        locationId:
          type: string
          format: uuid
        startDate:
          type: string
          format: date
        endDate:
          type: string
          format: date
        timezone:
          type: string
          example: "Europe/Stockholm"
        days:
          type: array
          items:
            $ref: "#/components/schemas/DayAvailability"
      required: [locationId, startDate, endDate, timezone, days]
    CreateBookingRequest:
      type: object
      properties:
        startsAt:
          type: string
          format: date-time
          example: "2026-07-20T17:00:00.000Z"
        partySize:
          type: integer
          example: 4
        name:
          type: string
          example: "Anna Andersson"
        email:
          type: string
          format: email
          example: "anna@example.com"
        phone:
          type: string
          example: "+46701234567"
        notes:
          type: string
          example: "Window table if possible"
        partnerBookingRef:
          type: string
          description: Your own booking id, mirrored back in status and webhooks.
          example: "rwg-abc-123"
        idempotencyKey:
          type: string
          maxLength: 120
          description: Retry key. Same key + location returns the same booking, never a duplicate.
          example: "your-unique-retry-key-123"
        lang:
          type: string
          enum: [sv, en, no, da]
          description: Guest confirmation/reminder language.
      required: [startsAt, partySize, name, email]
    BookingCreated:
      type: object
      properties:
        id:
          type: string
          example: "b1a2c3d4"
        startsAt:
          type: string
          format: date-time
        status:
          type: string
          example: "confirmed"
        partnerBookingRef:
          type: string
          nullable: true
      required: [id, startsAt, status]
    BookingStatus:
      type: object
      properties:
        id:
          type: string
        locationId:
          type: string
          format: uuid
        startsAt:
          type: string
          format: date-time
        partySize:
          type: integer
        status:
          type: string
          example: "confirmed"
        source:
          type: string
          example: "partner"
        channel:
          type: string
          example: "partner:reserve-with-google"
        partnerBookingRef:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
      required: [id, locationId, startsAt, partySize, status, source, channel, createdAt]
    OkResponse:
      type: object
      properties:
        ok:
          type: boolean
          example: true
      required: [ok]
    MenuItem:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: string
          nullable: true
        price_ore:
          type: integer
          description: Price in minor units (öre).
          example: 16500
        tags:
          type: array
          items:
            type: string
      required: [id, name]
    MenuCategory:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: string
          nullable: true
        items:
          type: array
          items:
            $ref: "#/components/schemas/MenuItem"
      required: [id, name, items]
    MenuResponse:
      type: object
      properties:
        locationName:
          type: string
          example: "Krogen"
        currency:
          type: string
          example: "SEK"
        categories:
          type: array
          items:
            $ref: "#/components/schemas/MenuCategory"
      required: [locationName, currency, categories]
