Guides & Tutorials11 min read

Zoom Webinar to CRM Workflow Automation with OpenClaw: A Step-by-Step Guide

By ButterGrow Team

TL;DR

This tutorial shows how to connect Zoom Webinar events to your CRM using OpenClaw so that registrations create or update contacts, reminders go out on time, and no show follow ups happen automatically. The goal is a dependable workflow automation that runs without manual intervention. You will configure a Zoom webhook, map fields, upsert records, and add retries plus a dead letter queue for reliability. By the end, you will ship a production ready flow that a marketer can own without engineering tickets.

What you will build

You will build an end to end automation that:

  • Creates or updates a CRM contact when a webinar registration is submitted.
  • Schedules reminder messages with a join link 24 hours and 30 minutes before the live session.
  • Writes attendance outcomes and triggers no show follow up.
  • Adds basic observability, idempotency, and a dead letter queue for safe operations.

If you are new to ButterGrow or OpenClaw, skim the AI marketing automation features to understand what ButterGrow does before you start configuring connectors. See the AI marketing automation features at the feature set overview in what ButterGrow does at the feature set.

Prerequisites

  • A Zoom account with Webinars enabled and permission to create App Marketplace Event Subscriptions.
  • A CRM account. The examples use HubSpot, but any CRM with an upsert API works.
  • An OpenClaw workspace with access to playbooks and secrets.
  • API keys for your CRM stored in your OpenClaw secrets vault.

Architecture overview

At a high level, the flow looks like this:

  1. Zoom sends a webhook for a registration, attendee join, or attendee leave event to your OpenClaw ingress endpoint.
  2. The playbook validates the signature, normalizes payload fields, and computes a dedupe key using email plus the webinar id.
  3. The playbook upserts a CRM contact and logs a webinar activity record linked to the contact.
  4. Schedulers create two reminder jobs. A follow up job runs after the event to handle attended vs no show branches.
  5. Retries, idempotency keys, and a dead letter queue protect the flow from transient failures and duplicate deliveries.

Step by step

Step 1Prepare Zoom and CRM

Create a Zoom Server to Server app and add Event Subscriptions for webinar.registration_created, webinar.participant_joined, and webinar.participant_left. Copy the secret used to sign webhook requests. In your CRM, decide whether you will model a custom Activity or Engagement to record registrations and attendance. Add custom fields as needed for webinar id, webinar topic, and join URL so that reporting and follow ups are easy.

To see how OpenClaw structures reusable automation, read our overview of reusable OpenClaw playbooks and versioning, which explains how to ship templates and evolve them safely. See reusable OpenClaw playbooks and versioning.

Step 2Create the OpenClaw playbook

Define a playbook that exposes a public ingress endpoint, validates the Zoom signature, maps fields, and calls your CRM. The following example shows a compact but production friendly structure.

# file: playbooks/zoom_webinar_crm.yaml
apiVersion: openclaw/v1
kind: Playbook
metadata:
  name: zoom-webinar-to-crm
  description: Upsert CRM contacts and schedule reminders from Zoom Webinar events
spec:
  ingress:
    path: /ingress/zoom-webinar
    method: POST
    auth: none
  secrets:
    - name: ZOOM_WEBHOOK_SECRET
    - name: CRM_API_KEY
  steps:
    - id: verify_signature
      uses: std/http/verify-hmac
      with:
        header: x-zm-signature
        secret: ${secrets.ZOOM_WEBHOOK_SECRET}
        algorithm: sha256
    - id: parse_event
      uses: std/json/parse
      with:
        from: ${request.body}
    - id: route
      uses: std/control/switch
      with:
        on: ${steps.parse_event.data.event}
        cases:
          webinar.registration_created: registrant_flow
          webinar.participant_joined: attendance_join
          webinar.participant_left: attendance_leave
  flows:
    registrant_flow:
      - id: normalize_registrant
        uses: std/object/project
        with:
          input: ${steps.parse_event.data.payload}
          pick:
            email: registrant.email
            first_name: registrant.first_name
            last_name: registrant.last_name
            webinar_id: object.id
            webinar_topic: object.topic
            start_time: object.start_time
            join_url: registrant.join_url
      - id: compute_keys
        uses: std/object/extend
        with:
          input: ${steps.normalize_registrant.output}
          add:
            dedupe_key: ${hash.sha1(${input.email}+"|"+${input.webinar_id})}
            start_time_rfc3339: ${time.to_rfc3339(${input.start_time})}
      - id: upsert_contact
        uses: connectors/hubspot/upsert-contact
        with:
          api_key: ${secrets.CRM_API_KEY}
          email: ${steps.compute_keys.output.email}
          properties:
            firstname: ${steps.compute_keys.output.first_name}
            lastname: ${steps.compute_keys.output.last_name}
            zoom_webinar_id: ${steps.compute_keys.output.webinar_id}
            zoom_webinar_topic: ${steps.compute_keys.output.webinar_topic}
            zoom_join_url: ${steps.compute_keys.output.join_url}
            zoom_dedupe_key: ${steps.compute_keys.output.dedupe_key}
      - id: log_registration
        uses: connectors/hubspot/create-engagement
        with:
          api_key: ${secrets.CRM_API_KEY}
          type: NOTE
          subject: Webinar Registration
          body: ${"Registered for "+steps.compute_keys.output.webinar_topic}
          associations:
            contactId: ${steps.upsert_contact.output.id}
      - id: schedule_24h
        uses: std/schedule/at
        with:
          run_at: ${time.minus(${steps.compute_keys.output.start_time_rfc3339}, "24h")}
          job: send_reminder
          payload: ${steps.compute_keys.output}
      - id: schedule_30m
        uses: std/schedule/at
        with:
          run_at: ${time.minus(${steps.compute_keys.output.start_time_rfc3339}, "30m")}
          job: send_reminder
          payload: ${steps.compute_keys.output}
    attendance_join:
      - id: mark_attended
        uses: connectors/hubspot/create-engagement
        with:
          api_key: ${secrets.CRM_API_KEY}
          type: NOTE
          subject: Webinar Join
          body: ${"Joined webinar "+steps.parse_event.data.payload.object.topic}
    attendance_leave:
      - id: mark_left
        uses: connectors/hubspot/create-engagement
        with:
          api_key: ${secrets.CRM_API_KEY}
          type: NOTE
          subject: Webinar Leave
          body: ${"Left webinar "+steps.parse_event.data.payload.object.topic}
  jobs:
    send_reminder:
      - id: render_email
        uses: std/template/render
        with:
          template: |
            Hi {{first_name}},
            Your webinar "{{webinar_topic}}" starts soon.
            Join here: {{join_url}}
          data: ${job.payload}
      - id: send_email
        uses: connectors/email/send
        with:
          to: ${job.payload.email}
          subject: Reminder for ${job.payload.webinar_topic}
          body: ${steps.render_email.output}
  reliability:
    retries:
      max_attempts: 5
      backoff: exponential
    idempotency:
      key: ${steps.compute_keys.output.dedupe_key}
    dlq:
      topic: zoom-webinar-dlq

This pattern keeps ingress light and isolates side effects in clearly named steps. It also gives you one place to add retry and idempotency behavior that spans all cases.

Step 3Subscribe to Zoom webhooks

In the Zoom App Marketplace, add an Event Subscription that points to your public ingress URL. Select the events you need for registration, joins, and leaves. Save the verification token or secret and paste it into your OpenClaw secret named ZOOM_WEBHOOK_SECRET. Here is a realistic example payload you can use in development.

{
  "event": "webinar.registration_created",
  "event_ts": 1732819200000,
  "payload": {
    "account_id": "abc123",
    "object": {
      "id": 9876543210,
      "topic": "Product Launch Q3",
      "start_time": "2026-08-15T17:00:00Z"
    },
    "registrant": {
      "email": "alex@example.com",
      "first_name": "Alex",
      "last_name": "Rivera",
      "join_url": "https://zoom.us/w/9876543210?tk=..."
    }
  }
}

During local development, replay events with curl to hit your ingress endpoint. Replace the signature with a valid HMAC if your verify step is strict in non production environments.

curl -X POST \
  -H "Content-Type: application/json" \
  -H "x-zm-signature: dev-test-skip" \
  --data @sample_registration.json \
  https://your-runner.example.com/ingress/zoom-webinar

Step 4Map fields and upsert to the CRM

Create a clear mapping between the Zoom payload and your CRM properties. This lets marketing and sales agree on what data is captured and where it lands.

Zoom source CRM target Notes
registrant.email email Primary identifier used for upsert
registrant.first_name firstname Ensure proper casing
registrant.last_name lastname Ensure proper casing
object.id zoom_webinar_id Used in dedupe key and reports
object.topic zoom_webinar_topic Useful for personalization
registrant.join_url zoom_join_url Used in reminders
object.start_time webinar_start_time Normalize to RFC 3339

If you maintain mappings in code, keep them in one place. This simple JavaScript snippet shows the idea you can adapt inside a task that transforms payloads.

export function mapRegistrant(payload) {
  const p = payload.payload;
  return {
    email: p.registrant.email.trim().toLowerCase(),
    firstname: p.registrant.first_name?.trim() || "",
    lastname: p.registrant.last_name?.trim() || "",
    zoom_webinar_id: String(p.object.id),
    zoom_webinar_topic: p.object.topic,
    zoom_join_url: p.registrant.join_url,
    webinar_start_time: new Date(p.object.start_time).toISOString()
  };
}

Step 5Schedule reminders that respect time zones

Use the scheduler to create two reminders. Convert the event start time into the registrant's local time zone when you render the email so the copy is human friendly. If a time zone is missing, fall back to the webinar time and include the zone abbreviation in the subject.

jobs:
  send_reminder:
    - id: render
      uses: std/template/render
      with:
        template: |
          Hi {{first_name}}, your session starts at {{format_time(webinar_start_time, tz)}}.
          Join here: {{join_url}}
        data:
          first_name: ${job.payload.first_name}
          webinar_start_time: ${job.payload.start_time_rfc3339}
          tz: ${lookup.timezone(${job.payload.email})}
    - id: deliver
      uses: connectors/email/send
      with:
        to: ${job.payload.email}
        subject: Reminder for ${job.payload.webinar_topic}
        body: ${steps.render.output}

Step 6Record attendance and run no show follow ups

When a participant joins, record an activity linked to the contact. After the webinar end time plus a short buffer, compute attended vs no show based on whether a join was seen. Send a light summary to those who attended and a recording plus next steps to those who missed it.

flows:
  post_event:
    - id: read_attendance
      uses: state/get
      with:
        key: ${steps.compute_keys.output.dedupe_key}
    - id: branch
      uses: std/control/if
      with:
        condition: ${steps.read_attendance.output.join_seen == true}
        then: attended
        else: no_show

Step 7Add observability, idempotency, retries, and a DLQ

Production flows deserve guardrails. Instrument steps with structured logs. Use a stable idempotency key so that duplicate webhook deliveries do not create extra records. Add exponential backoff on all network calls. Route messages that still fail into a dead letter topic so you can inspect and replay. If these practices are new to you, study our primer on idempotency, retries and DLQs in OpenClaw for implementation patterns you can copy. See the idempotency, retries and DLQs guide at idempotency, retries and DLQs guide.

Step 8Test the end to end path

Run the playbook in a staging workspace. Register for a test webinar with multiple emails including a role account and a personal account. Confirm that contacts are created or updated, reminders arrive on schedule, and attendance is recorded. For quick feedback loops during build out, replay saved JSON payloads so you do not wait on live events.

curl -sSL https://raw.githubusercontent.com/your-org/fixtures/main/zoom/registration.json \
  | curl -X POST -H "Content-Type: application/json" --data-binary @- \
    https://staging.example.com/ingress/zoom-webinar

Step 9Ship with confidence

Before you point production webhooks at your ingress, enable audit logs and verify that all secrets are pulled from the vault. Take note of rate limits on your CRM connector and set budgets so spikes do not burn through quotas. Share the operational runbook with the marketer who owns the webinar program so they can see where to look when something behaves unexpectedly.

Common pitfalls and quick fixes

  • Reminders arrive at the wrong local time. Normalize the timestamp and render it with the registrant time zone. Keep a fallback path for unknown zones.
  • Duplicate contacts appear after a re registration. Use email plus webinar id as the dedupe key and make upserts idempotent across retries.
  • Zoom calls the endpoint with a stale signature. Reject based on timestamp to prevent replay and alert the on call channel.
  • The CRM API returns a 429 rate limit. Apply exponential backoff and use a priority queue if you have other jobs that must not starve. If you want strategy examples, browse more from the ButterGrow blog at more from the ButterGrow blog.

Variations and extensions

  • Send SMS reminders to mobile numbers captured on the registration form if consent is present in your CRM.
  • Add campaign parameters to the join link so you can attribute engagement by source. You can generate UTM parameters centrally and append them during template rendering.
  • Push attendance outcomes to your advertising audiences for lookalike models on highly engaged registrants.
  • Pipe a recording link and a short summary into your knowledge base so sales can reference it in follow ups.

If you want a quick orientation on what the product covers out of the box, the best starting point is answers to common questions at the FAQ.

ButterGrow runs OpenClaw for you and ships a library of connectors and templates so teams can ship this integration in hours, not weeks. If you want this running in production with guardrails and monitoring, you can get started in minutes by following the onboarding flow.

References

Frequently Asked Questions

How do I test Zoom webhook events for webinars without a public URL?+

Use a tunneling tool like ngrok to expose your local OpenClaw runner and point the Zoom Event Subscription to the tunnel URL. For repeatable tests, save real payloads from staging and replay them with curl to your playbook's ingress endpoint. Keep a small catalog of sample events so you can regression test changes quickly.

Which CRM objects should I upsert for webinar registrations and attendance?+

Create or update a Contact based on email, and optionally create a custom Activity or Engagement record labeled Webinar Registration or Webinar Attended. Link the activity to the Contact and include the webinar topic, scheduled time, and join URL. For HubSpot, use Contacts plus Engagements. For Salesforce, use Lead or Contact plus a custom Task or Event.

How do I prevent duplicate registrations from creating duplicate contacts?+

Hash a stable dedupe key like email plus webinar ID and store it in your CRM as a hidden property. In OpenClaw, make the upsert idempotent by reading the existing record first and updating only when fields change. Add a retry policy with exponential backoff and route exhausted attempts to a DLQ for inspection.

How can I secure the Zoom webhook so only Zoom can call my endpoint?+

Validate the Zoom signature header and timestamp before processing. Reject events that fail HMAC verification or that are older than a short window to avoid replay. Keep the shared secret in your secrets manager and never log it.

Can I personalize reminders by time zone and custom questions captured on the Zoom form?+

Yes. Normalize the registrant's time zone when available and render the time using that zone. Map Zoom custom question responses into CRM fields, then merge them into your email template. Always keep a default path for missing or invalid time zones to avoid failed sends.

What changes if I use Salesforce instead of HubSpot for this workflow?+

The data model and API endpoints change, but the playbook stays the same shape. Swap the CRM connector to Salesforce and map fields to Lead or Contact plus a Task or Campaign Member. Keep the same dedupe strategy and webhook validation.

Ready to try ButterGrow?

See how ButterGrow can supercharge your growth with a quick demo.

Book a Demo