Browse the docs

Meetings

Zoom and Webex meetings on calendar events: per-user grants, server apps, recurrence and errors.

Meetings with calendar events

Name a service with onlineMeetingService (zoom or webex) when you create a calendar event, and Workwae creates the meeting, writes its join link into the event's onlineMeetingUrl on Google, Microsoft 365 and CalDAV, moves it when the event moves and deletes it when the event is deleted or cancelled. Meetings need the meetings module in the tenant's plan; without it the field answers 403 plan_module_unavailable.

curl -X POST "https://api.workwae.com/v1/accounts/$ACCOUNT_ID/calendar-events" \
  -H "Authorization: Bearer $WORKWAE_API_KEY" \
  -H "content-type: application/json" \
  -d '{
  "calendarId": "'"$CALENDAR_ID"'",
  "subject": "Customer onboarding",
  "start": { "dateTime": "2026-11-03T15:00:00", "timeZone": "America/Chicago" },
  "end": { "dateTime": "2026-11-03T15:45:00", "timeZone": "America/Chicago" },
  "recurrence": ["RRULE:FREQ=WEEKLY;BYDAY=TU;COUNT=8"],
  "onlineMeetingService": "zoom"
}'

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. A new subject, start, end or rule moves the meeting. Changing one occurrence leaves the service's series alone, and its join link works for every occurrence. Only one of onlineMeeting, onlineMeetingUrl and onlineMeetingService may be set (multiple_meeting_options).

Workwae only creates, updates and deletes meetings and looks up the host. It never reads recordings, chat, transcripts or participant lists.

Who hosts the meeting

For each event, Workwae uses the account's own grant at the service first, then the 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.

OptionHow it works
User grant, platform appThe account's user signs in to Zoom or Webex through Workwae's app on a hosted page. The meeting is hosted by that service user.
User grant, your own appThe same, through your Zoom OAuth app (zoom_oauth) or Webex integration (webex_oauth).
Server appA Zoom Server-to-Server OAuth app (zoom_s2s) or a Webex Service App (webex_service_app) hosts for every mailbox in the organization, as the service user with the mailbox's address. No per-user sign-in.

A server app is used only where the provider vouches for the mailbox's address: Gmail, Microsoft 365, and IMAP accounts signed in with Google or Microsoft OAuth. A password IMAP account needs its user's own grant. A mailbox with no matching user at the service is refused with meeting_host_missing.

Per-user grants

Create a hosted meeting connect session and send the user to its url. They see a page naming your tenant, the service and the account, sign in at Zoom or Webex, and return to your redirectUri with the outcome and your state. The sign-in must finish in the same browser, and one link connects once; connecting again replaces the account's grant. Pass meetingAppId to sign in through your own app.

const { url } = await workwae.meetingConnectSessions.create({
  accountId,
  service: "webex",
  redirectUri: "https://app.example.com/settings/meetings",
  state: "user-42",
});
// Send the user to url. They come back with
// ?status=success&service=webex&state=user-42

const { items } = await workwae.meetingConnections.list({ accountId });
for (const connection of items) {
  console.log(connection.service, connection.email, connection.status);
}

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

Server apps

Register one server app per service per tenant (a second answers meeting_app_exists). It starts unverified; a test requests a token the way meetings will, optionally looks up a host by address, and marks it active or failing. Deleting a server app leaves the meetings it created at the service.

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

const { app: checked, checks } = await workwae.meetingApps.test({
  appId: app.id,
  user: "[email protected]",
});
for (const check of checks) console.log(check.name, check.isOk, check.detail);
checked.status; // "active" | "failing"

Errors and reauthorization

A 401 or 403 from the service is retried once with a fresh token; refused again, the grant is marked reauth_required and meeting_connection.reauth_required is sent with the service, host email and reason. Mail and calendar sync never use meeting grants, so they keep running.

ReasonMeaning
meeting_service_not_connectedNo grant at the service and no server app the account may use.
meeting_service_reauth_requiredThe account's grant or the tenant's server app was refused.
meeting_host_missingThe server app has no user with the mailbox's address.
meeting_recurrence_unsupportedThe service cannot repeat a meeting by the event's rule.
meeting_deletedThe meeting is gone at the service when Workwae tries to move it.
meeting_service_rejected, meeting_service_unavailableThe service refused the meeting, or could not be reached.
import { isApiError } 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");
  }
  throw error;
}

Every route is in the Meeting connections and Meeting apps references; event fields are in the Calendar guide.