Features

Every inbox, calendar and contact book, behind one API

workwae connects your customers' Gmail, Microsoft 365 and IMAP accounts, keeps them in sync, and lets you read, write and send through one typed API. Here is everything it does today, and which provider supports what.

Mail

Sync, search and send from any mailbox

workwae keeps an index of every mailbox and message in step with the provider, so lists and searches answer from one place, and every write goes back to the provider and shows up in your index at once.

  • Sync. Mailboxes, labels and message metadata, with a quiet first sync that ends in one account.initialized, then a message.* or mailbox.* event for every change.
  • Bodies and attachments on request from the provider, or from encrypted body storage when you turn it on.
  • Search the index in full text (subject, names, addresses and stored bodies), or pass the query to the provider's own search.
  • Threads and drafts. List conversations and mark, move or delete them whole; create, edit and send drafts.
  • Outbox. Sends are queued, retried and reported as send.sent, send.retrying or send.failed, with scheduled sends, an undo window, cancel and reschedule.
  • Identities. Send as provider aliases, custom IMAP identities or Microsoft Send As addresses, with stored signatures.
  • Templates and mail merge. Handlebars or Markdown templates, and up to 100 personal messages per call.
  • Bounces and unsubscribes. Delivery failures are matched to the send and announced as message.bounced; one-click unsubscribe feeds suppression lists.
  • Open and click tracking, off by default, with every hit classified as a person or a machine (image proxy, prefetch, link scanner).

Explore mail Read the quickstart

mail.ts
import { createClient } from "@workwae/sdk";

const workwae = createClient({
  apiKey: process.env.WORKWAE_API_KEY,
});

// Full text over the index, or the provider's own search
const { items, source } = await workwae.search.messages({
  accountId,
  q: '"renewal quote" OR invoice',
});

// Queued, retried and reported through webhooks
const send = await workwae.outbox.create({
  accountId,
  to: [{ name: "Priya Shah", address: "priya@example.com" }],
  subject: "Your renewal quote",
  html: "<p>The quote is attached.</p>",
  tracking: { opens: true, clicks: true },
  reference: "deal_1042",
});

// Until send.undoUntil, this takes it back
await workwae.outbox.cancel({ accountId, outboxId: send.id });
Calendar

Every calendar, with recurring series done right

Calendar sync runs next to mail with its own backoff. Each recurring series is stored once with its occurrences and exceptions, so a change to a series is one event, not hundreds.

  • Every calendar on the account, with its owner, access, color and time zone, and which one is primary.
  • A rolling window around today, 90 days back and a year ahead, that moves forward on its own.
  • Full writes. Create, update and delete events and series (RRULE); changing one occurrence makes it an exception.
  • RSVP. Accept, tentatively accept or decline one occurrence or the whole series, with an optional comment.
  • Free/busy and suggestions. Busy blocks per attendee and common free slots inside working hours, from one provider request.
  • Online meetings. Teams on Microsoft 365, Meet on Google, a Zoom or Webex meeting workwae creates, or any link you attach.
  • Events as calendar_event.created, .updated (naming the fields that changed) and .deleted.

Explore calendar

calendar.ts
const { items } = await workwae.calendars.list({ accountId });
const primary = items.find((calendar) => calendar.isPrimary)!;
const timeZone = "America/Chicago";

// Common free slots for everyone, in working hours
const { suggestions } = await workwae.availability.find({
  accountId,
  attendees: ["priya@example.com", "sam@example.com"],
  start: "2026-11-02T00:00:00Z",
  end: "2026-11-07T00:00:00Z",
  durationMinutes: 45,
  timeZone,
  workingHours: { start: "09:00", end: "17:00" },
});

// A weekly series with a Teams or Meet link
await workwae.calendarEvents.create({
  accountId,
  calendarId: primary.id,
  subject: "Pipeline review",
  start: { dateTime: "2026-11-03T10:00:00", timeZone },
  end: { dateTime: "2026-11-03T10:45:00", timeZone },
  attendees: [{ address: "priya@example.com" }],
  recurrence: ["RRULE:FREQ=WEEKLY;BYDAY=TU;COUNT=12"],
  onlineMeeting: true,
});

// Answer an invitation
await workwae.calendarEvents.respond({
  accountId,
  eventId,
  response: "accepted",
});
Contacts

Address books from Google, Microsoft and CardDAV

Contacts sync the same way mail does: a quiet first pass, then an event for every change, and your own writes visible in the next read.

  • Sources. Google People, every Microsoft 365 contact folder, and CardDAV address books of IMAP accounts.
  • Read and search names, email addresses, phone numbers, organization, job title, birthday, notes and photos.
  • Write back. Create, update and delete contacts, in a chosen folder on Microsoft 365.
  • Events as contact.created, contact.updated and contact.deleted, after one contacts.initialized.
  • Opt-in. Contacts stay off until you turn them on, for every account or for one.

Explore contacts

contacts.ts
import { paginate } from "@workwae/sdk";

// Every page, one contact at a time
const contacts = paginate(workwae.contacts.list, {
  accountId,
  q: "acme",
});
for await (const contact of contacts) {
  console.log(contact.displayName, contact.emails);
}

await workwae.contacts.create({
  accountId,
  givenName: "Priya",
  familyName: "Shah",
  emails: [{ address: "priya@example.com", type: "work" }],
  organization: "Acme",
  jobTitle: "Head of Operations",
});
Meetings

Zoom and Webex meetings, created with the event

Name a service when you create an event and workwae creates the meeting, writes its join link into the event, and keeps the two together from then on.

  • Hosted by the right person. The account user's own Zoom or Webex grant, or your organization's Zoom Server-to-Server OAuth app or Webex Service App.
  • A hosted connect page for users to link their Zoom or Webex account, with your own apps or the platform's.
  • Series included. A recurring event gets a recurring meeting, and a changed subject, time or rule moves the meeting with it.
  • Cleaned up. Deleting or cancelling the event deletes the meeting; a failed create leaves nothing behind.
  • Scoped access. workwae only creates, updates and deletes meetings. It never reads recordings, chat or transcripts.

Explore meetings Using workwae with Zoom

meetings.ts
// Once: the user links their own Zoom account
const { url } = await workwae.meetingConnectSessions.create({
  accountId,
  service: "zoom",
  redirectUri: "https://app.example.com/settings/meetings",
});

// Then any event can come with a Zoom meeting
const event = await workwae.calendarEvents.create({
  accountId,
  calendarId,
  subject: "Intro call",
  start: { dateTime: "2026-11-04T15:00:00", timeZone: "UTC" },
  end: { dateTime: "2026-11-04T15:30:00", timeZone: "UTC" },
  attendees: [{ address: "sam@example.com" }],
  onlineMeetingService: "zoom",
});

event.onlineMeetingUrl; // the Zoom join link
Webhooks and change feed

Every change, pushed or pulled

Every change lands in one ordered feed. Read it with a cursor, receive it as signed webhooks, or both: a feed item and a webhook body are the same envelope.

  • Signed per Standard Webhooks; the SDK's parseWebhook verifies a delivery and returns it typed.
  • Filters by event type, account, and message criteria such as mailbox, sender, recipient, subject, attachments and direction.
  • Replay one event or a whole time range, with a fresh signature and the same event id, and send typed test events of any type.
  • A delivery log per endpoint, with retries that back off and honor Retry-After, so one slow endpoint never holds up the rest.
  • An ordered change feed paged by transaction, so events committed out of order are never skipped; an empty page means caught up.
  • Safe delivery. Private and loopback addresses are refused, DNS is pinned per delivery and redirects are never followed.

Explore webhooks and the change feed Read the webhooks guide

webhooks.ts
import { changes, parseWebhook } from "@workwae/sdk";

const { endpoint, secret } = await workwae.webhooks.create({
  url: "https://app.example.com/hooks/workwae",
  filters: {
    eventTypes: [
      "message.created",
      "send.failed",
      "calendar_event.updated",
    ],
  },
});

// In your handler: checks the signature and the timestamp
const event = await parseWebhook(secret, request.headers, body);
if (event.known && event.type === "message.created") {
  await enqueue(event.id, event.data.message.subject);
}

// Or pull the ordered feed and save the cursor
const feed = changes(workwae.changes.list, { cursor: saved });
for await (const change of feed) await handle(change);
await save(feed.cursor);

// Missed a day? Deliver it again
await workwae.webhooks.replay({
  endpointId: endpoint.id,
  since: new Date("2026-11-02T00:00:00Z"),
  until: new Date("2026-11-03T00:00:00Z"),
});
Connect flows

Let users connect a mailbox in one step

Send users to a hosted page, or keep them in your own interface and let workwae run the OAuth exchange. Either way, workwae asks the provider for the mailbox address and seals the credential before anything is stored.

  • Hosted connect page offering Google, Microsoft and an IMAP form, returning to a redirect URI on your allowlist.
  • Headless OAuth. Your own buttons; workwae returns the URL to send the user to and handles the callback.
  • Your OAuth apps or ours. Use your own Google and Microsoft apps, per Microsoft cloud, or workwae's.
  • IMAP and SMTP with a password or Google and Microsoft sign-in. The login is checked before anything is stored.
  • Autodiscovery from the address alone: a built-in provider table, the domain's autoconfig file, the Mozilla ISPDB, DNS SRV, then MX. CalDAV and CardDAV are found the same way.
  • Reconnects. A revoked grant moves the account to reauth_required with an event; a reconnect link keeps the same account.
  • Bound to one browser. Every sign-in passes a Continue page naming your product, so a forwarded link connects nothing.

Explore connect flows Connect flows guide IMAP and SMTP guide

connect.ts
// Hosted: one link to send the user to
const { url } = await workwae.connectSessions.create({
  redirectUri: "https://app.example.com/mail/connected",
  providers: ["google", "microsoft", "imap"],
  externalRef: "user_42",
  options: { calendarEnabled: true, contactsEnabled: true },
});

// Headless: your UI, any Microsoft cloud
const { authorizeUrl } = await workwae.oauthAuthorizations.create({
  provider: "microsoft",
  microsoftCloud: "gcc-high",
  redirectUri: "https://app.example.com/mail/connected",
});

// IMAP: find the servers from the address
const found = await workwae.autoconfig.lookup({
  email: "ada@example.org",
});

const account = await workwae.imapAccounts.create({
  email: "ada@example.org",
  password: appPassword,
});
Organization connections

A whole domain, without asking every user

An administrator authorizes workwae once, and you add mailboxes by address. Each is checked through the connection before its account is created.

  • Google Workspace domain-wide delegation through a service account: Gmail always, Calendar by default, Contacts when you choose.
  • Microsoft app-only access through admin consent to workwae's app, or your own app registration with a client secret or a certificate, in any cloud.
  • Directory listing to pick mailboxes from, and a test that reports each permission the connection is missing.
  • Shared mailboxes opened as accounts of their own through a user's Microsoft grant, over Graph or Exchange IMAP.
  • Consent you can trust. A Microsoft directory is recorded only from the administrator's own sign-in, and belongs to one connection.

Explore organization connections

organizations.ts
// Google Workspace: domain-wide delegation
const connection = await workwae.orgConnections.create({
  kind: "google_delegation",
  name: "Acme Workspace",
  domain: "acme.com",
  serviceAccountKey,
  contactsEnabled: true,
});

// Add mailboxes by address, one result each
const { items } = await workwae.orgConnections.addAccounts({
  connectionId: connection.id,
  mailboxes: [
    { email: "support@acme.com" },
    { email: "sales@acme.com" },
  ],
});

// Microsoft: open a shared mailbox with a user's grant
await workwae.sharedMailboxes.create({
  accountId,
  email: "billing@contoso.com",
});
AI

Understand replies, and draft the next one

Optional AI features, off until you turn them on. workwae sends only the text a feature needs to Gatewae, its AI gateway, and never uses your data to train models.

  • Reply classification. Replies to your sends are sorted into opt-out request, auto-reply, delivery failure or human reply, and announced as message.classified.
  • Auto-reply details: out of office with the return date, left the company or changed address with the alternate contacts named, ticket acknowledgments and challenge-responses.
  • Opt-outs handled. A likely opt-out from your recipient goes to your opt-out setting: suppressed, or held for review.
  • Drafting. Write an email body from instructions, optionally in reply to a message, for a person to edit. Nothing is sent or saved.
  • Minimal input. Classification sees the subject, the start of the reply's own text and its auto-reply headers.

Explore AI How AI features handle data

ai.ts
// A reply to one of your sends, classified
if (event.known && event.type === "message.classified") {
  const { classification } = event.data;
  if (classification.autoReplyCategory === "out_of_office") {
    await followUpOn(classification.returnDate);
  }
}

// Draft an answer for a person to edit
const { text } = await workwae.ai.draft({
  instructions: "Thank them and offer Tuesday afternoon",
  context: { accountId, replyToMessageId: messageId },
  format: "text",
});
Providers

What each provider supports

One API shape across every provider. Where push exists, workwae uses it, and a backstop poll covers any notification a provider drops.

Provider support by capability
Provider Mail Calendar Contacts Meetings Push and polling
Gmail and Google Workspace Gmail API: labels, threads, drafts, send, provider search Google Calendar: every calendar, series, RSVP, free/busy Google People API Google Meet, Zoom, Webex Mail through Cloud Pub/Sub, calendars through watch channels; contacts polled daily
Microsoft 365 and Outlook.com Microsoft Graph, including shared mailboxes Every calendar and calendar group, series, RSVP, free/busy Every contact folder Teams, Zoom, Webex Graph change notifications for mail, calendar and contacts
Microsoft 365 GCC High and DoD Microsoft Graph in that cloud Microsoft Graph in that cloud Microsoft Graph in that cloud Teams, Zoom, Webex Graph change notifications where the cloud offers them
IMAP and SMTP Any server, with a password or Google or Microsoft sign-in Through CalDAV Through CardDAV Not applicable IDLE on the inbox, NOTIFY where the server offers it
CalDAV and CardDAV Not applicable Any CalDAV server; invitations mailed when the server does not schedule Any CardDAV server Zoom, Webex, or any link Polled, and refreshed after every write
Zoom Not applicable Not applicable Not applicable Meetings for events; user sign-in or Server-to-Server OAuth app App removals handled as they happen
Webex Not applicable Not applicable Not applicable Meetings for events; user sign-in or Service App Not applicable
  • Every account is also polled on a schedule, so a dropped notification only delays a change.
  • Microsoft national clouds each need their own app registration; workwae signs in and calls Graph in the account's cloud. In 21Vianet, Graph has no message delta yet, so mail runs over IMAP and SMTP with Microsoft sign-in.
  • CalDAV and CardDAV servers are discovered for iCloud, Fastmail, Yahoo (calendar), GMX, Zoho, mailbox.org and Posteo, and any server publishing .well-known or DNS SRV records.
  • Zoom and Webex meetings on IMAP accounts signed in with a password need the user's own Zoom or Webex grant.
  • Google accounts over IMAP need your own Google OAuth app; the Gmail API works with workwae's.

Build on one API instead of five

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