Webinar registration: Typeform → Calendar → HubSpot → Slack
A Typeform signup becomes a Calendar invite, a HubSpot list add, and a Slack headcount digest.
A webinar signup should become a calendar invite and a segmented contact list without manual copying. This workflow fires on the Typeform registration webhook, sends the registrant a Google Calendar invite, upserts and lists them in HubSpot for that event, and keeps the marketing channel posted with a periodic headcount digest. The reminder is a separate scheduled step, not a Calendar setting — Calendar's own reminders only ever reach the organizer's view. Cancellations are a documented manual follow-up, not an automated reversal.
How it flows
- 01
Registrant submits, webhook fires and is verified
form_response arrives with the answers and hidden event fields. The HMAC signature is checked before anything else runs, and the endpoint acks inside Typeform's 30-second window before doing any downstream work.
- 02
Email extracted and validated
Pulled from the email-typed answer or the hidden email field, then checked for well-formed syntax before any Calendar or HubSpot call — a malformed address is skipped and logged rather than allowed to fail the whole request.
- 03
Calendar invite created and sent
events.insert runs with the registrant as an attendee and sendUpdates=all, so the invite actually lands in their inbox. The registrant's own calendar client applies its own default reminder — this pipeline does not and cannot set a custom one on their behalf.
- 04
Contact upserted into HubSpot
Batch upsert keyed on email creates or updates the contact and returns its ID for the next step.
- 05
Contact added to the webinar's registrant list
The resolved contact ID is added to the event's MANUAL list, which is what segments this webinar's registrants apart from every other contact in the portal.
- 06
Cancellation is a manual follow-up, not an automated one
Typeform has no cancellation or response-deletion webhook — only form_response on submission — so there is no first-party signal that tells this pipeline a registrant backed out. Rather than bolt on a second "cancel my registration" form and a reverse path through three more API calls, this is documented as a known limitation: a registrant who wants out is handled by a person removing them from the Calendar event and the HubSpot list by hand.
- 07
Scheduled T-minus reminder
Independent of the submission trigger: at T-24h and T-1h before the webinar, a scheduled job re-reads the HubSpot list and posts an internal readiness/headcount ping to the marketing Slack channel — not a registrant-facing reminder (registrants only get their calendar client's default on the invite).
- 08
Periodic headcount digest
On its own cadence, the current registrant count and the delta since the last post go to the marketing channel.
Set up each app
Work through these in order — later apps usually need a token or an id from an earlier one.
Typeform
Takes the signup and carries the event identity
- 01
Build the registration form with hidden event fields
Create the registration form with a typed email field (or plan to pass one via a hidden field) plus name. If one form template is reused across every webinar, add two URL parameters under Settings → URL parameters — Typeform's current name for what it used to call Hidden Fields — event_id and event_name, and pass both on the link you distribute so every submission already carries which webinar it is for.
- 02
Register the webhook and turn it on
Open the form → Connect tab → search the directory for Webhooks (it is on Typeform's free-tier feature list for the connector itself, but webhook delivery and the Webhooks REST API used to manage endpoints are paid-plan features — this pipeline receives deliveries; it does not call the Responses API) → Add a webhook → paste your endpoint's HTTPS URL → Save, then flip it on — it defaults to off. Set a signing secret in the same panel. Worth saying plainly: Typeform has no native Google Calendar integration at all — its own connector directory lists zero Typeform-side Calendar integrations, only a Zapier bridge — so the invite step below cannot be replaced by a native toggle. Typeform's native HubSpot connector only maps answers onto contact/company/deal properties, never onto a marketing list; and its native Slack connector only posts once per response, which is a different job than the periodic count this pipeline posts. Those two are exactly the legs this workflow does not rebuild.
- 03
Verify the signature before trusting the payload
Every delivery carries an HMAC-SHA256 signature computed over the raw request body with your webhook secret, in a header documented on Typeform's "secure your webhooks" page. Compute the same HMAC on receipt and reject anything that does not match — the endpoint is a public URL and will get probed. Typeform also enforces its own delivery contract: your endpoint must answer inside 30 seconds with a 2xx, or a 404/410 disables the webhook immediately and other failure codes queue retries. Acknowledge fast and do the Calendar/HubSpot work after the ack, not before it.
- 04
Read the registrant out of the payload
The email that becomes both the Calendar attendee and the HubSpot join key is whichever answer has field.type "email" (or hidden.email if you route it as a hidden field instead of an on-form question) — Typeform has no separate contact-identity concept, the answer value is the identity. Validate it as a syntactically well-formed address before calling Calendar or HubSpot: a malformed one 400s the Calendar call and silently drops that one invite if the error is not caught, though it cannot catch a real-looking typo like a misspelled domain — Typeform does not verify deliverability either.
form_response webhook payload (trimmed){ "event_id": "01GXJ8WEBINAR", "event_type": "form_response", "form_response": { "form_id": "abc123", "token": "resp_token_xyz", "submitted_at": "2026-08-09T14:32:00Z", "hidden": { "event_id": "webinar-2026-09-fall-launch", "event_name": "Fall Product Launch" }, "answers": [ { "field": { "id": "email_field", "type": "email" }, "type": "email", "email": "registrant@example.com" }, { "field": { "id": "name_field", "type": "short_text" }, "type": "text", "text": "Jane Doe" } ] } }
Google Calendar
Sends the invite; reminders live here only for the organizer
- 01
Enable the Calendar API and get a refresh token
Google Cloud Console → new or existing project → enable the Google Calendar API → OAuth consent screen → Credentials → OAuth client (or a service account with domain-wide delegation if sending from a shared calendar) → complete the standard OAuth2 flow once to obtain a refresh token. Scope to `https://www.googleapis.com/auth/calendar.events` — the write scope this pipeline needs, not the broader calendar scope.
- 02
Pick the calendar events get created on
Use calendarId=primary, or create a dedicated "Webinars" calendar and note its calendar ID instead — either works, a dedicated calendar just keeps webinar invites out of an individual's personal calendar noise.
- 03
Create the event with the invite actually sent
POST to events.insert with the registrant as an attendee and sendUpdates=all in the query string — sendUpdates=none creates the event silently and defeats the point of "confirmation email," since that query param, not a body field, is what controls whether Google actually emails the invite.
POST calendar/v3/calendars/primary/events?sendUpdates=allcurl -X POST "https://www.googleapis.com/calendar/v3/calendars/primary/events?sendUpdates=all" \ -H "Authorization: Bearer $CALENDAR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "summary": "Webinar: Fall Product Launch", "description": "Join link: https://...", "start": { "dateTime": "2026-09-10T17:00:00-04:00", "timeZone": "America/New_York" }, "end": { "dateTime": "2026-09-10T18:00:00-04:00", "timeZone": "America/New_York" }, "attendees": [ { "email": "registrant@example.com", "displayName": "Jane Doe" } ] }' - 04
Do not promise a per-registrant custom reminder
reminders.overrides on this same request only changes what the organizer's own account sees — Google's reminders concept page documents reminders as private, per-user data, and there is no API path that pushes a custom reminder time onto an attendee's own calendar. What the registrant actually gets automatically is their own calendar client's default reminder behavior on the invite, unconfigurable by the organizer. Do not set reminders.overrides expecting it to reach the registrant — it will not. The T-minus reminder this workflow promises is built as its own scheduled step against Slack, not as a Calendar field; see the flow below.
HubSpot
System of record for who registered, segmented per event
- 01
Create a private app with contact and list scopes
HubSpot → Development → Legacy apps → Create legacy app → Private → Scopes → check crm.objects.contacts.read, crm.objects.contacts.write, crm.lists.read and crm.lists.write → copy the access token. Send as Authorization: Bearer.
- 02
Create a MANUAL list for the webinar ahead of time
HubSpot Lists → Create list → List type: Manual (static). This has to be MANUAL or SNAPSHOT — a DYNAMIC (filter-based) list only ever populates itself from its own filter and cannot be written to through the memberships endpoint below, so a filter-based list silently breaks this step. Name it something like "Webinar: Fall Product Launch Registrants" and note the list ID; repeat per webinar, or read the property-tag alternative below if a reused list fits your reporting better. Keep a small config map keyed by the Typeform hidden event_id: webinar start/end (ISO with timeZone), Calendar summary text, and this list ID — the Calendar insert and the T-minus clock both read start/end from here; the form payload does not carry them. If the name field is a single short_text, split on the first space into firstname/lastname for the upsert (or send the whole string as firstname).
- 03
Upsert the contact by email
Resolve the registrant to a HubSpot contact — creating one if none exists — with a single upsert call keyed on email, so no separate lookup call is needed before the list-add step.
POST crm/v3/objects/contacts/batch/upsertcurl -X POST "https://api.hubapi.com/crm/v3/objects/contacts/batch/upsert" \ -H "Authorization: Bearer $HUBSPOT_PRIVATE_APP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "inputs": [ { "id": "registrant@example.com", "idProperty": "email", "properties": { "firstname": "Jane", "lastname": "Doe" } } ] }' - 04
Add the resolved contact to the webinar list
Take the contact ID the upsert call returned and add it to the list created above. A lighter-weight alternative that skips list creation entirely is a custom contact property (e.g. a multi-checkbox of webinars registered for) set in the same upsert call's properties object — cheaper, but it gives up HubSpot's native list-based reporting and workflow tooling, which is why list membership is the primary path here.
PUT crm/v3/lists/{listId}/memberships/addcurl -X PUT "https://api.hubapi.com/crm/v3/lists/$LIST_ID/memberships/add" \ -H "Authorization: Bearer $HUBSPOT_PRIVATE_APP_TOKEN" \ -H "Content-Type: application/json" \ -d '["<contact_id_from_upsert_response>"]'
Slack
Sends the T-minus reminder and the periodic headcount digest
- 01
Create the app and give it a posting scope
api.slack.com/apps → Create New App → From scratch → OAuth & Permissions → Bot Token Scopes → chat:write → Install to Workspace → copy the Bot User OAuth Token. Invite the bot to the marketing channel with /invite @app-name.
- 02
Post the T-minus reminder and the digest
Calendar still cannot push a custom reminder onto the registrant's own calendar — what they get is only their client default after the invite. Do not frame a Slack post as a substitute for that missing attendee reminder. The T-minus job in this pipeline is an internal marketing-channel ping: at T-24h and T-1h before the webinar starts (using the same event_id→start lookup as the invite step), re-read the HubSpot list and post readiness/headcount to the marketing channel — independent of Calendar reminders. For the ongoing headcount digest, either call chat.postMessage on your own cadence (e.g. daily) with the current list size and the delta since the last post, or use chat.scheduleMessage with a fixed post_at if the cadence is a fixed day/time rather than triggered by your own scheduler.
chat.postMessagecurl -X POST https://slack.com/api/chat.postMessage \ -H "Authorization: Bearer $SLACK_BOT_TOKEN" \ -H "Content-Type: application/json; charset=utf-8" \ -d '{"channel":"C0MARKETING","text":"Fall Product Launch: 214 registered (+18 since Monday)."}'