Connect flows
Connect Google, Microsoft and IMAP mailboxes through a hosted page, your own UI or an existing refresh token, and reconnect them.
Every mailbox Workwae reads is an account. This page covers the ways a user connects one, the OAuth apps behind the sign-in, and how to reconnect an account whose grant stopped working.
Choose a flow
| Flow | You build | Use it when |
|---|---|---|
| Hosted connect session | Nothing: Workwae hosts the provider choice, the OAuth hand-off and the IMAP form. | You want the shortest path, or need IMAP mailboxes. |
| Headless OAuth | Your own provider picker. Workwae returns the provider's authorization URL and handles the callback. | The connect step is part of your own UI. |
| Refresh-token import | Your own OAuth flow. You hand Workwae the refresh token. | You already hold refresh tokens and want no user interaction. |
| Password IMAP account | Your own form for address, password and servers. | The mailbox is neither Google nor Microsoft. |
| Organization connection | An administrator's one-time authorization. | You add every mailbox of a Google Workspace or Microsoft 365 domain. |
The browser flows end the same way: the user returns to your redirectUri, which must be on the allowlist. A hosted session may leave it out; the user then ends on a Workwae confirmation page and you read the outcome from the session.
Redirect URI allowlist
The tenant setting redirectUris lists where connect flows may send the user back. Edit it on the console's Settings page or with PATCH /v1/settings (admin scope).
- Up to 50 entries, each an absolute https URL. Plain http is accepted only for
localhostand127.0.0.1. - A redirect URI is allowed when its origin and path exactly match an entry. The query string and fragment are yours to vary.
- Anything else answers 400
redirect_uri_not_allowed. - A session whose address is removed from the list afterwards stops working.
Hosted connect sessions
Create a session and send the user's browser to its url before expiresAt.
curl -X POST "https://api.workwae.com/v1/connect-sessions" \
-H "Authorization: Bearer $WORKWAE_API_KEY" \
-H "content-type: application/json" \
-d '{
"redirectUri": "https://app.example.com/mail/connected",
"providers": ["google", "microsoft"],
"externalRef": "user_42",
"state": "csrf-token",
"options": { "calendarEnabled": true }
}'| Field | Meaning |
|---|---|
providers | What the page offers: google, microsoft, and imap (the IMAP/SMTP form for any other mailbox). Defaults to Google and Microsoft. |
redirectUri | Where the user returns with the outcome. Optional: without it they see a Workwae confirmation page, which needs no allowlist entry. |
state | Echoed back to the redirect URI unchanged. |
externalRef | Your identifier, stored on the account. |
loginHint | The address the user should connect. It prefills the provider sign-in and the IMAP form. |
accountId | Reconnect this existing account instead of creating a new one. |
oauthAppIds | Your own OAuth apps to use instead of Workwae's, per provider (google, microsoft). |
microsoftCloud | The Microsoft cloud for sign-in with Workwae's app. Ignored for your own app. |
options | Settings for the new account, below. |
mcp | { scopes, name? }: show the user an MCP key for the new account before they continue. See MCP for your users. |
ttlSeconds | How long the link works: 60 seconds to 24 hours, one hour by default. A session can be completed once. |
options sets up the account:
calendarEnabled,contactsEnabled: also sync calendars and contacts. Their scopes are asked for only when your plan includes the module.bodyStorage: whether message content is stored (see Read mail).mailAccess:api(default) orimap, which reads the mailbox over IMAP with the OAuth grant. See IMAP with OAuth.sharedMailboxesEnabled: Microsoft only; lets the account open the shared mailboxes its user can access.
With mcp, the session needs the admin scope. Once completed it names the minted key as mcpKeyId; when the account already holds 25 keys, it mints none and answers mcpKeySkipped.
Errors creating a session
| Status | Reason |
|---|---|
| 400 | redirect_uri_not_allowed; oauth_app_mismatch (an app is for the other provider); google_imap_requires_own_app; storage_unavailable (bodyStorage cache or full with no object store); org_connection_account or shared_mailbox_account (accountId cannot be reconnected this way) |
| 402 | billing_past_due: payment is past due beyond the grace period (new accounts only) |
| 403 | plan_limit_storage: the plan does not include body storage |
| 409 | account_mismatch (providers leaves out the account's sign-in), mcp_key_limit |
| 422 | unknown_account, unknown_oauth_app |
Handle the return
The browser comes back to your redirect URI with the outcome in the query string:
?status=success&account_id=<id>&state=<your state>
?status=error&error=<code>&state=<your state>Check state against what you sent, then use the account id. The account.connected event announces the same account.
Without a redirectUri, read the session instead. GET /v1/connect-sessions/{sessionId} returns its status (pending, completed or failed), the error of a failure, and the accountId and email it connected.
curl "https://api.workwae.com/v1/connect-sessions/$SESSION_ID" \
-H "Authorization: Bearer $WORKWAE_API_KEY"error | Meaning |
|---|---|
access_denied | The user declined the consent screen. |
scope_denied | The user unticked a permission the account needs. |
session_used | The session was already completed. |
session_expired | The link was opened after ttlSeconds. |
provider_not_allowed | The session does not offer the provider that was picked. |
plan_limit_accounts | Your plan's account limit is reached. Delete an account or raise the plan, then send a new link. |
account_exists | The mailbox is already connected. Create a reconnect session (accountId) to replace its sign-in. |
account_mismatch | A reconnect signed in with a different mailbox than the account being reconnected. |
account_not_found | The account being reconnected was deleted meanwhile. |
missing_refresh_token | The provider issued no refresh token, so the account could not be kept signed in. Try again; the user may need to remove the app's earlier grant first. |
token_exchange_failed | The provider refused to exchange the sign-in for tokens. |
identity_failed | The provider did not say which mailbox signed in. |
unknown_oauth_app | Your own OAuth app for this provider was removed before the sign-in finished. |
google_imap_requires_own_app | Google over IMAP was requested through Workwae's app, which connects Gmail through the Gmail API only. Use your own Google OAuth app. |
billing_past_due | Your payment is past due beyond the grace period, so no new account is connected. |
provider_error | The provider reported an error of its own. |
internal_error | Something failed on Workwae's side; repeat the attempt with a new link. |
- Treat a code not listed here as a failed attempt: new codes may be added.
- If the return address is no longer on the allowlist, the user sees an error page instead, since there is nowhere safe to send them.
Headless OAuth
With your own provider picker, request the authorization URL for the chosen provider and redirect the user to it. The callback, the account creation and the return to your redirect URI work as for hosted sessions.
curl -X POST "https://api.workwae.com/v1/oauth-authorizations" \
-H "Authorization: Bearer $WORKWAE_API_KEY" \
-H "content-type: application/json" \
-d '{
"provider": "microsoft",
"redirectUri": "https://app.example.com/mail/connected",
"state": "csrf-token"
}'It takes provider (google or microsoft) and the same redirectUri, state, externalRef, accountId, microsoftCloud and options as a session, with one oauthAppId instead of oauthAppIds.
Open the URL in the user's own browser
authorizeUrl is a page on the API origin that names your organization and the provider; the user continues to the provider with its button. Continuing works once and binds the sign-in to that browser with an HttpOnly cookie. A callback arriving in any other browser gets an error page and connects nothing.
Import a refresh token
When your own OAuth app already holds a refresh token with the mail (and calendar) scopes, connect the account without a browser. Name the app with oauthAppId (registered below); omit it for tokens issued to Workwae's app. The account's address is the one the provider reports for the grant.
curl -X POST "https://api.workwae.com/v1/accounts" \
-H "Authorization: Bearer $WORKWAE_API_KEY" \
-H "content-type: application/json" \
-d '{
"provider": "google",
"refreshToken": "1//0g...",
"oauthAppId": "6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b",
"externalRef": "user_42"
}'The token is checked with the provider before the account is created. Refusals answer 400 with invalid_grant (revoked, expired or never valid), invalid_client, scope_denied, token_exchange_failed or identity_failed; the full list is in the Accounts reference.
Your own OAuth apps
By default users sign in through Workwae's Google and Microsoft apps. Register your own to show your name on the consent screen and own the grant.
Register the callback with the provider
Add the redirect URI for your provider to the app at Google or Microsoft:
https://api.workwae.com/connect/callback/google
https://api.workwae.com/connect/callback/microsoftRegister the app with Workwae
POST /v1/oauth-apps needs the admin scope.
curl -X POST "https://api.workwae.com/v1/oauth-apps" \
-H "Authorization: Bearer $WORKWAE_API_KEY" \
-H "content-type: application/json" \
-d '{
"provider": "microsoft",
"cloud": "global",
"name": "Example Mail",
"clientId": "00000000-0000-0000-0000-000000000000",
"clientSecret": "...",
"authority": "common"
}'- Microsoft apps need a
cloud(400cloud_required): each cloud needs its own app registration.authorityis the directory (tenant) id, orcommon. - Google apps take neither
cloudnorauthority(400not_microsoft).
Use it
Pass its id as oauthAppIds on a hosted session, or as oauthAppId for headless OAuth and import.
PATCH /v1/oauth-apps/{appId}changes the name, client secret or authority. Accounts on the app use a new secret from their next token refresh.- An app cannot be deleted while accounts still use it.
Gmail push
Accounts on your own Google app can receive Gmail changes by push instead of polling.
- In the app's Cloud project, create a Pub/Sub topic and an authenticated push subscription to the app's
gmailPushEndpoint, which is also the token audience. Settings > OAuth apps in the console shows a ready gcloud script. - Set
gmailPush(topic,serviceAccount) withPATCH /v1/oauth-apps/{appId}.
Setting it wakes the app's accounts so it applies at once; saving it again retries a watch Gmail refused. gmailPush: null stops the watches and returns the accounts to polling.
Microsoft clouds
microsoftCloud | Cloud |
|---|---|
global | Microsoft 365, including GCC (moderate). The default. |
gcc-high | US Government GCC High. |
dod | US Government DoD. |
china | Microsoft 365 operated by 21Vianet. |
microsoftCloud applies to sign-ins with Workwae's app, for the clouds Workwae runs one in; for a cloud without one, headless OAuth and import answer 400 provider_not_configured. For other clouds, register your own app per cloud; the app carries its cloud. The account reports it as cloud.
Reconnect an account
When a provider revokes a grant (a password change, an administrator removing consent), the account moves to reauth_required and an account.reauth_required event names the reason. Send the user through a connect flow again with the account's id:
curl -X POST "https://api.workwae.com/v1/connect-sessions" \
-H "Authorization: Bearer $WORKWAE_API_KEY" \
-H "content-type: application/json" \
-d '{
"redirectUri": "https://app.example.com/mail/connected",
"accountId": "6f1c2a4e-8b3d-4e5f-9a7b-1c2d3e4f5a6b"
}'- The session signs in to the account's own provider, cloud, OAuth app and mail access;
providersmust include that provider. A password IMAP account gets the IMAP form. - Headless OAuth takes
accountIdtoo, with the account's address as the sign-in hint. - On success the same account becomes active again and
account.connectedis sent withreauthorized: true. - Accounts from an organization connection or a shared mailbox are restored through the connection or the delegating account instead.
Paused is yours, reauth_required is Workwae's
You can set an account to paused or active with PATCH /v1/accounts/{accountId}. reauth_required and disabled are set by the platform; reconnecting the account clears reauth_required.