> ## Documentation Index
> Fetch the complete documentation index at: https://hack-club-pin-docs-urls.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Create an event in the series

> **Requires a series API key.** A mobile token, a global API token and an
event API key are all refused with `403` — an event has to land in a
series, and only a series key names one unambiguously.

The series is taken from the key, never from the request body. You
cannot create an event in another series by passing a different
`event_series_id`: a foreign value is rejected with `403` rather than
silently overridden (passing the key's own series id or slug is fine and
redundant).

### Required fields

`name` and `support_email` — exactly what the web's new-event form
requires. `support_email` must be on `@hackclub.com` or
`@events.hackclub.com`: it is the from and reply-to address on every
participant and guardian email, so it has to be an address Hack Club
controls.

Everything else is optional here because it is optional on the web too:

- `slug` is generated from `name` when omitted (lowercase, dashes).
- `timezone` defaults to `UTC`.
- Schedule, location and module fields are collected by the web's setup
  wizard *after* creation, and may all be sent in this one call instead.

### What this does not do

- **Logo and banner** are not accepted. Upload them from the event's
  dashboard; multipart uploads are out of scope for this endpoint.
- **Setup is not marked complete.** Like the web's create step, the new
  event is a draft (`setup_complete: false`) until someone finishes the
  setup wizard — which is also where waiver templates are configured.
  Registration should not be opened before that.
- **Waiver / DocuSeal templates** are not settable here.

### Side effects

The key's owner is given an explicit `event_admin` role on the new
event, mirroring what the web does for the series member who creates
one. The creation is written to the audit log, attributed to the key's
owner with the key's name in the metadata.




## OpenAPI

````yaml /openapi.yml post /series/{series_id}/events
openapi: 3.0.3
info:
  title: Attend API
  version: v1
  description: >
    REST API for [Attend](https://attend.hackclub.com), Hack Club's event

    management platform. The API is used by the Attend mobile app and by

    trusted server-to-server integrations.


    ## Authentication


    Almost every endpoint requires a bearer token:


    ```

    Authorization: Bearer <token>

    ```


    Four kinds of token are accepted:


    - **Mobile token** — issued to a signed-in user by `POST /session`. Sets the
      current user. May return the response header
      `X-Token-Refresh-Recommended: true` when the token is nearing expiry;
      call `POST /session/refresh` to rotate it.
    - **Global API token** — a superadmin-issued token. Only honoured while its
      owner is still a global admin. Sets the current user. May be **scoped**
      when issued, in which case it reaches only the endpoints that accept
      that scope and returns `403` everywhere else, read-only endpoints
      included. An unscoped token carries its owner's full access. Scopes are
      noted on the endpoints that accept them; the only one today is
      `bans:write`.
    - **Series API key** — one key for a whole event series, issued from
      **Series → API Keys** in the dashboard by a series owner. Does **not** set
      a current user. It acts as an event API key on *every* event in its
      series, and it is the only credential that can create an event. Scopes do
      not apply — a series key's reach is its series. See the **Series**
      section.
    - **Event API key** — per-event key (`EventApiToken`, or the legacy
      `Event#api_key`). Does **not** set a current user, so it only works on the
      subset of endpoints that don't require one (participant `lookup`,
      `roster`, `create`, and the travel calendar).

    Missing or invalid credentials return `401 { "error": "Unauthorized" }`.


    ### What an API key cannot do


    Neither key kind sets a current user, so endpoints that must attribute work

    to a person stay closed to both — regardless of how broad the key is. A

    series key is wider in *reach* (more events), never in *depth*:


    | Endpoint | Series key | Event key |

    | --- | --- | --- |

    | `GET/POST /series/…` | ✅ its own series | ❌ `403` |

    | `POST /series/{id}/events` | ✅ its own series | ❌ `403` |

    | Participant `lookup`, `roster`, `create` | ✅ any event in the series | ✅
    its own event |

    | `GET /events/{id}/travel` | ✅ any event in the series | ✅ its own event |

    | `GET /events/{id}/participants` (full payload) | ❌ `403` | ❌ `403` |

    | Notes, scans, scan contexts, Slack blasts, NFC badges | ❌ `403` | ❌ `403`
    |


    Either key can be revoked without credentials by POSTing its own secret to

    `POST /tokens/revoke`; Attend emails the key's owner (or, if their account

    is gone, the series owners) when that happens.


    ## Conventions


    - All timestamps are ISO 8601 strings and may be `null` where noted.

    - Errors use the envelope `{ "error": "<message>" }`.

    - `:event_id` path segments accept a numeric id or the event slug on the
      participants endpoints; other endpoints accept the numeric id only.

    > **Note:** Inbound webhook receivers (Postmark, Help Scout, DocuSeal,

    > Slack events) also live under `/api/v1` but are called by third parties,

    > not by API consumers, and are not documented here.
servers:
  - url: https://attend.hackclub.com/api/v1
    description: Production
  - url: http://localhost:3000/api/v1
    description: Local development
security:
  - bearerAuth: []
tags:
  - name: Authentication
    description: Obtain, refresh, and revoke mobile bearer tokens.
  - name: Account
    description: The authenticated user's profile and accessible events.
  - name: Series
    description: >-
      Event series, and the events inside one, addressed with a single series
      API key. Creating an event lives here — an event has to belong to a
      series, and only a series key names one unambiguously.
  - name: Participants
    description: Participant registrations for an event.
  - name: Notes
    description: Staff notes on a participant's registration.
  - name: Scans
    description: QR / NFC / manual scans and check-in.
  - name: Scan Contexts
    description: The scan stations configured for an event.
  - name: Travel Calendar
    description: Chronological participant travel and pickup operations.
  - name: Slack Blasts
    description: Broadcast Slack messages to an event's participants.
  - name: NFC Badges
    description: Provision and manage participant NFC badges.
  - name: Push Tokens
    description: Register device push-notification tokens.
  - name: Bans
    description: The app-wide ban list. Global admins only.
  - name: Travel
    description: >-
      Airport and flight lookups. These endpoints authenticate with the web
      session cookie (Devise), not a bearer token.
paths:
  /series/{series_id}/events:
    post:
      tags:
        - Series
      summary: Create an event in the series
      description: |
        **Requires a series API key.** A mobile token, a global API token and an
        event API key are all refused with `403` — an event has to land in a
        series, and only a series key names one unambiguously.

        The series is taken from the key, never from the request body. You
        cannot create an event in another series by passing a different
        `event_series_id`: a foreign value is rejected with `403` rather than
        silently overridden (passing the key's own series id or slug is fine and
        redundant).

        ### Required fields

        `name` and `support_email` — exactly what the web's new-event form
        requires. `support_email` must be on `@hackclub.com` or
        `@events.hackclub.com`: it is the from and reply-to address on every
        participant and guardian email, so it has to be an address Hack Club
        controls.

        Everything else is optional here because it is optional on the web too:

        - `slug` is generated from `name` when omitted (lowercase, dashes).
        - `timezone` defaults to `UTC`.
        - Schedule, location and module fields are collected by the web's setup
          wizard *after* creation, and may all be sent in this one call instead.

        ### What this does not do

        - **Logo and banner** are not accepted. Upload them from the event's
          dashboard; multipart uploads are out of scope for this endpoint.
        - **Setup is not marked complete.** Like the web's create step, the new
          event is a draft (`setup_complete: false`) until someone finishes the
          setup wizard — which is also where waiver templates are configured.
          Registration should not be opened before that.
        - **Waiver / DocuSeal templates** are not settable here.

        ### Side effects

        The key's owner is given an explicit `event_admin` role on the new
        event, mirroring what the web does for the series member who creates
        one. The creation is written to the audit log, attributed to the key's
        owner with the key's name in the metadata.
      parameters:
        - $ref: '#/components/parameters/SeriesIdNested'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - event
              properties:
                event:
                  $ref: '#/components/schemas/EventCreateAttributes'
            examples:
              minimal:
                summary: Only what the web requires
                value:
                  event:
                    name: Sunbeam Summer 2027
                    support_email: sunbeam@hackclub.com
              fully_configured:
                summary: Basics, schedule, location and modules in one call
                value:
                  event:
                    name: Sunbeam Summer 2027
                    slug: sunbeam-summer-2027
                    support_email: sunbeam@hackclub.com
                    timezone: Europe/Helsinki
                    starts_at: '2027-07-12T09:00:00Z'
                    ends_at: '2027-07-15T17:00:00Z'
                    registration_open_at: '2027-04-01T00:00:00Z'
                    registration_close_at: '2027-06-01T00:00:00Z'
                    venue_name: Kaapelitehdas
                    location_city: Helsinki
                    location_country: Finland
                    location_address: Tallberginkatu 1
                    travel_enabled: true
                    accommodation_enabled: true
                    groups_enabled: true
      responses:
        '201':
          description: Event created, in the key's own series.
          content:
            application/json:
              schema:
                type: object
                properties:
                  event:
                    $ref: '#/components/schemas/SeriesEvent'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: |
            One of:

            - the caller is not a series API key (`Creating an event requires a
              series API key.`)
            - the key belongs to a different series
            - the body named a different series in `event_series_id`
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: >-
            A required field was missing, or the event failed validation (bad
            support-email domain, slug already taken, reserved slug).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                missing_required:
                  value:
                    error: name and support_email are required
                bad_support_email:
                  value:
                    error: >-
                      Support email must be a @hackclub.com or
                      @events.hackclub.com address
components:
  parameters:
    SeriesIdNested:
      name: series_id
      in: path
      required: true
      schema:
        type: string
      description: >-
        Series UUID, series slug, or the literal `current` — the series the
        calling series API key belongs to. `current` is only valid for a series
        API key.
  schemas:
    EventCreateAttributes:
      allOf:
        - $ref: '#/components/schemas/EventWritableAttributes'
        - type: object
          required:
            - name
            - support_email
          properties:
            event_series_id:
              type: string
              description: >-
                Not needed — the series comes from the key. If sent it must
                match the key's own series (id or slug); any other value is
                rejected with `403`.
    SeriesEvent:
      allOf:
        - $ref: '#/components/schemas/SeriesEventSummary'
        - type: object
          properties:
            modules:
              $ref: '#/components/schemas/EventModules'
            hotel_scan_context_id:
              type: string
              format: uuid
              nullable: true
            participant_count:
              type: integer
              example: 120
    Error:
      type: object
      properties:
        error:
          type: string
          example: Unauthorized
    EventWritableAttributes:
      type: object
      description: >-
        Every event field this API can write — the same set as the web's event
        settings form plus its setup wizard, minus the series (decided by the
        key), the logo/banner (uploaded from the dashboard) and the DocuSeal
        waiver templates (configured in the setup wizard).
      properties:
        name:
          type: string
          example: Sunbeam Summer 2027
        slug:
          type: string
          description: >-
            Lowercase letters, numbers and dashes. Generated from `name` when
            omitted. Must be unique, and cannot be a reserved word (`new`,
            `admin`, `api`, `series`, …).
          example: sunbeam-summer-2027
        support_email:
          type: string
          format: email
          description: >-
            Must be `@hackclub.com` or `@events.hackclub.com` — it is the from
            and reply-to address on every participant and guardian email.
          example: sunbeam@hackclub.com
        timezone:
          type: string
          description: >-
            IANA identifier or Rails zone name. Defaults to `UTC`. Zone-less
            timestamps (`2027-07-12T09:00`) are interpreted in this zone, so
            send it alongside the schedule; timestamps with an offset or `Z` are
            taken as given.
          example: Europe/Helsinki
        starts_at:
          type: string
          format: date-time
          nullable: true
        ends_at:
          type: string
          format: date-time
          nullable: true
        registration_open_at:
          type: string
          format: date-time
          nullable: true
        registration_close_at:
          type: string
          format: date-time
          nullable: true
        venue_name:
          type: string
          nullable: true
          example: Kaapelitehdas
        location_city:
          type: string
          nullable: true
          example: Helsinki
        location_country:
          type: string
          nullable: true
          example: Finland
        location_address:
          type: string
          nullable: true
          description: >-
            Saving an address, venue, city or country geocodes it in the
            background, filling in latitude and longitude — so a later `GET` may
            show coordinates you never sent.
          example: Tallberginkatu 1
        location_latitude:
          type: number
          format: double
          nullable: true
        location_longitude:
          type: number
          format: double
          nullable: true
        freedom_waivers_enabled:
          type: boolean
          description: Defaults to true on create.
        travel_enabled:
          type: boolean
          description: Defaults to true on create.
        visa_options_enabled:
          type: boolean
          description: Defaults to true on create.
        visa_application_url:
          type: string
          nullable: true
        accommodation_enabled:
          type: boolean
          description: Defaults to true on create.
        roommate_preferences_enabled:
          type: boolean
          description: Defaults to true on create.
        guardian_invites_locked:
          type: boolean
        nfc_badges_enabled:
          type: boolean
        nfc_badge_write_on_checkin_enabled:
          type: boolean
        groups_enabled:
          type: boolean
        hotel_scan_context_id:
          type: string
          format: uuid
          nullable: true
          description: Must be a scan context belonging to this event.
    SeriesEventSummary:
      type: object
      properties:
        id:
          type: string
          format: uuid
        slug:
          type: string
          example: sunbeam-summer-2027
        name:
          type: string
          example: Sunbeam Summer 2027
        series:
          type: object
          properties:
            id:
              type: string
              format: uuid
              nullable: true
            slug:
              type: string
              nullable: true
            name:
              type: string
              nullable: true
        support_email:
          type: string
          format: email
          nullable: true
        timezone:
          type: string
          nullable: true
          description: IANA identifier, e.g. `Europe/Helsinki`.
          example: Europe/Helsinki
        starts_at:
          type: string
          format: date-time
          nullable: true
        ends_at:
          type: string
          format: date-time
          nullable: true
        registration_open_at:
          type: string
          format: date-time
          nullable: true
        registration_close_at:
          type: string
          format: date-time
          nullable: true
        venue_name:
          type: string
          nullable: true
        location_city:
          type: string
          nullable: true
        location_country:
          type: string
          nullable: true
        location_address:
          type: string
          nullable: true
        location_latitude:
          type: number
          format: double
          nullable: true
        location_longitude:
          type: number
          format: double
          nullable: true
        setup_complete:
          type: boolean
          description: >-
            False until someone finishes the setup wizard on the web. An event
            created through this API starts as a draft.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    EventModules:
      type: object
      description: >-
        The event's feature toggles, always returned as real booleans (they are
        stored loosely, so read them from here rather than assuming a shape).
      properties:
        freedom_waivers_enabled:
          type: boolean
        travel_enabled:
          type: boolean
        visa_options_enabled:
          type: boolean
        visa_application_url:
          type: string
          nullable: true
        accommodation_enabled:
          type: boolean
        roommate_preferences_enabled:
          type: boolean
        guardian_invites_locked:
          type: boolean
        nfc_badges_enabled:
          type: boolean
        nfc_badge_write_on_checkin_enabled:
          type: boolean
        groups_enabled:
          type: boolean
  responses:
    Unauthorized:
      description: Missing or invalid credentials.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Event or record not found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        Mobile token, global API token, series API key, or event API key. See
        Authentication above for which endpoints each one reaches.

````