Typestate
Sign in

Event registration and tickets

Guests register for your events with free or paid tickets, and you check them in at the door by their code.

For an organiser of workshops, meetups or a wedding.

Stripe logo StripeResend logo Resend
Start it with your agentStarting from a blueprint does not count against the action changes of your plan.

Start the event registration and tickets blueprint on Typestate

Asked you for your website's address and the address your emails come from.

Started Event registration and tickets: 3 record types, 6 web endpoints.

9 actions are compiling.

Deployed to Sandbox. Try it at your Sandbox address.

What your agent does when you start this blueprint

What runs

Each step has 3 parts: who starts it, how it reaches your backend, and the action that answers.

  1. Youadd an event with its tickets
    Web endpoint, your keyPOST /events
    Add an EventSaves the event and each ticket type with its seats.saves an Eventsaves TicketTypes
  2. A guesttakes a paid seat
    Web endpointPOST /events/register
    RegisterHolds the seat for an hour and opens the payment page.saves a Registration
  3. Stripetells the backend the payment arrived
    Payment eventa payment succeeds
    Record a Paid TicketConfirms the seat and emails the guest their code.changes a Registrationsends 1 email
  4. The backendreminds each guest the day before
    TimerEvery hour at 00 past
    Remind Tomorrow's GuestsEmails each confirmed guest their code.sends emailschanges Registrations
  5. Your teamchecks the guest in at the door
    Web endpoint, your keyPOST /events/check-in
    Check InMarks the code as used, so it works once.changes a Registration
  6. A guestcancels three days before
    Web endpointPOST /events/cancel
    Cancel a RegistrationFrees the seat and refunds the ticket.changes a Registrationrefunds 1 paymentsends 1 email
  • Web endpoint: your website or app calls it
  • Payment event: Stripe tells your backend
  • Timer: it runs at a set time

The rule you will want to change first

A paid ticket cancelled more than 48 hours before the start is refunded in full, and the seat goes back.

It lives in the action Cancel a Registration. Tell your agent how you want it instead: one sentence changes, and you read the change before it goes Live.

What it is made of

The map shows the records it keeps, the actions it runs and the services it calls.

What it keeps

Eventtitle, slug, starts at, place, description, state
TicketTypeevent, name, paid, seats
Registrationevent, ticket type, name, email, starts at, code, state, payment, reminded

What it does

Add an Eventweb endpoint, your key
List Eventsweb endpoint
Registerweb endpoint
Record a Paid Ticketwhen a payment succeeds
Refund a Cancelled Ticket
Cancel a Registrationweb endpointThe rule to change first
Check Inweb endpoint, your key
List Attendeesweb endpoint, your key
Remind Tomorrow's GuestsEvery hour at 00 past

What it reaches

StripeTake one-off payments, deposits and subscriptions on your own Stripe account.
ResendSend email from your own address: sign-in links, receipts, reminders.
Your website6 web endpoints

What it keeps

Event

6 fields

One event, with its start and its place.

title
required, at most 200 characters
slug
required, never twice
starts at
required, a date and time
place
required, at most 300 characters
description
optional, at most 5000 characters
state
required, one of open, closed

Everyone sees the events. Only your key adds and changes them.

TicketType

4 fields

One kind of ticket for an event, free or paid, with its seats.

event
required
name
required, at most 100 characters
paid
required, yes or no
seats
required, a whole number, 1 to 10000

Everyone sees the ticket types. Only your key adds and changes them.

Registration

9 fields

One guest's seat, with the code they show at the door.

event
required
ticket type
required
name
required, at most 200 characters
email
required, an email address
starts at
required, a date and time
code
required, never twice, at most 10 characters
state
required, one of pending, confirmed, cancelled, checked_in
payment
optional
reminded
required, yes or no

A guest registers and cancels through the actions, the payment confirms, and your key checks in. Only your key deletes one.

What it does

Open an action to read its steps: each sentence, with the calls that do it under it.

Add an EventYou add an event with its ticket types: free, paid, and how many seats each has.

Who starts it

From the webPOST /events

What it is given

Titletext, needed

Starts ata date and time, needed

Placetext, needed

Descriptiontext, optional

TicketsList, needed

What it does

  • input.starts_atwhat the caller gives
  • ticketa value it works out
  • Eventa record type
  • Eventan attribute of a record or an answer
  • 10a fixed text or number
  • nowwhat it knows without being told
  • Refusewhere it says no

When the given start is now or earlier, refuse with "An event starts in the future".When fewer than 1 or more than 10 ticket types are given, refuse with "Give between 1 and 10 ticket types".

When input.starts_at is on or before now

Refuse "An event starts in the future"

When number of input.tickets is less than 1

or number of input.tickets is more than 10

Refuse "Give between 1 and 10 ticket types"

Create an Event with the given title, starts_at, place and description, and slug set to the title written for a web address.

Add an Event

set description to input.description

set place to input.place

set slug to the slug of the given title, for a web address

set starts_at to input.starts_at

set title to input.title

For each given ticket type, create a TicketType with event_id set to that Event's id, and name, paid and seats set to the given name, paid and seats.

For each ticket in input.tickets

Add a TicketType

set event_id to event.id

set name to ticket.name

set paid to ticket.paid

set seats to ticket.seats

Answer with that Event's slug.

Answer

slug is event.slug

List EventsYour site shows the coming events, each with its ticket types.

Who starts it

From the webGET /events

What it is given

Nothing. It needs no input.

What it does

  • eventsa value it works out
  • Eventa record type
  • Eventan attribute of a record or an answer
  • "open"a fixed text or number
  • nowwhat it knows without being told

List the Events whose state is open and whose starts_at is after now, earliest first.

events is

Find Events where

starts_at is after now

state is "open"

sorted by starts_at, lowest first

For each Event, list the TicketTypes whose event_id is its id, and answer its title, slug, starts_at, place and description, with the id, name, paid and seats of each TicketType.

rows is an empty list

For each event in events

types is

Find TicketTypes where

event_id is event.id

Adds an object with (description = the event's description, place = the event's place, slug = the event's slug, starts at = the event's starts at, tickets = for each item of the value named types, as type, an object with (name = the type's name, paid = the type's paid, seats = the type's seats, ticket = the type's id), title = the event's title) to rows.

Answer

events is rows

RegisterA guest takes a seat. A free ticket is confirmed at once, a paid one after the payment.

Who starts it

From the webPOST /events/register

What it is given

Tickettext, needed

Nametext, needed

EmailEmail, needed

What it does

  • input.ticketwhat the caller gives
  • typea value it works out
  • TicketTypea record type
  • TicketTypean attribute of a record or an answer
  • "open"a fixed text or number
  • nowwhat it knows without being told
  • fora part of an email or a payment
  • refusewhere it says no

Find the TicketType whose id is the given ticket.When none matches, refuse with "No such ticket".

type is

Find the first TicketType where

id is input.ticket

when none refuse "No such ticket"

Find the Event whose id is that TicketType's event_id.When none matches, or its state is not open, or its starts_at is now or earlier, refuse with "Registration for this event is closed".

event is

Find the first Event where

id is type.event_id

when none refuse "Registration for this event is closed"

When event.state is not "open"

or event.starts_at is on or before now

Refuse "Registration for this event is closed"

The seats taken are the count of Registrations for that TicketType whose state is confirmed or checked_in, plus those whose state is pending and that were made in the last hour.When the seats taken are that TicketType's seats or more, refuse with "These tickets are sold out".

A payment page left open for an hour gives its seat back, so a guest who walks away does not keep a seat.

held is

Count Registrations where

state is one of "confirmed" or "checked_in"

ticket_type_id is type.id

paying is

Count Registrations where

created_at is after now - 1 hour

state is "pending"

ticket_type_id is type.id

When held + paying is at least type.seats

Refuse "These tickets are sold out"

Create a Registration with event_id and ticket_type_id set to that Event's and that TicketType's ids, name and email set to the given name and email, starts_at set to that Event's starts_at, a new code of 10 random letters and digits, and state set to pending when that TicketType is paid, and to confirmed when it is free.

Add a Registration

set code to a random code of 10 letters and digits

set email to input.email

set event_id to event.id

set name to input.name

set starts_at to event.starts_at

set state to "pending" when the type's paid is true, otherwise "confirmed"

set ticket_type_id to type.id

When that TicketType is paid, open a payment page for the Ticket product, with the registration's code as the reference and the given email as the payer's email, and answer with the code and the address of that payment page.

When type.paid

page is

Open a payment page for Ticket

for is input.email

reference is registration.code

Answer

code is registration.code

payment_url is page.url

Otherwise email the given email with the subject "You are registered", saying that Event's title, its start, its place and the registration's code.

Send an email

to input.email

template "Registration Email"

code registration.code

place event.place

starts_at date and time of event.starts_at

title event.title

Answer with the registration's code.

Answer

code is registration.code

Record a Paid TicketA paid ticket is confirmed when its payment arrives, and the guest gets their code.

What it is given

Referencetext, needed

Paymenttext, needed

What it does

  • input.referencewhat the caller gives
  • registrationa value it works out
  • Registrationa record type
  • Registrationan attribute of a record or an answer
  • "confirmed"a fixed text or number
  • toa part of an email or a payment
  • refusewhere it says no

A payment for a ticket succeeded.Find the Registration whose code is the payment's reference.When none matches, refuse with "No registration matches this payment".

registration is

Find the first Registration where

code is input.reference

when none refuse "No registration matches this payment"

Change that Registration's state to confirmed and its payment to the payment's id.

Change the Registration where

id is registration.id

set payment to input.payment

set state to "confirmed"

Find the Event whose id is that Registration's event_id.When none matches, refuse with "No such event".

event is

Find the first Event where

id is registration.event_id

when none refuse "No such event"

Email that Registration's email with the subject "You are registered", saying that Event's title, its start, its place and the registration's code.

Send an email

to registration.email

template "Registration Email"

code registration.code

place event.place

starts_at date and time of event.starts_at

title event.title

Refund a Cancelled TicketThe backend refunds a cancelled ticket on its own, so a guest who never signed in gets the money back.

What it is given

Registrationtext, needed

What it does

  • input.registrationwhat the caller gives
  • registrationa value it works out
  • Registrationa record type
  • Registrationan attribute of a record or an answer
  • "cancelled"a fixed text or number
  • paymenta part of an email or a payment
  • refusewhere it says no

Find the Registration whose id is the given registration.When none matches, refuse with "No such registration".

registration is

Find the first Registration where

id is input.registration

when none refuse "No such registration"

When that Registration's state is not cancelled, or it has no payment, refuse with "Only a cancelled paid ticket is refunded".

When registration.state is not "cancelled"

or registration.payment holds no value

Refuse "Only a cancelled paid ticket is refunded"

Refund that Registration's payment in full, as requested by the guest.

Refund

payment registration.payment

amount all of it

Cancel a RegistrationA guest cancels from their link. A paid ticket is refunded up to 48 hours before the start.

Who starts it

From the webPOST /events/cancel

What it is given

Codetext, needed

EmailEmail, needed

What it does

  • input.codewhat the caller gives
  • registrationa value it works out
  • Registrationa record type
  • Registrationan attribute of a record or an answer
  • "cancelled"a fixed text or number
  • nowwhat it knows without being told
  • registrationa part of an email or a payment
  • refusewhere it says no

Find the Registration whose code is the given code and whose email is the given email.When none matches, refuse with "No registration matches this link".

registration is

Find the first Registration where

code is input.code

email is input.email

when none refuse "No registration matches this link"

When that Registration's state is cancelled or checked_in, refuse with "This registration cannot be cancelled".

When registration.state is one of "cancelled" or "checked_in"

Refuse "This registration cannot be cancelled"

When that Registration's starts_at is now or earlier, refuse with "This event has started, so the registration cannot be cancelled".

When registration.starts_at is on or before now

Refuse "This event has started, so the registration cannot be cancelled"

Change that Registration's state to cancelled.

Change the Registration where

id is registration.id

set state to "cancelled"

When that Registration has a payment and its starts_at is more than 48 hours from now, refund that payment in full in the background, with Refund a Cancelled Ticket.

Up to 48 hours before the start, a freed seat can still go to another guest.

refunded is

registration.payment holds a value

and hours from now to registration.starts_at is more than 48

When refunded

Run Refund a Cancelled Ticket in the background

registration is registration.id

Email that Registration's email with the subject "Your registration is cancelled", saying whether the payment is refunded.

Send an email

to registration.email

template "Cancel a Registration Email"

value "Your payment is being refunded in full." when the value named refunded is true, otherwise "No payment is refunded."

Answer with cancelled as the state, and whether the payment is refunded.

Answer

refunded is refunded

state is "cancelled"

Check InAt the door, you check a guest in by the code in their email.

Who starts it

From the webPOST /events/check-in

What it is given

Codetext, needed

What it does

  • registrationa value it works out
  • Registrationa record type
  • Registrationan attribute of a record or an answer
  • "checked_in"a fixed text or number
  • refusewhere it says no

Find the Registration whose code is the given code in capital letters.When none matches, refuse with "No such ticket code".

registration is

Find the first Registration where

code is the given code in capital letters

when none refuse "No such ticket code"

When that Registration's state is checked_in, refuse with "This ticket was checked in already".When its state is not confirmed, refuse with "This ticket is not confirmed".

When registration.state is "checked_in"

Refuse "This ticket was checked in already"

When registration.state is not "confirmed"

Refuse "This ticket is not confirmed"

Change that Registration's state to checked_in.

Change the Registration where

id is registration.id

set state to "checked_in"

Answer with that Registration's name, and checked_in as the state.

Answer

name is registration.name

state is "checked_in"

List AttendeesYou read who comes to an event, and who checked in.

Who starts it

From the webGET /events/attendees

What it is given

Eventtext, needed

What it does

  • input.eventwhat the caller gives
  • eventa value it works out
  • Eventa record type
  • Eventan attribute of a record or an answer
  • "confirmed"a fixed text or number
  • refusewhere it says no

Find the Event whose slug is the given event.When none matches, refuse with "No such event".

event is

Find the first Event where

slug is input.event

when none refuse "No such event"

List the Registrations whose event_id is that Event's id and whose state is confirmed or checked_in, first registered first.

guests is

Find Registrations where

event_id is event.id

state is one of "confirmed" or "checked_in"

sorted by created_at, lowest first

For each Registration, answer its name, email, code and state, and answer the count of those that are checked_in.

rows is

each guest of guests as

code is guest.code

email is guest.email

name is guest.name

state is guest.state

Answer

checked_in is the number of the items of the value named guests, each as guest, where the guest's state is "checked_in"

guests is rows

Remind Tomorrow's GuestsA day before the event, each confirmed guest gets a reminder with their code.

Who starts it

A timer, every hour at 00 past

What it is given

Nothing. It needs no input.

What it does

  • guestsa value it works out
  • Registrationa record type
  • Registrationan attribute of a record or an answer
  • falsea fixed text or number
  • toa part of an email or a payment

List the Registrations whose state is confirmed, whose reminded is false, and whose starts_at is from now up to 24 hours from now.

guests is

Find Registrations where

reminded is false

starts_at {"after":{"context":"now"},"on_or_before":{"add_hours":[{"context":"now"},24]}}

state is "confirmed"

For each of those Registrations, email its email with the subject "See you tomorrow", saying its starts_at and its code, and change its reminded to true.

For each guest in guests, at most 50

Send an email

to guest.email

template "Remind Tomorrow's Guests Email"

code guest.code

starts_at date and time of guest.starts_at

Change the Registration where

id is guest.id

set reminded to true

What it does by itself

  • Every hour, guests of an event in the next 24 hours get their reminder.

Who signs in and what they may read

People sign up and sign in with an email address and a password, or with a provider you add, such as Google.

  • Event: Everyone sees the events. Only your key adds and changes them.
  • TicketType: Everyone sees the ticket types. Only your key adds and changes them.
  • Registration: A guest registers and cancels through the actions, the payment confirms, and your key checks in. Only your key deletes one.

Money

  • Ticket, EUR 25.00, one time. A paid ticket: EUR 25. Change the price before you sell the first ticket. Paid on a Stripe page, on your own Stripe account.
  • When a payment succeeds, Record a Paid Ticket runs.

Web endpoints

What it doesWho may callAddress
Your site lists the coming events.anyoneGET /events
Your site posts each registration here.anyonePOST /events/register
A guest cancels with their code and email.anyonePOST /events/cancel
Only you, with your key, add an event.you, with your keyPOST /events
Only your team, with your key, checks a guest in.you, with your keyPOST /events/check-in
Only you, with your key, read the guests.you, with your keyGET /events/attendees

What it cannot do yet

  • Every paid ticket costs the price of one product, Ticket. Change the price with update_payment_product before you sell.
  • It does not build the pages. Your site lists the events and posts the form to it.
  • It does not make a printed ticket. The guest shows the code from their email.

Build the pages with your tool

Typestate is the backend. Paste this brief into Lovable, v0, Claude Code or the tool that builds your pages.

Show the brief
Build the pages for "Event registration and tickets". The backend runs on Typestate and answers these web endpoints: GET /events: Your site lists the coming events. Who may call: anyone. POST /events/register: Your site posts each registration here. Who may call: anyone. POST /events/cancel: A guest cancels with their code and email. Who may call: anyone. POST /events: Only you, with your key, add an event. Who may call: you, with your key. POST /events/check-in: Only your team, with your key, checks a guest in. Who may call: you, with your key. GET /events/attendees: Only you, with your key, read the guests. Who may call: you, with your key. People sign up at POST /auth/signup and sign in at POST /auth/login, which answers a token. Send it as Authorization: Bearer <token>. A refused call answers a sentence that can be shown to the person as it is.

Start it

  1. Tell your agent the sentence on the right.
  2. It asks you for the https address of your site, such as https://events.example.com. Guests come back to it after they pay.
  3. It asks you for the address emails come from, on a domain you verified at Resend, such as "City Meetups <hello@meetups.example>".
  4. It tells you what only you can do, such as pasting your Stripe key in the cockpit. A key never goes through the agent.
  5. Open your Sandbox address and try it yourself.

Starting from this blueprint does not count against the action changes of your plan. The Free plan runs it on Sandbox. Taking real money on Live needs a paid plan.

Start the event registration and tickets blueprint on TypestateChoose a plan

Related blueprints