> ## Documentation Index
> Fetch the complete documentation index at: https://attend.hackclub.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Update a participant

> Edits the person and/or where their registration stands. Only the keys
you send are written.

`status` is the API form of the withdraw and reinstate buttons:
`withdrawn` withdraws them, `in_progress` reinstates them — after which
the displayed status is recomputed from their onboarding progress.

Date of birth, phone number and address are writable by every caller
that gets this far, including API keys and PII-restricted roles, which
cannot read them back: the response is redacted exactly as a read
would be.

To backfill details without overwriting anything the participant
entered themselves, send `fill_blanks: true`. Only fields that are
empty (or legal names still on the "Unknown" placeholder) are written,
and the response lists the fields that were left alone in
`skipped_fields`. The address counts as one value: if any part of one
is on file, every address field sent is skipped, so a backfill never
mixes its address into the participant's. Find the registration id for
an email with the lookup endpoint.




## OpenAPI

````yaml /openapi.yml patch /events/{event_id}/participants/{id}
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: `bans:write` and
      `support:read`.
    - **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`, `update` and `delete`, event setup, event staff, 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 |

    | `PATCH`/`DELETE` a participant | ✅ any event in the series | ✅ its own
    event |

    | `/events/{id}/setup/…` and `/events/{id}/staff` | ✅ 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`
    |

    | `/events/{id}/messages/…` and `/events/{id}/incidents/…` | ❌ `403` | ❌
    `403` |

    | `GET /incident_reports/options` and `POST /incident_reports` | ❌ `403` | ❌
    `403` |

    | `GET /messages` and `GET /messages/{id}` (inbox) | ❌ `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, setup, and staff endpoints; other endpoints accept the
      numeric id only.
    - A caller that may *write* a field cannot always *read* it back: dates of
      birth, phone numbers, addresses and the details of the people around a
      participant are omitted (not nulled) for API keys and PII-restricted
      roles, in responses to writes just as in responses to reads.

    > **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: Event Setup
    description: >-
      The tail of the setup wizard — waiver configuration and taking an event
      out of draft — for events created through the API.
  - name: Event Staff
    description: Who has access to an event, and what they can do there.
  - 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: Stays
    description: >-
      An event's stays — during the event, the night before, the night after —
      and the roster of who's staying for each list-only stay. Built for a shop
      that sells the night: add buyers to the roster on purchase, remove them on
      refund. Accepts an event or series API key, or a global token scoped to
      `rooming:write`.
  - name: Travel Calendar
    description: Chronological participant travel and pickup operations.
  - name: Slack Blasts
    description: Broadcast Slack messages to an event's participants.
  - name: Messages
    description: >-
      Compose, preview, schedule and send an organizer broadcast over Slack,
      email, SMS and push, and follow its deliveries.
  - name: Incidents
    description: >-
      An event's incident log — logging, sharing, commenting on and escalating
      incidents to Hack Club HQ.
  - name: NFC Badges
    description: Provision and manage participant NFC badges.
  - name: Push Tokens
    description: Register device push-notification tokens.
  - name: Inbox
    description: The organizer messages delivered to the signed-in participant.
  - name: Incident Reports
    description: File a confidential incident report straight to Hack Club HQ.
  - 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:
  /events/{event_id}/participants/{id}:
    patch:
      tags:
        - Participants
      summary: Update a participant
      description: |
        Edits the person and/or where their registration stands. Only the keys
        you send are written.

        `status` is the API form of the withdraw and reinstate buttons:
        `withdrawn` withdraws them, `in_progress` reinstates them — after which
        the displayed status is recomputed from their onboarding progress.

        Date of birth, phone number and address are writable by every caller
        that gets this far, including API keys and PII-restricted roles, which
        cannot read them back: the response is redacted exactly as a read
        would be.

        To backfill details without overwriting anything the participant
        entered themselves, send `fill_blanks: true`. Only fields that are
        empty (or legal names still on the "Unknown" placeholder) are written,
        and the response lists the fields that were left alone in
        `skipped_fields`. The address counts as one value: if any part of one
        is on file, every address field sent is skipped, so a backfill never
        mixes its address into the participant's. Find the registration id for
        an email with the lookup endpoint.
      parameters:
        - $ref: '#/components/parameters/EventId'
        - $ref: '#/components/parameters/RegistrationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                status:
                  type: string
                  enum:
                    - invited
                    - in_progress
                    - awaiting_guardian
                    - complete
                    - withdrawn
                    - rejected
                fill_blanks:
                  type: boolean
                  default: false
                  description: Only write participant fields that have no value yet.
                participant:
                  type: object
                  properties:
                    legal_first_name:
                      type: string
                    legal_last_name:
                      type: string
                    preferred_name:
                      type: string
                    email:
                      type: string
                      format: email
                    date_of_birth:
                      type: string
                      format: date
                      description: Can't be in the future.
                    phone:
                      type: string
                      description: E.164 format, e.g. +12025550100
                    pronouns:
                      type: string
                    tshirt_size:
                      type: string
                    country_of_residence:
                      type: string
                      description: >-
                        ISO 3166-1 alpha-2 code (e.g. `US`) or the country's
                        English name.
                    address_line_1:
                      type: string
                    address_line_2:
                      type: string
                    city:
                      type: string
                    state:
                      type: string
                    postal_code:
                      type: string
      responses:
        '200':
          description: The updated participant.
          content:
            application/json:
              schema:
                type: object
                properties:
                  participant:
                    $ref: '#/components/schemas/Participant'
                  skipped_fields:
                    type: array
                    items:
                      type: string
                    description: >-
                      Only with `fill_blanks`: the fields sent that already had
                      a value, so were not written.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: >-
            Nothing to update, an unknown status, or the model rejected the
            change (nothing is written in that case).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  parameters:
    EventId:
      name: event_id
      in: path
      required: true
      schema:
        type: string
      description: Event id or slug.
    RegistrationId:
      name: id
      in: path
      required: true
      schema:
        type: integer
      description: The participant_event id — one person's registration for this event.
  schemas:
    Participant:
      type: object
      description: >-
        A participant registration. Sensitive fields (allergies, medical,
        dietary, safeguarding, emergency and guardian contacts) are only present
        for global admins and safeguarding leads.
      properties:
        participant_id:
          type: integer
        participant_event_id:
          type: integer
        display_name:
          type: string
          example: Jane
        full_name:
          type: string
          example: Jane Doe
        email:
          type: string
          format: email
        slack_user_id:
          type: string
          nullable: true
        phone:
          type: string
          nullable: true
        pronouns:
          type: string
          nullable: true
        headshot_url:
          type: string
          nullable: true
        status:
          $ref: '#/components/schemas/ParticipantStatus'
        checked_in_at:
          type: string
          format: date-time
          nullable: true
        nfc_badge_token:
          type: string
          nullable: true
        nfc_badge_assigned:
          type: boolean
        has_anaphylaxis_risk:
          type: boolean
        requires_refrigeration:
          type: boolean
        cross_contamination_risk:
          type: boolean
        high_support_flag:
          type: boolean
        can_leave_unaccompanied:
          type: boolean
        waiver_signed:
          type: boolean
        updated_at:
          type: string
          format: date-time
        scans_by_context:
          type: array
          items:
            $ref: '#/components/schemas/ScansByContext'
        travel_inbound:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/Travel'
        travel_outbound:
          nullable: true
          allOf:
            - $ref: '#/components/schemas/Travel'
        allergies:
          type: string
          nullable: true
          description: Sensitive — permitted callers only.
        medical_conditions:
          type: string
          nullable: true
          description: Sensitive.
        medications:
          type: string
          nullable: true
          description: Sensitive.
        diet_type:
          type: string
          nullable: true
          description: Sensitive.
        life_threatening_allergies:
          type: string
          nullable: true
          description: Sensitive.
        freedom_waiver_granted:
          type: boolean
          description: Sensitive.
        emergency_contacts:
          type: array
          description: Sensitive — permitted callers only.
          items:
            type: object
            properties:
              name:
                type: string
              phone:
                type: string
              relationship:
                type: string
              priority:
                type: integer
        parent_guardian_name:
          type: string
          description: Sensitive; present only if a primary guardian exists.
        parent_guardian_phone:
          type: string
          description: Sensitive.
        parent_guardian_email:
          type: string
          description: Sensitive.
    Error:
      type: object
      properties:
        error:
          type: string
          example: Unauthorized
    ParticipantStatus:
      type: string
      enum:
        - invited
        - in_progress
        - awaiting_guardian
        - complete
        - withdrawn
        - rejected
    ScansByContext:
      type: object
      properties:
        scan_context_id:
          type: integer
        scan_context_name:
          type: string
        checks_in:
          type: boolean
        is_travel_pickup:
          type: boolean
        is_airport:
          type: boolean
          deprecated: true
          description: Deprecated alias for `is_travel_pickup`.
        scan_count:
          type: integer
        first_scanned_at:
          type: string
          format: date-time
          nullable: true
        last_scanned_at:
          type: string
          format: date-time
          nullable: true
    Travel:
      type: object
      description: A participant's inbound or outbound travel record.
      properties:
        id:
          type: integer
        direction:
          type: string
          example: inbound
        mode:
          type: string
          example: plane
        visa_required:
          type: boolean
          nullable: true
        visa_status:
          type: string
          nullable: true
        visa_type:
          type: string
          nullable: true
        visa_number:
          type: string
          nullable: true
        passport_nationality:
          type: string
          nullable: true
        is_unaccompanied_minor:
          type: boolean
        carrier:
          type: string
          nullable: true
        flight_number:
          type: string
          nullable: true
        train_departure_station:
          type: string
          nullable: true
        train_arrival_station:
          type: string
          nullable: true
        departure_station:
          type: string
          nullable: true
        arrival_station:
          type: string
          nullable: true
        departure_city:
          type: string
          nullable: true
        arrival_city:
          type: string
          nullable: true
        departure_time:
          type: string
          format: date-time
          nullable: true
        arrival_time:
          type: string
          format: date-time
          nullable: true
        expected_arrival_time:
          type: string
          format: date-time
          nullable: true
        bus_departure_location:
          type: string
          nullable: true
        bus_arrival_location:
          type: string
          nullable: true
        origin_address:
          type: string
          nullable: true
        other_details:
          type: string
          nullable: true
        notes:
          type: string
          nullable: true
        pickup_dismissed_at:
          type: string
          format: date-time
          nullable: true
        legs:
          type: array
          items:
            $ref: '#/components/schemas/TravelLeg'
    TravelLeg:
      type: object
      properties:
        id:
          type: integer
        position:
          type: integer
        flight_code:
          type: string
          nullable: true
        departure_airport:
          type: string
          nullable: true
        arrival_airport:
          type: string
          nullable: true
        departure_time:
          type: string
          format: date-time
          nullable: true
        arrival_time:
          type: string
          format: date-time
          nullable: true
        live_status:
          type: string
          nullable: true
        live_departure_time:
          type: string
          format: date-time
          nullable: true
        live_arrival_time:
          type: string
          format: date-time
          nullable: true
        travel_picked_up_at:
          type: string
          format: date-time
          nullable: true
        airport_picked_up_at:
          type: string
          format: date-time
          nullable: true
          deprecated: true
          description: Deprecated alias for `travel_picked_up_at`.
  responses:
    Unauthorized:
      description: Missing or invalid credentials.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: The caller cannot access this event or action.
      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.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.