openapi: 3.0.3
info:
  title: Wild Card Ticketing API
  version: "1.1.0"
  x-logo:
    url: https://thewildcardapp.com/brand/wordmark-400.png
    altText: Wild Card
  description: |
    Server-to-server API for ticketing clients. Create and manage your events and
    ticket types, issue **sharable (group) tickets**, and read what has been
    purchased from you.

    ## Authentication
    Send your client API key as a bearer token on every request:

    ```
    Authorization: Bearer wc_live_xxxxxxxxxxxxxxxx
    ```

    Generate and revoke keys in your Wild Card client portal. Keep them server
    side; a key grants full access to your events and sales.

    ## Conventions
    - **Money is always in pence** (integer). `price_pence: 1000` is £10.00.
    - **Times are ISO 8601** strings, e.g. `2026-12-31T20:00:00+00:00`.
    - Every request is scoped to your client. You can only read and write your own
      events, ticket types, allocations and orders.
    - Errors return `{ "error": "message" }` with the matching HTTP status
      (`401` bad key, `404` not yours / not found, `422` validation).

    ## Versioning
    The API is versioned in the path. This document describes **v1**, served at
    `https://api.thewildcardapp.com/api/v1`. Within v1 we only add fields and
    endpoints, never remove or repurpose them, so you can integrate against v1 and
    rely on it. A breaking change would ship as a new path prefix (`/api/v2`).

    ## Sharable (group) tickets
    A sharable ticket type is a **group**, for example a table of 10. The flow:

    1. Create a ticket type with `is_sharable: true` and a `group_size`
       (`POST /events/{id}/ticket-types`).
    2. Issue a group to an owner by email (`POST /events/{id}/allocations`). Wild
       Card emails the owner a one-time code.
    3. The owner adds the group in the Wild Card app with that code, then shares a
       link. Each friend who opens the link claims one seat, and a real ticket
       lands in their app, until the group is used up.

    You never handle the seats yourself: issue the group, and Wild Card runs the
    claim, the emails and the QR tickets.
servers:
  - url: https://api.thewildcardapp.com/api/v1
    description: Production
security:
  - ApiKey: []
tags:
  - name: Account
    description: Your client profile.
  - name: Events
    description: Create and manage your events.
  - name: Ticket types
    description: The tickets on sale for an event, including sharable (group) tickets.
  - name: Sharable tickets
    description: Issue a group of tickets to an owner who shares the seats out.
  - name: Orders
    description: Read what has been purchased from you.
components:
  securitySchemes:
    ApiKey:
      type: http
      scheme: bearer
      description: "Your client API key, e.g. Authorization: Bearer wc_live_..."
  schemas:
    TicketType:
      type: object
      properties:
        id: { type: integer, example: 42 }
        event_id: { type: integer, example: 7 }
        name: { type: string, example: "General admission" }
        description: { type: string, nullable: true, example: "Standing, doors 7pm" }
        price_pence: { type: integer, example: 1500, description: "Price in pence. 0 = free allocation." }
        currency: { type: string, example: "gbp" }
        capacity: { type: integer, nullable: true, example: 200, description: "null = unlimited" }
        max_per_order: { type: integer, nullable: true, example: 6 }
        plus_only: { type: boolean, example: false }
        is_sharable: { type: boolean, example: false, description: "A sharable group ticket (e.g. a table of N). Issue it with POST /events/{id}/allocations." }
        group_size: { type: integer, nullable: true, example: null, description: "Seats per issued group, when is_sharable is true." }
        is_active: { type: boolean, example: true }
        sold: { type: integer, example: 37 }
    Allocation:
      type: object
      description: One issued group of a sharable ticket type.
      properties:
        id: { type: integer, example: 5 }
        uuid: { type: string, example: "9d1f7c2a-4b6e-4f0a-9c3d-2e1b5a7c8d9e" }
        event_id: { type: integer, example: 7 }
        ticket_type_id: { type: integer, example: 42 }
        label: { type: string, nullable: true, example: "Ringside table" }
        quantity: { type: integer, example: 10, description: "Seats in this group." }
        owner_email: { type: string, format: email, example: "owner@example.com" }
        owner_name: { type: string, nullable: true, example: "Jordan Steel" }
        share_url: { type: string, example: "https://thewildcardapp.com/t/7KQ4M1P2", description: "The link the owner shares with their friends." }
        claimed: { type: integer, example: 0 }
        remaining: { type: integer, example: 10 }
        is_active: { type: boolean, example: true }
    Event:
      type: object
      properties:
        id: { type: integer, example: 7 }
        name: { type: string, example: "Friday Live Session" }
        description: { type: string, nullable: true, example: "Live music, 18+" }
        start_date: { type: string, format: date-time, nullable: true, example: "2026-12-05T20:00:00+00:00" }
        end_date: { type: string, format: date-time, nullable: true, example: "2026-12-06T01:00:00+00:00" }
        is_active: { type: boolean, example: true }
        ticket_types:
          type: array
          items: { $ref: '#/components/schemas/TicketType' }
    Order:
      type: object
      properties:
        uuid: { type: string, example: "1b2c3d4e-0000-1111-2222-333344445555" }
        event_id: { type: integer, example: 7 }
        status: { type: string, enum: [pending, paid, failed, cancelled], example: "paid" }
        currency: { type: string, example: "gbp" }
        subtotal_pence: { type: integer, example: 3000 }
        booking_fee_pence: { type: integer, example: 225 }
        application_fee_pence: { type: integer, example: 225, description: "Platform fee kept by Wild Card (booking fee + commission)." }
        total_pence: { type: integer, example: 3225 }
        tickets: { type: integer, example: 2 }
        paid_at: { type: string, format: date-time, nullable: true, example: "2026-11-20T18:04:11+00:00" }
        created_at: { type: string, format: date-time, nullable: true, example: "2026-11-20T18:03:55+00:00" }
    Error:
      type: object
      properties:
        error: { type: string, example: "That ticket type is not a sharable (group) ticket." }
paths:
  /me:
    get:
      tags: [Account]
      summary: Your client profile + Stripe Connect status
      responses:
        '200':
          description: OK
          content:
            application/json:
              example:
                organiser: { id: 3, name: "The Brickworks", type: "standard", status: "active" }
                stripe: { connect_ready: true }
        '401':
          description: Missing or invalid API key
          content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
  /events:
    get:
      tags: [Events]
      summary: List your events
      responses:
        '200':
          description: OK
          content:
            application/json:
              example:
                events:
                  - { id: 7, name: "Friday Live Session", start_date: "2026-12-05T20:00:00+00:00", is_active: true }
    post:
      tags: [Events]
      summary: Create an event
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                description: { type: string }
                start_date: { type: string, format: date-time }
                end_date: { type: string, format: date-time }
            example:
              name: "Friday Live Session"
              description: "Live music, 18+"
              start_date: "2026-12-05T20:00:00+00:00"
              end_date: "2026-12-06T01:00:00+00:00"
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { type: object, properties: { event: { $ref: '#/components/schemas/Event' } } }
  /events/{id}:
    get:
      tags: [Events]
      summary: Get one event
      parameters: [{ name: id, in: path, required: true, schema: { type: integer } }]
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: object, properties: { event: { $ref: '#/components/schemas/Event' } } } } } }
        '404': { description: Not found, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
    patch:
      tags: [Events]
      summary: Update an event
      parameters: [{ name: id, in: path, required: true, schema: { type: integer } }]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                description: { type: string, nullable: true }
                start_date: { type: string, format: date-time, nullable: true }
                end_date: { type: string, format: date-time, nullable: true }
                is_active: { type: boolean }
            example:
              is_active: true
      responses:
        '200': { description: OK }
  /events/{id}/ticket-types:
    post:
      tags: [Ticket types]
      summary: Add a ticket type to an event
      description: |
        Create a ticket type. Set `is_sharable: true` with a `group_size` to make
        it a sharable (group) ticket that you then issue with
        `POST /events/{id}/allocations`.
      parameters: [{ name: id, in: path, required: true, schema: { type: integer } }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, price_pence]
              properties:
                name: { type: string }
                description: { type: string }
                price_pence: { type: integer, minimum: 0 }
                currency: { type: string, example: gbp }
                capacity: { type: integer, nullable: true }
                max_per_order: { type: integer, nullable: true }
                plus_only: { type: boolean }
                is_sharable: { type: boolean, description: "Mark this as a sharable group ticket (e.g. a table of N)." }
                group_size: { type: integer, nullable: true, description: "Seats per group. Required when is_sharable is true." }
                is_active: { type: boolean }
            examples:
              standard:
                summary: A normal paid ticket
                value:
                  name: "General admission"
                  price_pence: 1500
                  capacity: 200
                  max_per_order: 6
              sharable:
                summary: A sharable group ticket (a table of 10)
                value:
                  name: "Ringside table"
                  price_pence: 0
                  is_sharable: true
                  group_size: 10
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { type: object, properties: { ticket_type: { $ref: '#/components/schemas/TicketType' } } }
  /ticket-types/{id}:
    patch:
      tags: [Ticket types]
      summary: Update a ticket type
      parameters: [{ name: id, in: path, required: true, schema: { type: integer } }]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                price_pence: { type: integer, minimum: 0 }
                capacity: { type: integer, nullable: true }
                is_sharable: { type: boolean }
                group_size: { type: integer, nullable: true }
                is_active: { type: boolean }
            example:
              is_active: false
      responses:
        '200': { description: OK }
        '404': { description: Not found }
  /events/{id}/allocations:
    post:
      tags: [Sharable tickets]
      summary: Issue a sharable (group) ticket to an owner
      description: |
        Issue one group of a sharable ticket type (e.g. a table of 10) to an owner
        identified by email. Wild Card emails the owner a claim code they enter in
        the app to take ownership, after which they share the seats with their
        friends. The ticket type must have `is_sharable: true`.

        `quantity` defaults to the ticket type's `group_size`; pass it to override
        the size of this one group.
      parameters: [{ name: id, in: path, required: true, schema: { type: integer } }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ticket_type_id, owner_email]
              properties:
                ticket_type_id: { type: integer, description: "Must be a sharable ticket type on this event." }
                owner_email: { type: string, format: email }
                owner_name: { type: string }
                quantity: { type: integer, minimum: 1, description: "Seats in this group. Defaults to the type's group_size." }
                label: { type: string }
                code: { type: string, description: "Optional preferred share code; auto-generated if omitted." }
            example:
              ticket_type_id: 42
              owner_email: "owner@example.com"
              owner_name: "Jordan Steel"
              quantity: 10
              label: "Ringside table"
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { type: object, properties: { allocation: { $ref: '#/components/schemas/Allocation' } } }
              example:
                allocation:
                  id: 5
                  uuid: "9d1f7c2a-4b6e-4f0a-9c3d-2e1b5a7c8d9e"
                  event_id: 7
                  ticket_type_id: 42
                  label: "Ringside table"
                  quantity: 10
                  owner_email: "owner@example.com"
                  owner_name: "Jordan Steel"
                  share_url: "https://thewildcardapp.com/t/7KQ4M1P2"
                  claimed: 0
                  remaining: 10
                  is_active: true
        '422':
          description: Ticket type is missing or is not sharable
          content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
  /events/{id}/orders:
    get:
      tags: [Orders]
      summary: List orders for an event (what has been purchased from you)
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
        - { name: status, in: query, required: false, schema: { type: string, enum: [pending, paid, failed, cancelled] } }
        - { name: per_page, in: query, required: false, schema: { type: integer, maximum: 100 } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              example:
                orders:
                  - { uuid: "1b2c3d4e-0000-1111-2222-333344445555", status: "paid", total_pence: 3225, tickets: 2 }
  /orders/{uuid}:
    get:
      tags: [Orders]
      summary: Get one order
      parameters: [{ name: uuid, in: path, required: true, schema: { type: string } }]
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { type: object, properties: { order: { $ref: '#/components/schemas/Order' } } }
        '404': { description: Not found }
