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
/events/:eventId/participantsJWT/events/:eventId/participantsJWT/events/participants/:participantIdJWT/events/participants/:participantId/confirmJWT/events/participants/:participantId/cancelJWT/events/participants/:participantIdJWT/client/events/:eventId/participantsNo auth/client/events/participation/mineJWT/client/events/participation/:participantId/respondJWTParticipant 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 unlesssilent: trueparticipant-confirmed— fires when status flips toconfirmedparticipant-cancelled— fires on staff cancelparticipant-reminder-week/-day— scheduled by the event scheduler ifsettings.sendEventRemindersis true on the event
Late registrations
Two ways to add someone close to the event:
- Late participant:
POST /events/:eventId/participantswithstatus: 'confirmed'skips the invite cycle and records them directly. - 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.