1. Features
  2. Meetings
Meetings

Zoom and Webex meetings that follow the event.

Name a service when you create a calendar event and workwae creates the meeting, writes its join link into the event, moves it when the event moves and deletes it when the event is cancelled. Hosted by the user's own grant or by your organization's server app.

weekly-zoom.ts
const timeZone = "America/Chicago";

// A weekly event gets a recurring Zoom meeting
const event = await workwae.calendarEvents.create({
  accountId,
  calendarId,
  subject: "Customer onboarding",
  start: { dateTime: "2026-11-03T15:00:00", timeZone },
  end: { dateTime: "2026-11-03T15:45:00", timeZone },
  attendees: [{ address: "sam@example.com" }],
  recurrence: ["RRULE:FREQ=WEEKLY;BYDAY=TU;COUNT=8"],
  onlineMeetingService: "zoom",
});

event.onlineMeetingUrl; // the Zoom join link, on the event

// Moving the series moves the Zoom meeting too
await workwae.calendarEvents.update({
  accountId,
  eventId: event.id,
  start: { dateTime: "2026-11-03T16:00:00", timeZone },
  end: { dateTime: "2026-11-03T16:45:00", timeZone },
});
What you get

Meetings as part of the event, not a second integration

One field on event create. workwae handles the service's OAuth, the host, the meeting's recurrence and its lifecycle, and tells you when a grant needs attention.

Created with the event

onlineMeetingService: "zoom" or "webex" on create; the join link becomes the event's onlineMeetingUrl on Google, Microsoft 365 and CalDAV.

Per-user grants

Each account's user links their own Zoom or Webex account on a hosted page, with the platform's sign-in app or your own. Meetings are hosted by that service user.

Organization server apps

A Zoom Server-to-Server OAuth app or a Webex Service App hosts meetings for every mailbox in the organization, as the service user with the mailbox's address, with no per-user sign-in.

Recurring meetings

A series gets a recurring meeting translated from its rule. A rule the service cannot repeat by is refused with meeting_recurrence_unsupported before anything is created.

Kept in step

A new subject, start, end or rule moves the meeting; deleting or cancelling the event deletes it. A failed create leaves no meeting behind.

Reauth you hear about

A grant the service refuses is marked reauth_required and announced as meeting_connection.reauth_required. Mail and calendar sync go on.

Scoped access

workwae only creates, updates and deletes meetings and identifies the host. It never reads recordings, chat, transcripts or participant lists.

Sealed credentials

Grants and app secrets are encrypted and bound to their own record, so a value copied onto another account or app does not open there.

A hosted connect page

One link per user, valid for an hour by default, that returns to your app with the outcome. It shows who is asking before anything is connected.

How it works: hosting

Who hosts the meeting

For each event, workwae uses the account's own grant at the service first, then your tenant's server app for that service, and otherwise refuses with meeting_service_not_connected. A grant that needs reconnecting is not passed over for the app.

Ways to host Zoom and Webex meetings
Option Zoom Webex Who hosts
User grant, platform app User OAuth with PKCE through workwae's Zoom app Integration OAuth with PKCE through workwae's Webex integration The service user who signed in
User grant, your own app Your Zoom OAuth app (zoom_oauth) Your Webex integration (webex_oauth) The service user who signed in
Server app A Server-to-Server OAuth app (zoom_s2s) with the account ID, client ID and secret; the host is looked up by email A Service App (webex_service_app) an organization administrator authorized, with its client ID, secret and refresh token The organization's user with the mailbox's address
Scopes meeting:write:meeting, meeting:update:meeting, meeting:delete:meeting, user:read:user; their :admin versions for a server app meeting:schedules_write, meeting:schedules_read, spark:people_read; meeting:admin_schedule_write, meeting:admin_schedule_read, spark-admin:people_read for a Service App Each registered app lists the scopes to grant it
Tokens Every refresh rotates the refresh token; refreshes are serialized so two processes never void a healthy grant. A server app's token lasts an hour Access tokens last 14 days, refresh tokens 90 days; a Service App refreshes like an integration A 401 or 403 is retried once with a fresh token
  • A server app hosts as the user with the mailbox's address, so it is used only where the provider vouches for that address: Gmail, Microsoft 365, and IMAP accounts signed in with Google or Microsoft OAuth.
  • An IMAP account signed in with a password has whatever address its creator typed, so it needs the user's own grant; workwae refuses with meeting_service_not_connected before the app is asked for anything.
  • With a server app, a mailbox with no matching user at the service is refused with meeting_host_missing. Test an app, optionally with a user's address, to see each check before it hosts a meeting.
  • Meetings need the meetings module in the tenant's plan; without it, onlineMeetingService answers plan_module_unavailable.
How it works: the meeting

Times, recurrence and lifecycle

Each service has its own limits on times and repeats. workwae translates what it can, clamps what it must, and refuses a rule it cannot carry over before anything is created.

Zoom and Webex meeting behavior
Aspect Zoom Webex
Times The event's wall-clock start and time zone; the duration in whole minutes, from 1 to 1,440 The start cannot be in the past; the end is clamped to 10 minutes through 23 hours 59 minutes after it
Repeats Daily (interval up to 99), weekly on chosen weekdays (up to 50), monthly on one day or one weekday of one week (up to 10), ending after up to 60 occurrences, at a date, or never; FREQ=DAILY;BYDAY=... becomes weekly The event's RRULE
Refused with meeting_recurrence_unsupported Yearly rules, RDATE, EXDATE, several days or dates a month, and WKST with several days every second week or more Monthly or yearly rules with several days or dates, RDATE and EXDATE
On update The recurrence is sent only when the event's rule changed; Zoom keeps its own otherwise Subject, times and rule follow the event
Notifications Deletes skip Zoom's cancellation reminder Webex sends no email on any write
  • The calendar provider sends the invitations and cancellations, so attendees hear about the meeting once, from the calendar.
  • The meeting is created inside the event create, once the account is found active, and deleted again if anything fails before its link is saved. The link is saved right after the provider creates the event, so a failed read-back still leaves the meeting reachable through it.
  • Before moving a meeting, workwae checks the connection with one read, so a revoked grant stops the update before anything changes. A failure after the event changed is reported with the event changed; repeat the request.
  • Changing one occurrence leaves the service's series alone: its join link works for every occurrence. Setting an onlineMeetingUrl other than the meeting's own deletes the meeting; sending its own link back changes nothing.
  • Deleting or cancelling the event deletes the provider event first and the meeting after it. If the provider refuses, both stay. If the service will not delete the meeting afterwards, it is logged, stays at the service, and the request still succeeds.
Build it: user grants

Let each user link their own account

Create a hosted session and send the user to its link. They sign in at Zoom or Webex, then return to your redirect URI with the outcome, or to workwae's own confirmation page.

  • A Continue page first. Zoom and Webex skip consent for an app a user already authorized, so every sign-in first shows a page naming your tenant, the service and the account. Nothing changes until the user continues.
  • Bound to one browser. The sign-in must finish in the browser that pressed Continue; a callback anywhere else connects nothing.
  • One grant per link. Later sign-ins end with session_used, and one after expiresAt with session_expired. Connecting again replaces the account's grant.
  • Your own app by passing meetingAppId; register the redirect URI the app record lists at the service.
connect-webex.ts
const { url } = await workwae.meetingConnectSessions.create({
  accountId,
  service: "webex",
  redirectUri: "https://app.example.com/settings/meetings",
  state: "user-42",
  ttlSeconds: 1800,
});
// Send the user to url. They come back with
// ?status=success&service=webex&state=user-42

// Later: what is connected, and is it healthy?
const { items } = await workwae.meetingConnections.list({
  accountId,
});
for (const connection of items) {
  console.log(
    connection.service,     // "zoom" | "webex"
    connection.email,       // the host
    connection.status,      // "active" | "reauth_required"
    connection.statusReason,
  );
}

// Disconnect: the grant is deleted, meetings stay
await workwae.meetingConnections.delete({
  accountId,
  service: "webex",
});
Build it: server apps

Host for a whole organization

Register your Zoom Server-to-Server OAuth app or Webex Service App once. Every account without a grant of its own then gets meetings hosted by its own user at the service.

  • One server app per service per tenant; a second answers meeting_app_exists.
  • Verified before use. A server app starts unverified; a test requests a token the way meetings will and marks it active or failing.
  • Refusals mark the app. A refused token marks the app failing, and the event create answers meeting_service_reauth_required.
  • Meetings stay. Deleting a server app leaves the meetings it created at the service.
zoom-server-app.ts
const app = await workwae.meetingApps.create({
  kind: "zoom_s2s",
  name: "Acme Zoom",
  accountId: zoomAccountId, // from the app's credentials page
  clientId,
  clientSecret,
});
app.scopes; // what to grant the app in the Zoom Marketplace

// Token, then a host lookup for one address
const { app: checked, checks } = await workwae.meetingApps.test({
  appId: app.id,
  user: "priya@acme.com",
});
for (const check of checks) {
  console.log(check.name, check.isOk, check.detail);
}
checked.status; // "active" | "failing"

// A Webex Service App takes the refresh token instead
await workwae.meetingApps.create({
  kind: "webex_service_app",
  name: "Acme Webex",
  clientId: webexClientId,
  clientSecret: webexClientSecret,
  refreshToken: webexRefreshToken,
});
Build it: failures

Handle a missing or refused connection

Meeting errors are typed reasons, not service error strings, and a refused grant is announced once as an event, so you can prompt the right user to reconnect.

  • Refused grants. A 401 or 403 is retried once with a fresh token; refused again, or a refused refresh, marks the grant reauth_required.
  • Zoom app removals. When a user removes workwae's Zoom app in the Zoom App Marketplace, Zoom notifies workwae. Every grant that Zoom user gave the platform app is deleted at once, and each account gets meeting_connection.reauth_required with reason app_deauthorized.
  • Meetings outlive grants. Disconnecting or removing the app leaves existing meetings and their links on the events.
  • Calendar keeps working. Mail and calendar sync never use meeting grants, so a refused one stops only meeting work.
meeting-errors.ts
import { isApiError, parseWebhook } from "@workwae/sdk";

try {
  await workwae.calendarEvents.create({
    ...booking,
    onlineMeetingService: "zoom",
  });
} catch (error) {
  if (
    isApiError(error, "meeting_service_not_connected") ||
    isApiError(error, "meeting_service_reauth_required")
  ) {
    return askUserToConnect(booking.accountId, "zoom");
  }
  if (isApiError(error, "meeting_recurrence_unsupported")) {
    return suggestSimplerRule(booking);
  }
  throw error;
}

// In your webhook handler
const event = await parseWebhook(secret, request.headers, body);
const reauth = "meeting_connection.reauth_required";
if (event.known && event.type === reauth) {
  // reason: e.g. "invalid_grant" or "app_deauthorized"
  const { service, email, reason } = event.data;
  await notifyUser(event.accountId, service, email, reason);
}
Events and errors

What you will see

Connection changes arrive as webhooks and feed items; meeting problems on an event write answer with a stable reason. See webhooks and the change feed.

Meeting events and error reasons
Event or reason Meaning
meeting_connection.connected An account connected or reconnected its own grant; data: service, email, reconnected
meeting_connection.reauth_required The service refused the account's grant; data: service, email, reason
calendar_event.updated The event changed; a new or removed join link shows as onlineMeetingUrl in changes
meeting_service_not_connected No grant at the service and no server app the account may use
meeting_service_reauth_required The account's grant or the tenant's server app was refused and must be reconnected or fixed
meeting_host_missing The server app has no user with the mailbox's address
meeting_recurrence_unsupported The service cannot repeat a meeting by the event's rule
meeting_deleted The event's meeting is gone at the service when workwae tries to move it
meeting_service_rejected, meeting_service_unavailable The service refused the meeting, or could not be reached
multiple_meeting_options More than one of onlineMeeting, onlineMeetingUrl and onlineMeetingService
FAQ

Meeting questions

Do I need my own Zoom or Webex app?

No. Users can link their accounts through workwae's platform apps. Register your own Zoom OAuth app or Webex integration to have users sign in through it instead, or a server app to host for a whole organization without per-user sign-in.

What happens to meetings when a user disconnects?

The grant is deleted (and revoked at Zoom), and meetings already created stay at the service with their links on the events. Later events use the tenant's server app, if there is one the account may use.

Why can't a password IMAP account use our server app?

A server app hosts as the organization's user with the mailbox's address. For a password IMAP account that address is whatever was typed, so workwae requires the user's own grant instead of trusting it.

Can I change one occurrence of a series with a Zoom meeting?

Yes. The occurrence changes on the calendar, and the service's recurring meeting stays as it is; its join link works for every occurrence. Changing the series' times or rule moves the meeting.

Who sends the meeting invitations?

The calendar does. Zoom and Webex are told not to email attendees, so people get one invitation with the join link in it.

Early access

Put a meeting link on every event, without a second integration

workwae is in early access. Tell us what you are building, and read the docs in the meantime.