OAuth API
Authorize
Entry point for the OAuth2 authorization flow. Creates or retrieves the user in the database, loads any existing tokens, stores the OAuth request in a cookie, and redirects to the provider selection page where users can connect their calendar accounts.
This endpoint does not require authentication. It initiates the OAuth2 authorization flow.
Endpoint: GET /authorize
Request Parameters
Project/application identifier. This uniquely identifies your application in the Mobiscroll Connect system.
Your own unique identifier for the user in your system who is being authorized.
The scope of access requested from the user. Can be one of free-busy, read, or read-write. If not specified, defaults to read-write.
read-writeNot used to choose the callback. Connect always redirects the user to the redirect URI registered for your application in the dashboard, with the authorization code; a redirect_uri sent in the query is replaced by the registered one. Send that same registered URI to POST /token.
Optional state parameter to maintain across the OAuth flow. This is passed back to your redirect_uri and can be used to prevent CSRF attacks and maintain application state.
undefinedLanguage for the Connect pages (provider selection, consent, login, and error pages). See Supported languages for the values Connect accepts. When omitted, the UI falls back to the browser's Accept-Language header, then English. Arabic (ar), Hebrew (he) and Persian (fa) render right-to-left. Example: ?lng=es.
Accept-Language, then enComma-separated list of the providers to offer on the provider selection page, for example providers=google,microsoft. Accepted values: google, microsoft, apple, caldav; unknown values are ignored, and when none is valid all providers are shown. display_providers is accepted as an alias.
Color scheme of the Connect pages: light, dark or auto (follows the device setting). It takes precedence over your application's default theme — see Theme precedence.
autoOAuth2 response type. Typically set to "code" for the authorization code flow.
undefinedResponse
Redirects to the provider selection page (/authorize) with all query parameters preserved, and redirect_uri set to your application's registered redirect URI. The provider selection page allows the user to:
- Choose which calendar provider(s) to connect (Google Calendar, Microsoft Outlook, Apple Calendar, CalDAV)
- Authenticate with each selected provider
- Review and approve the OAuth consent screen
- Complete the OAuth authorization flow
After the user successfully authenticates and approves the authorization, they will be redirected back to your application's redirect_uri with:
code: The authorization code that can be exchanged for an access token (this is theuser_idvalue)state: The state parameter you provided (if any), for CSRF protection
Example redirect to your application:
https://app.example.com/callback?code=user-456&state=xyz789
Sets oauth_req cookie containing the complete OAuth request for later retrieval. The cookie has the following properties:
- httpOnly: true
- path: /
- maxAge: 2 hours
- sameSite: lax
Error Responses
- 400 - Bad Request. Error creating or retrieving user
- 401 - Unauthorized. Invalid or unrecognized
client_id - 500 - Internal Server Error. Database or unexpected error
Examples
- REST
- Node.js SDK
- Python SDK
- PHP SDK
- .NET SDK
- Java SDK
- Go SDK
- Ruby SDK
GET /authorize?client_id=proj-123&user_id=user-456&redirect_uri=https://app.example.com/callback&response_type=code&state=xyz789&scope=read-write
# Optional: append &lng=es to localize the Connect pages,
# &theme=dark to set their color scheme, or &providers=google,microsoft to limit the providers offered
302 Redirect
Location: /authorize?client_id=proj-123&user_id=user-456&redirect_uri=https://app.example.com/callback&response_type=code&state=xyz789&scope=read-write
Set-Cookie: oauth_req={"client_id":"proj-123","user_id":"user-456",...}; HttpOnly; Path=/; Max-Age=7200; SameSite=Lax
// Generate the authorization URL
const authUrl = client.auth.generateAuthUrl({
userId: 'user-456',
// Optional parameters
// state: 'xyz789',
// scope: 'read-write',
// lng: 'es', // localize the Connect pages
// theme: 'dark', // color scheme of the Connect pages
});
// Redirect the user to authUrl
// res.redirect(authUrl);
# Generate the authorization URL
auth_url = client.auth.generate_auth_url(
user_id='user-456',
# Optional parameters:
# scope='read-write',
# state='xyz789',
# lng='es', # localize the Connect pages
# theme='dark', # color scheme of the Connect pages
)
# Redirect the user to auth_url
# return redirect(auth_url)
// Generate the authorization URL
$authUrl = $client->auth()->generateAuthUrl(
userId: 'user-456',
// Optional parameters:
// scope: 'read-write',
// state: 'xyz789',
// lng: 'es', // localize the Connect pages
// theme: 'dark', // color scheme of the Connect pages
);
// Redirect the user to $authUrl
header('Location: ' . $authUrl);
// Generate the authorization URL
var authUrl = client.Auth.GenerateAuthUrl(new AuthorizeParams
{
UserId = "user-456",
// Optional parameters:
// Scope = "read-write",
// State = "xyz789",
// Lng = "es", // localize the Connect pages
// Theme = "dark", // color scheme of the Connect pages
});
// Redirect the user to authUrl
// return Redirect(authUrl);
import com.mobiscroll.connect.models.AuthUrlParams;
// Generate the authorization URL
String authUrl = client.auth().generateAuthUrl(AuthUrlParams.builder()
.userId("user-456")
// Optional parameters:
// .scope("read-write")
// .state("xyz789")
// .lng("es") // localize the Connect pages
// .theme("dark") // color scheme of the Connect pages
.build());
// Redirect the user to authUrl
// response.sendRedirect(authUrl);
// Generate the authorization URL
authURL := client.Auth().GenerateAuthURL(&mobiscroll.AuthURLParams{
UserID: "user-456",
// Optional parameters:
// Scope: "read-write",
// State: "xyz789",
// Lng: "es", // localize the Connect pages
// Theme: "dark", // color scheme of the Connect pages
})
// Redirect the user to authURL
// http.Redirect(w, r, authURL, http.StatusFound)
# Generate the authorization URL
auth_url = client.auth.generate_auth_url(
user_id: 'user-456',
# Optional parameters:
# scope: 'read-write',
# state: 'xyz789',
# lng: 'es', # localize the Connect pages
# theme: 'dark' # color scheme of the Connect pages
)
# Redirect the user to auth_url
# redirect auth_url
- The
client_idis validated against the database before proceeding - If the
client_idis invalid, the user is redirected to an error page - The user is always redirected to the redirect URI registered for the
client_id; aredirect_uriin the query is not used - The OAuth request is stored in a cookie to maintain state across the authorization flow
- If the user already exists in the database, their existing information is retrieved
- Any existing provider tokens are loaded and initialized in the token store
- The cookie expires after 2 hours if the flow is not completed
- All query parameters are passed to the provider selection page, with
redirect_urireplaced by the registered one
Pass lng on the authorize URL to render the Connect pages (provider selection, consent, login, and error pages) in a specific language. See Supported languages for the current list.
- If
lngis omitted, the UI falls back to the browser'sAccept-Languageheader, then English. - Arabic (
ar), Hebrew (he) and Persian (fa) are right-to-left; the UI direction switches automatically. - Example:
/authorize?client_id=...&user_id=...&lng=es. In the SDKs, passlngto the auth-URL builder (see the examples above).
Set a Display Name for your application in the dashboard. When set, the authorization and connection-management screens are personalized with it (e.g. "<Display Name> is using Mobiscroll Connect ..." and a "Back to <Display Name>" button) so users always know which app they are connecting to. When no display name is set, a generic wording is shown. No request parameter is required - the display name is resolved from the client_id.
Calling /authorize again for a user_id that already has tokens does not revoke any existing access or refresh tokens. The existing tokens are loaded and remain valid. The authorization flow simply allows the user to connect additional calendar provider accounts on top of the ones already connected.
To fully invalidate a user's tokens before re-authorization, call POST /revoke first.
Token management
Token Lifetimes
The following table summarizes the lifetime of every credential issued by Mobiscroll Connect.
| Token / Credential | Lifetime | Notes |
|---|---|---|
| Authorization code | 10 minutes | Single-use — deleted on first exchange |
| Access token (JWT) | 1 hour (3600 s) | exp claim embedded in the JWT; rejected immediately after expiry even if the signature is valid |
| Refresh token | No embedded expiry | The refresh token does not expire after a given time. Validity enforced server-side via JTI rotation (rotation happens on refresh call, not by time). Remains valid until rotated (plus a 30-second grace period) or explicitly revoked. If refresh returns invalid_grant or the newly issued refresh token is lost before persistence, the user must re-authorize. |
| OAuth session cookie | 2 hours | oauth_req cookie that carries the in-progress authorization state |
Every POST /token call with grant_type=refresh_token issues a new refresh token and invalidates the previous one after a 30-second grace period.
Presenting a replaced refresh token after that returns invalid_grant, and presenting one from two or more rotations back revokes the user's authorization — see Refreshing from several instances.
Calling /revoke invalidates the entire token family (all access tokens and refresh tokens for that user/client pair) at once.
Refreshing from several instances
When your application runs as more than one process — several containers or pods, a process manager in cluster mode, serverless functions, background workers — two of them can refresh the same user's token at nearly the same moment. Connect accepts this within limits:
| The refresh token you present | Result |
|---|---|
| The current one | 200 — a new access token and a new refresh token |
| The one it replaced, within 30 seconds of the replacement | 200 — a new access token and a refresh token interchangeable with the one the first request received. Every instance keeps working, and it does not matter which of the returned refresh tokens you store |
| The one it replaced, more than 30 seconds later | 403 invalid_grant. Nothing is revoked — load the current token from your storage and retry |
| One from two or more rotations back | 403 invalid_grant, and Connect treats it as a copied token being reused: it revokes the authorization (every refresh token and access token for that user). The user must re-authorize |
The last row is why every instance must work from the current token rather than its own copy:
- Store tokens where all your instances read them — a database or a shared cache, not only each process's memory.
- Give each client the current tokens from that storage when it starts a job or handles a request, rather than one copy loaded at startup.
- Write the new tokens back as soon as a refresh returns.
You do not need a lock around refreshes. The official SDKs also share one refresh between concurrent calls on the same client instance, but they do not coordinate across processes — see each SDK's integration guide for persisting refreshed tokens and loading them back.
Handling token expiry on API calls
All protected API endpoints (/events, /calendars, /subscribe-webhook, etc.) validate the Authorization: Bearer <access_token> header. When the access token is missing, expired, or revoked the server returns 401 Unauthorized.
Recommended handling flow:
API call
└─► 401 Unauthorized
└─► POST /token (grant_type=refresh_token)
├─► 200 OK → persist new tokens, retry original request
└─► 403 invalid_grant → user must re-authorize (redirect to GET /authorize)
Implement a single request-retry wrapper (or an HTTP interceptor) that catches 401 responses, attempts one token refresh, and only falls back to re-authorization if the refresh itself fails.
Get Access Token
Exchanges an authorization code for an access token. This is the token endpoint of the OAuth2 authorization code flow.
This endpoint requires client authentication using either HTTP Basic authentication or client credentials in the request body.
Endpoint: POST /token
Authentication
This endpoint supports two authentication methods:
HTTP Basic Authentication:
Authorization: Basic base64(client_id:client_secret)
Request Body Authentication:
- Include
client_idandclient_secretin the request body
Request Parameters
The OAuth2 grant type. Must be set to "authorization_code" for the authorization code flow.
The authorization code received from the authorization endpoint. This code is the user_id and is returned to your redirect_uri after the user completes the authorization flow.
The redirect URI used in the authorization request. This must match exactly with the redirect URI used when obtaining the authorization code.
Your application's client identifier. Required if not using HTTP Basic authentication.
undefinedYour application's client secret. Required if not using HTTP Basic authentication.
undefinedResponse
The access token (JWT) that can be used to authenticate API requests. This token contains the user ID, client ID, and project ID in the payload.
An access token is a credential (usually a string) that your application receives after a successful authentication. It is used to authorize API requests on behalf of a user. In Mobiscroll Connect, the access token is a JWT (JSON Web Token).
A JWT is a secure, compact, URL-safe token format that encodes claims about the user and the application. Mobiscroll Connect uses JWTs as access tokens, which include information such as the user ID, client ID, and expiration time. The server verifies the JWT to ensure requests are authentic and authorized.
The type of token returned. Always "Bearer".
A refresh token that can be used to obtain a new access token when the current one expires. Refresh tokens do not expire on their own — they remain valid until used or explicitly revoked. Each use of a refresh token issues a new access token and a new refresh token (token rotation), and the previous refresh token is invalidated after a 30-second grace period. Store this token securely on the server side.
An access token is short-lived (1 hour). Rather than forcing the user to log in again every hour, the server issues a refresh token alongside the access token. Your application stores the refresh token securely (server-side, never in the browser) and uses it — silently, without user interaction — to obtain a fresh access token whenever the current one expires.
If you lose the refresh token (e.g. it was never persisted, or was overwritten before the new one was saved), the user must complete the full OAuth authorization flow again to restore access.
Mobiscroll Connect implements refresh token rotation: every time you exchange a refresh token for a new access token, the server issues a brand-new refresh token and invalidates the old one after 30 seconds. Presenting the old one after that is rejected with invalid_grant, and presenting one from two or more rotations back also revokes the user's authorization — see Refreshing from several instances.
The response does not include expires_in. Access tokens are valid for 1 hour; to schedule a refresh, read the exp claim of the access token (see JWT Token Payload), or refresh when an API call returns 401.
Error Responses
Error bodies carry error (one of the codes below) and a human-readable error_description.
- 400 - Bad Request
invalid_request- Missing required parameters, for examplecode
- 401 - Unauthorized
invalid_client- Client authentication failed
- 403 - Forbidden
invalid_grant- Invalid authorization code, code expired, client mismatch, or redirect URI mismatch
- 500 - Internal Server Error
server_error- The request could not be completed; retry it
- 501 - Not Implemented
unsupported_grant_type-grant_typeis notauthorization_codeorrefresh_token
Token Validation
The endpoint performs the following validations:
- Authorization code exists - The code must be valid and found in the database
- Client match - The client_id must match the one used to obtain the code
- Redirect URI match - The redirect_uri must exactly match the original request
- Code expiration - The authorization code expires after 10 minutes
- Single use - The authorization code is deleted after successful exchange
JWT Token Payload
The access token is a JWT containing:
{
"sub": "user_id", // User identifier
"aud": "client_id", // Client/Project identifier
"projectId": "client_id", // Project identifier
"scope": "read-write", // Granted scope
"jti": "uuid", // Unique token ID
"exp": 1234567890 // Expiration timestamp (1 hour from issuance)
}
The refresh token is a JWT containing:
{
"sub": "user_id", // User identifier
"aud": "client_id", // Client/Project identifier
"projectId": "client_id", // Project identifier
"type": "refresh", // Token type discriminator
"jti": "uuid", // Unique token ID (used for rotation tracking)
// No "exp" — validity is managed server-side via the database
}
Examples
- REST
- Node.js SDK
- Python SDK
- PHP SDK
- .NET SDK
- Java SDK
- Go SDK
- Ruby SDK
POST /token
Authorization: Basic YmFzZTY0X2VuY29kZWRfY3JlZGVudGlhbHM=
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=user-456&redirect_uri=https://app.example.com/callback
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=user-456&redirect_uri=https://app.example.com/callback&client_id=proj-123&client_secret=secret-xyz
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
{
"error": "invalid_grant",
"error_description": "Invalid authorization code"
}
{
"error": "invalid_grant",
"error_description": "Authorization code expired"
}
// Exchange authorization code for access token
// The client uses the configured clientId and clientSecret
const tokenResponse = await client.auth.getToken(code);
// The client is automatically authenticated with the new token
// Persist tokenResponse to restore it later with client.setCredentials()
# Exchange authorization code for access token
# The client is automatically authenticated after get_token()
token_response = client.auth.get_token(code)
# Persist tokens for future requests
session['access_token'] = token_response.access_token
session['refresh_token'] = token_response.refresh_token
// Exchange authorization code for access token
$code = $_GET['code'];
$tokenResponse = $client->auth()->getToken($code);
// Store the token and authenticate the client
$client->auth()->setCredentials($tokenResponse);
// Exchange authorization code for access token
// The client is automatically authenticated with the new token
var tokenResponse = await client.Auth.GetTokenAsync(code);
// Persist tokenResponse to restore it later with client.Auth.SetCredentials()
import com.mobiscroll.connect.models.TokenResponse;
// Exchange authorization code for access token
// The client is automatically authenticated with the new token
TokenResponse tokens = client.auth().getToken(code);
// Persist tokens server-side keyed by your user
httpSession.setAttribute("access_token", tokens.getAccessToken());
httpSession.setAttribute("refresh_token", tokens.getRefreshToken());
// Exchange authorization code for access token
// The client is automatically authenticated with the new token
tokens, err := client.Auth().GetToken(ctx, code)
if err != nil {
// handle error
return
}
// Persist tokens server-side keyed by your user
session.Set("access_token", tokens.AccessToken)
session.Set("refresh_token", tokens.RefreshToken)
# Exchange authorization code for access token
# The client is automatically authenticated with the new token
tokens = client.auth.get_token(code)
# Persist tokens server-side keyed by your user
session[:access_token] = tokens.access_token
session[:refresh_token] = tokens.refresh_token
{
"error": "invalid_grant",
"error_description": "Client mismatch"
}
- Authorization codes expire after 10 minutes
- Authorization codes are single-use and deleted after successful exchange
- Access tokens expire after 1 hour
- A refresh token is always returned alongside the access token; store it securely server-side
- The access token is a JWT that must be included as a Bearer token in all subsequent API requests
- Client credentials can be provided via HTTP Basic authentication or in the request body
- The redirect_uri must exactly match the one used in the authorization request
- Revoked access tokens are rejected immediately even if the JWT signature is still valid
Refresh Access Token
Obtains a new access token using a refresh token. Each call also issues a new refresh token and invalidates the previously used one after a 30-second grace period (refresh token rotation).
This endpoint requires client authentication using either HTTP Basic authentication or client credentials in the request body.
Endpoint: POST /token
Authentication
Same as Get Access Token — HTTP Basic or request body credentials.
Request Parameters
Must be "refresh_token".
The refresh token obtained from a previous token exchange.
Your application's client identifier. Required if not using HTTP Basic authentication.
undefinedYour application's client secret. Required if not using HTTP Basic authentication.
undefinedResponse
A new access token (JWT) valid for 1 hour.
Always "Bearer".
A new refresh token. The previously used refresh token stops working 30 seconds later. Store this new token in place of the old one.
The new access token carries the same scope as the original authorization. Scope cannot be upgraded through token refresh — the user must re-authorize (via GET /authorize) to request a different scope.
Once this endpoint responds successfully, the old refresh token keeps working for only 30 seconds. You must durably persist the new refresh token before discarding the old one. If your application crashes, loses the response, or fails to save the token to your database, and does not retry within those 30 seconds:
- The old token is invalidated — it cannot be reused.
- The new token is lost — it was only ever returned in this response.
- The user must re-authorize (complete the full OAuth flow again) to restore access.
To mitigate this, persist the new token atomically (e.g. in a database transaction) before acknowledging success. If saving fails, repeat the refresh with the old token straight away — within the 30 seconds it returns a working refresh token again.
Concurrent refreshes with the same token are safe; refreshing with an outdated copy is not. See Refreshing from several instances.
The response does not include expires_in; the new access token is valid for 1 hour (its exp claim).
Error Responses
- 400 - Bad Request
invalid_request-refresh_tokenis missing
- 401 - Unauthorized
invalid_client- Client authentication failed
- 403 - Forbidden
invalid_grant- Refresh token not found, revoked, replaced more than 30 seconds ago, client mismatch, or wrong token type. A refresh token from two or more rotations back also revokes the user's authorization — see Refreshing from several instances
- 500 - Internal Server Error
server_error- The request could not be completed; retry it with the same refresh token
- 501 - Not Implemented
unsupported_grant_type-grant_typeis notauthorization_codeorrefresh_token
Examples
- REST
- Node.js SDK
- Python SDK
- PHP SDK
- .NET SDK
- Java SDK
- Go SDK
- Ruby SDK
POST /token
Authorization: Basic YmFzZTY0X2VuY29kZWRfY3JlZGVudGlhbHM=
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
{
"error": "invalid_grant",
"error_description": "Invalid refresh token"
}
// Token refresh is handled automatically by the SDK.
// Listen for the 'tokens' event to persist the new tokens whenever a refresh occurs.
client.on('tokens', (updatedTokens) => {
// Persist in your database or session store
session.tokens = updatedTokens;
});
# Token refresh is handled automatically by the SDK.
# Register a callback to persist the new tokens whenever a refresh occurs.
from mobiscroll_connect import TokenResponse
def persist_tokens(tokens: TokenResponse) -> None:
# Persist in your database or session store
session['access_token'] = tokens.access_token
session['refresh_token'] = tokens.refresh_token
client.on_tokens_refreshed(persist_tokens)
use Mobiscroll\Connect\TokenResponse;
// Token refresh is handled automatically by the SDK.
// Register a callback to persist the new tokens whenever a refresh occurs.
$client->onTokensRefreshed(function (TokenResponse $updatedTokens) {
// Persist in your database or session store
$_SESSION['tokens'] = $updatedTokens->toArray();
});
// Token refresh is handled automatically by the SDK.
// Register a callback to persist the new tokens whenever a refresh occurs.
client.OnTokensRefreshed(updatedTokens =>
{
// Persist in your database or session store
HttpContext.Session.SetString("access_token", updatedTokens.AccessToken);
HttpContext.Session.SetString("refresh_token", updatedTokens.RefreshToken ?? "");
});
// Token refresh is handled automatically by the SDK.
// Register a callback to persist the new tokens whenever a refresh occurs.
client.onTokensRefreshed(updatedTokens -> {
// Persist in your database or session store
httpSession.setAttribute("access_token", updatedTokens.getAccessToken());
httpSession.setAttribute("refresh_token", updatedTokens.getRefreshToken());
});
// Token refresh is handled automatically by the SDK.
// Register a callback to persist the new tokens whenever a refresh occurs.
client.OnTokensRefreshed(func(updated *mobiscroll.TokenResponse) {
// Persist in your database or session store
session.Set("access_token", updated.AccessToken)
session.Set("refresh_token", updated.RefreshToken)
})
# Token refresh is handled automatically by the SDK.
# Register a callback to persist the new tokens whenever a refresh occurs.
client.on_tokens_refreshed do |tokens|
# Persist in your database or session store
session[:access_token] = tokens.access_token
session[:refresh_token] = tokens.refresh_token if tokens.refresh_token
end
- Always replace the stored refresh token with the new one returned in the response
- Concurrent refresh attempts with the same token all succeed when they arrive within 30 seconds of the first one
- Presenting a replaced refresh token more than 30 seconds later is rejected with
invalid_grant; presenting one from two or more rotations back also revokes the user's authorization - Refresh tokens are managed server-side; they have no embedded expiry date
Get Connection Status
Returns the calendar accounts the user has connected, grouped by provider, with the consent and health state of each account. Use it to show the user which accounts are connected, to find the id to pass to POST /disconnect, and to find accounts that need to be reconnected.
This endpoint requires a valid access token in the Authorization: Bearer <access_token> header. Refresh tokens are not accepted.
Endpoint: GET /connection-status
Response
Connected accounts keyed by provider. The object always contains the google, microsoft, apple and caldav keys; a provider with no connected account has an empty array. Each account object contains:
Account identifier, usually the account's email address. Pass it as account to POST /disconnect to disconnect only this account.
Label to show for the account. Currently the same value as id.
The scopes the provider actually granted for this account, which are not necessarily the scopes Connect asked for. Empty for Apple and CalDAV, which authenticate with a username and app password. See Partial consent.
Whether the granted scopes cover calendar access at the scope of the access token used for this request. false means the account connected but withheld calendar access, so it can list no calendars until the user reconnects. null for Apple and CalDAV, and for accounts with no recorded scopes. See Partial consent.
Whether the stored provider credentials still work. reauth_required means the provider has rejected them and the account's calendars are not syncing; the user has to connect the account again. See Connection health.
ISO 8601 timestamp of the last change to syncState, or null if it has never changed.
true when your application has reached the connected-account limit of the free Develop tier. Accounts are counted across all users of your application. On other plans this is always false.
The connected-account limit of the free Develop tier. On other plans this is always 0.
Error Responses
- 401 - Unauthorized. The access token is missing, invalid, expired or revoked, or a refresh token was sent. The
errorfield describes the reason.
Examples
- REST
- Node.js SDK
- Python SDK
- PHP SDK
- .NET SDK
- Java SDK
- Go SDK
- Ruby SDK
GET /connection-status
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
{
"connections": {
"google": [
{
"id": "user@gmail.com",
"display": "user@gmail.com",
"grantedScopes": ["openid", "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/calendar"],
"calendarPermissionGranted": true,
"syncState": "active",
"syncStateUpdatedAt": null
}
],
"microsoft": [
{
"id": "user@outlook.com",
"display": "user@outlook.com",
"grantedScopes": ["openid", "profile", "https://graph.microsoft.com/Calendars.ReadWrite"],
"calendarPermissionGranted": true,
"syncState": "reauth_required",
"syncStateUpdatedAt": "2026-09-01T10:00:00.000Z"
}
],
"apple": [],
"caldav": []
},
"limitReached": false,
"limit": 0
}
const status = await client.auth.getConnectionStatus();
for (const account of status.connections.google) {
console.log(account.id, account.syncState, account.calendarPermissionGranted);
}
status = client.auth.get_connection_status()
for account in status.connections.get('google', []):
print(account.id, account.sync_state, account.calendar_permission_granted)
$status = $client->auth()->getConnectionStatus();
foreach ($status->connections['google'] ?? [] as $account) {
echo $account['id'] . ' ' . $account['syncState'] . "\n";
}
var status = await client.Auth.GetConnectionStatusAsync();
if (status.Connections.TryGetValue("google", out var googleAccounts))
{
foreach (var account in googleAccounts)
{
Console.WriteLine($"{account.Id} {account.SyncState} {account.CalendarPermissionGranted}");
}
}
import com.mobiscroll.connect.Provider;
import com.mobiscroll.connect.models.ConnectedAccount;
import com.mobiscroll.connect.models.ConnectionStatus;
import java.util.List;
ConnectionStatus status = client.auth().getConnectionStatus();
for (ConnectedAccount account : status.getConnections().getOrDefault(Provider.GOOGLE, List.of())) {
System.out.println(account.getId() + " " + account.getSyncState());
}
status, err := client.Auth().GetConnectionStatus(ctx)
if err != nil {
// handle error
return
}
for _, account := range status.Connections[mobiscroll.ProviderGoogle] {
fmt.Println(account.ID, account.SyncState)
}
status = client.auth.get_connection_status
(status.connections['google'] || []).each do |account|
puts "#{account.id} #{account.sync_state}"
end
- Accounts in the
reauth_requiredstate stay listed, so you can show them and prompt the user to reconnect - Disconnected accounts are not listed
- The SDKs return
limitReachedbut notlimit
Disconnect Account
Disconnects one calendar account of the user, or every account of one provider. Connect stops the webhook subscriptions of the account's calendars, deletes the stored provider credentials, and marks the account as disconnected: it no longer appears in the connection status and no longer counts toward your plan's connected-account limit.
Disconnect does not revoke the user's Connect access and refresh tokens. To invalidate those, use POST /revoke — see Disconnect vs Revoke.
This endpoint requires a valid access token in the Authorization: Bearer <access_token> header. Expired and revoked access tokens and refresh tokens are not accepted. The parameters are sent in the query string; the request body is empty.
Endpoint: POST /disconnect
Query Parameters
The provider of the account to disconnect: google, microsoft, apple or caldav.
The id of the account to disconnect, as returned by GET /connection-status (connections[provider][].id). Omit it to disconnect every account of the provider.
Response
Returns 200 OK when the account is disconnected. The request is idempotent: disconnecting an account that is not connected also returns 200.
{
"success": true
}
Error Responses
- 400 - Bad Request
Missing or invalid provider-provideris missing or is not one ofgoogle,microsoft,apple,caldavUser not found- this user has never authorized your application
- 401 - Unauthorized. The access token is missing, invalid, expired or revoked, or a refresh token was sent. The
errorfield describes the reason.
Examples
- REST
- Node.js SDK
- Python SDK
- PHP SDK
- .NET SDK
- Java SDK
- Go SDK
- Ruby SDK
POST /disconnect?provider=google&account=user@gmail.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
POST /disconnect?provider=microsoft
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
{
"success": true
}
{
"error": "Missing or invalid provider"
}
// Disconnect one account (its id from getConnectionStatus)
await client.auth.disconnect({ provider: 'google', account: 'user@gmail.com' });
// Disconnect every Microsoft account of the user
await client.auth.disconnect({ provider: 'microsoft' });
# Disconnect one account (its id from get_connection_status)
client.auth.disconnect('google', account='user@gmail.com')
# Disconnect every Microsoft account of the user
client.auth.disconnect('microsoft')
// Disconnect one account (its id from getConnectionStatus)
$client->auth()->disconnect(provider: 'google', account: 'user@gmail.com');
// Disconnect every Microsoft account of the user
$client->auth()->disconnect(provider: 'microsoft');
// Disconnect one account (its Id from GetConnectionStatusAsync)
await client.Auth.DisconnectAsync(new DisconnectParams
{
Provider = Provider.Google,
Account = "user@gmail.com",
});
// Disconnect every Microsoft account of the user
await client.Auth.DisconnectAsync(new DisconnectParams { Provider = Provider.Microsoft });
import com.mobiscroll.connect.Provider;
import com.mobiscroll.connect.models.DisconnectParams;
// Disconnect one account (its id from getConnectionStatus)
client.auth().disconnect(DisconnectParams.builder()
.provider(Provider.GOOGLE)
.account("user@gmail.com")
.build());
// Disconnect every Microsoft account of the user
client.auth().disconnect(DisconnectParams.builder().provider(Provider.MICROSOFT).build());
// Disconnect one account (its ID from GetConnectionStatus)
_, err := client.Auth().Disconnect(ctx, &mobiscroll.DisconnectParams{
Provider: mobiscroll.ProviderGoogle,
Account: "user@gmail.com",
})
// Disconnect every Microsoft account of the user
_, err = client.Auth().Disconnect(ctx, &mobiscroll.DisconnectParams{Provider: mobiscroll.ProviderMicrosoft})
# Disconnect one account (its id from get_connection_status)
client.auth.disconnect(provider: Mobiscroll::Connect::Provider::GOOGLE, account: 'user@gmail.com')
# Disconnect every Microsoft account of the user
client.auth.disconnect(provider: Mobiscroll::Connect::Provider::MICROSOFT)
- A calendar that another still-connected account of the same provider can also see (for example, a shared calendar) keeps its webhook subscription
- Deliveries already waiting for a retry can still arrive for up to about 27.6 hours
- The user's Connect access and refresh tokens stay valid. Use
POST /revoketo invalidate them - The grant at the provider is not revoked: your application stays listed in the user's Google or Microsoft account permissions until the user removes it there
- To connect the account again, send the user through
GET /authorize
Revoke Token
Revokes every access token and refresh token issued to the user for your application. Use it when the user signs out of your application, or to invalidate tokens that may have been exposed.
Revoke does not disconnect calendar accounts: they stay connected, and webhook deliveries for their calendars continue. To remove a calendar account, use POST /disconnect — see Disconnect vs Revoke.
This endpoint does not require authentication. The token itself is used to identify the user and client.
Endpoint: POST /revoke
Request Body
Either an access token or a refresh token. An expired access token is accepted too. The server decodes it to extract the user and client identifiers, then revokes all active tokens for that user/client combination.
Response
Returns 200 OK even if the token was already invalid or revoked — this prevents leaking information about token state.
{
"success": true
}
Error Responses
- 400 - Bad Request.
tokenfield is missing or not a string.
{
"error": "invalid_request",
"error_description": "token is required"
}
Examples
- REST
- Node.js SDK
- Python SDK
- PHP SDK
- .NET SDK
- Java SDK
- Go SDK
- Ruby SDK
POST /revoke
Content-Type: application/json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
{
"success": true
}
// Revoke the stored refresh token (else the access token) and clear the client's credentials
await client.auth.revoke();
// Or revoke a token you stored yourself
await client.auth.revoke(storedRefreshToken);
# Revoke the stored refresh token (else the access token) and clear the client's credentials
client.auth.revoke()
# Or revoke a token you stored yourself
client.auth.revoke(stored_refresh_token)
# On AsyncMobiscrollConnectClient: await client.auth.revoke()
// Revoke the stored refresh token (else the access token) and clear the client's credentials
$client->auth()->revoke();
// Or revoke a token you stored yourself
$client->auth()->revoke($storedRefreshToken);
// Revoke the stored refresh token (else the access token) and clear the client's credentials
await client.Auth.RevokeAsync();
// Or revoke a token you stored yourself
await client.Auth.RevokeAsync(storedRefreshToken);
// Revoke the stored refresh token (else the access token) and clear the client's credentials
client.auth().revoke();
// Or revoke a token you stored yourself
client.auth().revoke(storedRefreshToken);
// Pass an empty token to revoke the stored refresh token (else the access token)
// and clear the client's credentials
_, err := client.Auth().Revoke(ctx, "")
// Or revoke a token you stored yourself
_, err = client.Auth().Revoke(ctx, storedRefreshToken)
# Revoke the stored refresh token (else the access token) and clear the client's credentials
client.auth.revoke
# Or revoke a token you stored yourself
client.auth.revoke(stored_refresh_token)
- Both access tokens and refresh tokens are accepted, including an expired access token — the endpoint revokes the entire token family regardless of which token type is provided
- The response is always
200with{ "success": true }to avoid leaking information about token validity - Called without a token, the SDKs revoke their stored refresh token (or, if there is none, the stored access token) and clear their stored credentials. When you pass a token, the stored credentials are kept. With no token passed and none stored, the SDKs raise a validation error without sending a request
- After revocation, the user must complete the OAuth authorization flow again to obtain new tokens. Their calendar accounts are still connected, so re-authorizing the same
user_idmakes them available again
Disconnect vs Revoke
Disconnect and revoke remove different things, and neither does the other's job.
Disconnect — POST /disconnect | Revoke — POST /revoke | |
|---|---|---|
| Removes | One calendar account, or every account of one provider: its stored provider credentials and the webhook subscriptions of its calendars | Every Connect access token and refresh token of the user |
| Keeps working | The user's Connect tokens and their other connected accounts. The grant at Google or Microsoft stays in place until the user removes it there | All connected calendar accounts and their webhook deliveries. The accounts still count toward your plan's connected-account limit |
| Authentication | A valid access token | The token to revoke; an expired access token or a refresh token is accepted |
| Use it when | The user removes a calendar account from your application | The user signs out of your application, or tokens may have been exposed |
To remove a user from your application completely, first call disconnect for each provider that has connected accounts in the connection status, then call revoke. Disconnect needs a valid access token, so after revoke it is rejected with 401.