Documentation

Participants

The participant roster — speakers, sponsors, organisers, attendees — and RSVP flow.

A participant is anyone associated with the event in a role beyond "they bought a ticket": speakers, sponsors, organisers, exhibitors, panelists, volunteers. Participants are tracked separately from tickets — a person can be both a speaker (participant) and a ticket holder (ticket), and the two records cross-reference by email.

Endpoints

POST/events/:eventId/participantsJWT
GET/events/:eventId/participantsJWT
PUT/events/participants/:participantIdJWT
POST/events/participants/:participantId/confirmJWT
POST/events/participants/:participantId/cancelJWT
DELETE/events/participants/:participantIdJWT
GET/client/events/:eventId/participantsNo auth
GET/client/events/participation/mineJWT
PUT/client/events/participation/:participantId/respondJWT

Participant shape

type Participant = {
  pk: string;
  sk: string;
  datatype: 'event_participant';
  data: {
    event: string;
    email: string;
    name: string;
    type: 'speaker' | 'sponsor' | 'organizer' | 'exhibitor' | 'panelist' | 'volunteer' | 'attendee';
    role?: string;            // freeform, e.g. 'keynote', 'gold sponsor'
    status: 'invited' | 'confirmed' | 'declined' | 'cancelled' | 'tentative';
    bio?: string;
    image?: string;
    company?: string;
    jobTitle?: string;
    social?: { twitter?: string; linkedin?: string; website?: string };
    sessions?: string[];      // sessionIds this participant is part of
    featured?: boolean;       // surface prominently in the participant list
    sponsorTier?: string;     // for type: 'sponsor'
    inviteSentAt?: string;
    respondedAt?: string;
  };
};

Adding participants

await fetch(`/events/${eventId}/participants`, {
  method: 'POST',
  headers: {
    orgid: 'my-org',
    Authorization: `Bearer ${jwt}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    email: '[email protected]',
    name: 'Alice Lee',
    type: 'speaker',
    role: 'keynote',
    company: 'Acme Type Co',
    bio: '...',
    sessions: ['session-id-1'],
    featured: true,
  }),
});

If inviteSentAt is left unset, the service sends the participant-invited notification on create — turn this off by passing silent: true.

Confirming and cancelling

Staff workflow:

// Confirm a tentative invitation
await fetch(`/events/participants/${participantId}/confirm`, {
  method: 'POST',
  headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}` },
});

// Cancel
await fetch(`/events/participants/${participantId}/cancel`, {
  method: 'POST',
  headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}` },
});

Customer-side RSVP flow lives at PUT /client/events/participation/:participantId/respond:

await fetch(`/client/events/participation/${participantId}/respond`, {
  method: 'PUT',
  headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ response: 'accepted' }), // 'accepted' | 'declined' | 'tentative'
});

The endpoint matches the participant by id and verifies the caller's email matches participant.data.email before applying the response.

Listing the roster

Public:

const { data } = await fetch(
  `/client/events/${eventId}/participants?type=speaker&featured=true`,
  { headers: { orgid: 'my-org' } }
).then(r => r.json());

Staff (full list with all statuses):

const all = await fetch(
  `/events/${eventId}/participants?type=sponsor&status=confirmed&limit=100`,
  { headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}` } }
).then(r => r.json());

The customer endpoint hides unconfirmed/cancelled participants and only returns the public-safe projection (no internal notes, no inviteSentAt).

Communications

Participants share email infrastructure with the rest of AppEngine. To send a custom message — say, speaker prep instructions — use the Broadcast module, segmented by querying event_participant for this event:

// 1. Build an audience: all confirmed speakers for this event
// 2. Trigger a broadcast campaign with a template that loads participant context
//    See /docs/appengine/broadcast/overview

The default participant lifecycle templates are:

  • participant-invited — sent on create unless silent: true
  • participant-confirmed — fires when status flips to confirmed
  • participant-cancelled — fires on staff cancel
  • participant-reminder-week / -day — scheduled by the event scheduler if settings.sendEventReminders is true on the event

Late registrations

Two ways to add someone close to the event:

  1. Late participant: POST /events/:eventId/participants with status: 'confirmed' skips the invite cycle and records them directly.
  2. Late ticket purchase: walk-up via pre-generated tickets is the right path for paid attendees.

My participation

A signed-in customer can fetch their own participation roster across events:

const mine = await fetch('/client/events/participation/mine', {
  headers: { orgid: 'my-org', Authorization: `Bearer ${jwt}` },
}).then(r => r.json());

// [{ event: {...}, participantId, type, role, status, sessions: [...] }, ...]

This powers the "My events" tab on a customer profile.

Participants and ticket holders are independent records — invite a sponsor as a participant and they don't get a ticket. If a sponsor needs a ticket too, comp them via POST /events/tickets/comp after creating the participant record.