Skip to main content

PHP SDK

The Mobiscroll Connect PHP SDK provides a convenient way to integrate Mobiscroll Connect in PHP backends.

Setup​

Install the package using Composer:

composer require mobiscroll/connect-php

The SDK requires PHP 8.1 or later and the sodium extension (ext-sodium), which is bundled with PHP since 7.2. Composer installs the other dependencies, including psr/simple-cache, automatically.

Client Initialization​

To use the SDK, initialize MobiscrollConnectClient with your client credentials.

Class: Mobiscroll\Connect\MobiscrollConnectClient

constructorMobiscrollConnectClient::__construct

Constructor arguments.

clientIdstring

Your Client ID obtained from the Mobiscroll Connect dashboard.

clientSecretstring

Your Client Secret obtained from the Mobiscroll Connect dashboard.

redirectUristring

Your application's redirect URI that matches the one configured in the Mobiscroll Connect dashboard.

webhookPublicKey?string

Optional. A pinned whpk_ public key for webhooks()->verifyWebhook. It is used only when the keys endpoint cannot be reached, and it stops verifying once Mobiscroll retires that key, so replace it on every key rotation. Leave it unset unless your handler cannot make outbound requests.

webhookKeyCache?Psr\SimpleCache\CacheInterface

Optional. A PSR-16 cache, such as your framework's cache, APCu or Redis, that shares the fetched webhook keys and fetch timestamps across requests and workers. Under PHP-FPM nothing stays in memory between requests, so without it each request fetches the keys once, on its first verification. See webhooks()->verifyWebhook.

Usage:

<?php

use Mobiscroll\Connect\MobiscrollConnectClient;

$client = new MobiscrollConnectClient(
clientId: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_CLIENT_SECRET',
redirectUri: 'YOUR_REDIRECT_URI'
);

Methods​

setCredentials​

Sets the access token for the client. This is required before making any API calls that require authentication.

Method: client->auth()->setCredentials(tokens)

tokensTokenResponse

The token response object returned by client->auth()->getToken(code).

onTokensRefreshed​

Registers a callback to be invoked whenever the SDK automatically refreshes the access token. Use this to persist the updated tokens so they survive future requests.

Method: client->onTokensRefreshed(callback)

callbackcallable(TokenResponse): void

A callable that receives the updated TokenResponse after a successful automatic token refresh.

Token Refresh​

The PHP SDK handles token refresh automatically. When any API call returns a 401 Unauthorized response and the client has a refresh_token stored, the SDK will silently exchange it for a new access token and retry the original request — with no action required from your application.

When the refresh succeeds, the SDK invokes your onTokensRefreshed callback with the updated TokenResponse. You must register this callback and persist the new tokens, otherwise they will be lost between requests.

use Mobiscroll\Connect\TokenResponse;

$client->onTokensRefreshed(function (TokenResponse $updatedTokens) {
// Persist $updatedTokens in your database or session store
// so the new access_token and refresh_token survive future requests
$_SESSION['tokens'] = $updatedTokens->toArray();
});

If the refresh token itself is invalid or has been revoked, the SDK throws an AuthenticationError and the user must re-authorize.

Running more than one instance​

The PHP SDK does not deduplicate refreshes. Under PHP-FPM every request runs in its own process, so there is no other in-process call to share a refresh with — concurrent requests for the same user each refresh on their own.

Connect accepts several requests refreshing the same token at once, but a request that refreshes with a copy that is two or more refreshes out of date revokes the user's authorization — see Refreshing from several instances. So:

  • persist the tokens from your onTokensRefreshed callback to storage that every request reads, such as your database, and
  • at the start of each request, call setCredentials with the current tokens from that storage.

Error Handling​

All SDK methods throw exceptions that extend Mobiscroll\Connect\Exceptions\MobiscrollConnectException. You can catch the base exception or any of the specific subclasses.

ExceptionHTTP StatusgetCodeString()
AuthenticationError401, 403AUTHENTICATION_ERROR
CalendarPermissionError403CALENDAR_PERMISSION_REQUIRED
ValidationError400, 422VALIDATION_ERROR
NotFoundError404NOT_FOUND_ERROR
RateLimitError429RATE_LIMIT_ERROR
ServerError5xxSERVER_ERROR
NetworkError—NETWORK_ERROR
WebhookVerificationError—WEBHOOK_VERIFICATION_ERROR

ValidationError exposes a getDetails() method that returns field-level validation errors. RateLimitError exposes getRetryAfter() (seconds) and ServerError exposes getStatusCode(). WebhookVerificationError is thrown by webhooks()->verifyWebhook and WebhookVerifier::verifyWebhookSignature, not by API calls; its getReason() method says why the delivery was rejected.

use Mobiscroll\Connect\Exceptions\{
AuthenticationError,
ValidationError,
RateLimitError,
MobiscrollConnectException,
};

try {
$events = $client->events()->list(['start' => new DateTime('2024-01-01')]);
} catch (AuthenticationError $e) {
// Token expired or invalid — refresh manually or re-authorize the user
} catch (ValidationError $e) {
// Invalid request parameters
$details = $e->getDetails();
} catch (RateLimitError $e) {
$retryAfter = $e->getRetryAfter(); // seconds
} catch (MobiscrollConnectException $e) {
// Catch-all for any other SDK error
}

Auth API​

The client->auth() resource handles the OAuth authorization flow, including generating authorization URLs, exchanging codes for tokens, managing connection status, and disconnecting providers.

Each account returned by getConnectionStatus reports grantedScopes and calendarPermissionGranted. A false flag means the account connected but withheld calendar access on the provider's consent screen, so it can list no calendars until the user reconnects — see Partial consent. Each account also reports syncState: reauth_required means the provider has since rejected the stored credentials and the account has stopped syncing — a different question from calendarPermissionGranted, which only records what was agreed at connect time. See Connection health.

When no connected account has calendar access, calendar and event calls raise CalendarPermissionError — getAccounts() names the accounts that must reconnect. It subclasses AuthenticationError, so existing handlers keep working.

To localize the Connect pages, pass an optional lng to generateAuthUrl, e.g. generateAuthUrl(userId: ..., lng: 'es'). When omitted, the UI falls back to the browser's Accept-Language header, then English; Arabic, Hebrew and Persian render right-to-left. See Supported languages for the languages Connect supports.


auth()->generateAuthUrl​

Builds the URL that starts the authorization flow. Redirect the user to it: once they connect their calendars, Connect redirects them back to your redirect URI with an authorization code. The URL is built locally, without an API request. See GET /authorize.

Method: client->auth()->generateAuthUrl(userId, scope, state, lng)

Returns: string

userIdstring

Your identifier for the user. scope, state and lng are optional; pass them as named arguments.

Usage:

// Generate the authorization URL
$authUrl = $client->auth()->generateAuthUrl(
userId: 'user-456',
// Optional parameters:
// scope: 'read-write',
// state: 'xyz789',
// lng: 'es', // localize the Connect pages
);

// Redirect the user to $authUrl
header('Location: ' . $authUrl);

auth()->getToken​

Exchanges the authorization code from your redirect URI for an access token and a refresh token. The PHP SDK does not store the returned tokens on the client — pass them to setCredentials before calling other methods. See POST /token.

Method: client->auth()->getToken(code)

Returns: TokenResponse

codestring

The authorization code received on your redirect URI.

Usage:

// 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);

Calendars API​

The client->calendars() resource allows you to list available calendars from all connected providers (Google, Outlook, etc.). It corresponds to the /calendars endpoints.


calendars()->list​

Lists the calendars of every account the user has connected. See GET /calendars.

Method: client->calendars()->list()

Returns: array

Usage:

$calendars = $client->calendars()->list();

Events API​

The client->events() resource provides methods to create, read, update, and delete calendar events across all connected accounts. It corresponds to the /events endpoints.


events()->list​

Lists events from the user's calendars, one page at a time. When the response contains a nextPageToken, pass it to the next call to get the following page. See GET /events.

Method: client->events()->list(params)

Returns: array — events, and nextPageToken when more pages are available.

paramsarray

All keys are optional: start, end, calendarIds (calendar IDs grouped by provider), pageSize, nextPageToken and singleEvents.

Usage:

// Fetch initial events with date range filter
$response = $client->events()->list([
'pageSize' => 50,
'start' => '2025-10-01T00:00:00Z',
'end' => '2025-10-31T23:59:59Z',
]);
$events = $response['events'];

// Load more events using nextPageToken
$nextResponse = $client->events()->list([
'pageSize' => 50,
'nextPageToken' => $response['nextPageToken'] ?? null,
]);

// Filter by specific calendars
$filteredEvents = $client->events()->list([
'pageSize' => 25,
'calendarIds' => ['google' => ['personal@gmail.com', 'work@company.com']],
]);

// Get recurring event series masters (not expanded)
$masters = $client->events()->list([
'pageSize' => 50,
'singleEvents' => false,
]);

events()->create​

Creates an event in the given calendar. Set recurrence to create a recurring series. See POST /event.

Method: client->events()->create(event)

Returns: CalendarEvent

eventarray

The event fields, using the API field names. provider, calendarId, title, start and end are required. Optional fields include description, location, allDay and recurrence.

Usage:

// Create a simple event
$event = $client->events()->create([
'provider' => 'google',
'calendarId' => 'primary',
'title' => 'Team Meeting',
'description' => 'Discuss project updates',
'start' => '2025-11-01T10:00:00Z',
'end' => '2025-11-01T11:00:00Z',
'location' => 'Conference Room A',
]);

// Create a recurring event
$recurringEvent = $client->events()->create([
'provider' => 'microsoft',
'calendarId' => 'AAMkAGVmMDEz...',
'title' => 'Weekly Standup',
'start' => '2025-11-01T09:00:00Z',
'end' => '2025-11-01T09:30:00Z',
'allDay' => false,
'recurrence' => [
'frequency' => 'WEEKLY',
'interval' => 1,
'count' => 10,
'byDay' => ['MO', 'WE', 'FR'],
],
]);

// Create an all-day event
$allDayEvent = $client->events()->create([
'provider' => 'apple',
'calendarId' => 'https://caldav.icloud.com/.../calendars/...',
'title' => 'Conference',
'start' => '2025-11-15T00:00:00Z',
'end' => '2025-11-16T00:00:00Z',
'allDay' => true,
'description' => 'Annual tech conference',
]);

events()->update​

Updates an existing event. For an occurrence of a recurring series, set recurringEventId, and use updateMode to choose which occurrences change: this, following or all. See PUT /event.

Method: client->events()->update(event)

Returns: CalendarEvent

eventarray

provider, calendarId and eventId, plus the fields you want to change. Add recurringEventId and updateMode for recurring events.

Usage:

// Update a simple event
$updatedEvent = $client->events()->update([
'provider' => 'google',
'calendarId' => 'primary',
'eventId' => 'event123abc',
'title' => 'Updated Team Meeting',
'start' => '2025-11-01T14:00:00Z',
'end' => '2025-11-01T15:00:00Z',
]);

// Update a single instance of a recurring event
$updatedInstance = $client->events()->update([
'provider' => 'microsoft',
'eventId' => 'instance456',
'recurringEventId' => 'series123',
'calendarId' => 'AAMkAGVmMDEz...',
'updateMode' => 'this',
'title' => 'Standup - Special Topic Today',
'start' => '2025-11-01T09:00:00Z',
'end' => '2025-11-01T10:00:00Z',
]);

// Update all future occurrences
$updatedFuture = $client->events()->update([
'provider' => 'apple',
'eventId' => 'recurring-event-id',
'calendarId' => 'https://caldav.icloud.com/.../calendars/...',
'updateMode' => 'following',
'location' => 'New Conference Room B',
]);

events()->delete​

Deletes an event. For an occurrence of a recurring series, set recurringEventId, and use deleteMode to choose which occurrences are deleted: this, following or all. See DELETE /event.

Method: client->events()->delete(params)

Returns: array

paramsarray

provider, calendarId and eventId are required. recurringEventId and deleteMode are optional.

Usage:

// Delete a simple event
$client->events()->delete([
'provider' => 'google',
'calendarId' => 'primary',
'eventId' => 'event123abc',
]);

// Delete a single instance of a recurring event
$client->events()->delete([
'provider' => 'microsoft',
'calendarId' => 'AAMkAGVmMDEz...',
'eventId' => 'instance456',
'recurringEventId' => 'series123',
'deleteMode' => 'this',
]);

// Delete entire recurring series
$client->events()->delete([
'provider' => 'apple',
'calendarId' => 'https://caldav.icloud.com/.../calendars/...',
'eventId' => 'recurring-event-id',
'deleteMode' => 'all',
]);

Webhooks API​

The client->webhooks() resource lets you subscribe a calendar to change notifications, unsubscribe an existing channel, and verify the deliveries Connect sends to your webhook URL. Subscribing and unsubscribing correspond to the /subscribe-webhook and /unsubscribe-webhook endpoints. Connect subscribes and renews channels automatically when a calendar is connected — those two methods are for managing a specific channel explicitly.


webhooks()->subscribeWebhook​

Subscribes a calendar to change notifications. See POST /subscribe-webhook.

Method: client->webhooks()->subscribeWebhook(params)

Returns: SubscribeWebhookResponse

paramsarray

provider and calendarId are required. channelId is optional and generated by the server when omitted. expiration is an optional Unix timestamp in milliseconds.

Usage:

$subscription = $client->webhooks()->subscribeWebhook([
'provider' => 'google',
'calendarId' => 'work@company.com',
'channelId' => 'my-channel-123',
]);

webhooks()->unsubscribeWebhook​

Removes an existing webhook subscription. See POST /unsubscribe-webhook.

Method: client->webhooks()->unsubscribeWebhook(params)

Returns: UnsubscribeWebhookResponse

paramsarray

provider and channelId are required. resourceId is the provider resource ID returned by subscribeWebhook, where the provider uses one.

Usage:

$client->webhooks()->unsubscribeWebhook([
'provider' => 'google',
'channelId' => 'my-channel-123',
'resourceId' => 'resource-abc',
]);

webhooks()->verifyWebhook​

Checks that a delivery to your webhook URL came from Mobiscroll Connect and was not changed on the way, then returns it parsed. It verifies the Ed25519 signature and rejects a webhook-timestamp more than 5 minutes from your server's clock — see Verifying deliveries. Available from version 1.6.0.

Method: client->webhooks()->verifyWebhook(payload, headers)

Returns: WebhookDelivery

Throws: Mobiscroll\Connect\Exceptions\WebhookVerificationError when the delivery is not genuine. Respond with a 4xx and do not process it.

payloadstring

The raw request body, exactly as received. Do not pass a decoded array: re-encoding changes the bytes, and every signature check fails.

headersarray|MessageInterface

The request headers, in any casing, as an array or a PSR-7 message (Psr\Http\Message\MessageInterface). Must include webhook-id, webhook-timestamp and webhook-signature.

The SDK fetches the public keys from /.well-known/webhook-keys on the first delivery, not when the client is created, and caches them for the whole process, shared by every client instance. It refreshes them as the endpoint's Cache-Control header allows. When no signature matches, it fetches the keys again before rejecting the delivery, at most once a minute, so a key rotation needs no action from you. If a refresh fails, it keeps using the keys it already has. The pinned webhookPublicKey is used only when no keys could be fetched at all.

Under PHP-FPM every request is its own process, so the in-memory cache lives for one request and each request fetches the keys on its first verification. Pass a PSR-16 cache as the webhookKeyCache option to share the keys and the fetch timestamps across requests and workers. Long-running workers such as RoadRunner, Swoole and queue workers keep the keys in memory either way.

$e->getReason() returns one of the string values below. Compare it with the constant on WebhookVerificationError:

ConstantgetReason() valueMeaning
MISSING_HEADERSmissing_headerswebhook-id, webhook-timestamp or webhook-signature is missing
INVALID_TIMESTAMPinvalid_timestampwebhook-timestamp is not a Unix time in seconds
TIMESTAMP_OUT_OF_TOLERANCEtimestamp_out_of_tolerancewebhook-timestamp is more than 5 minutes from your server's clock
NO_PUBLIC_KEYSno_public_keysNo keys could be fetched and no webhookPublicKey is configured
NO_MATCHING_SIGNATUREno_matching_signatureNo signature matches any key — a forged or modified delivery
INVALID_PAYLOADinvalid_payloadThe body is not a JSON object

Usage:

use Mobiscroll\Connect\Exceptions\WebhookVerificationError;

// webhook.php
try {
$delivery = $client->webhooks()->verifyWebhook(file_get_contents('php://input'), getallheaders());
} catch (WebhookVerificationError $e) {
// 503 lets Connect retry when the keys could not be loaded; 401 is final.
http_response_code($e->getReason() === WebhookVerificationError::NO_PUBLIC_KEYS ? 503 : 401);
exit;
}

http_response_code(204);
handleDelivery($delivery); // $delivery->userId, $delivery->calendarId, $delivery->events, ...

In Laravel or Symfony, pass $request->getContent() and $request->headers->all().

To share the keys across PHP-FPM requests, pass a PSR-16 cache when you create the client:

$client = new MobiscrollConnectClient(
clientId: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_CLIENT_SECRET',
redirectUri: 'YOUR_REDIRECT_URI',
webhookKeyCache: $cache, // any Psr\SimpleCache\CacheInterface
);

WebhookVerifier::verifyWebhookSignature​

Verifies a delivery against public keys you supply, with no network request. Use it in handlers that cannot make outbound requests; otherwise prefer webhooks()->verifyWebhook, which keeps the keys current for you. Available from version 1.6.0.

Method: Mobiscroll\Connect\WebhookVerifier::verifyWebhookSignature(payload, headers, publicKeys, toleranceSeconds, now) (static)

Returns: void

Throws: WebhookVerificationError, with the same getReason() values as webhooks()->verifyWebhook, except INVALID_PAYLOAD: this method does not parse the body. NO_PUBLIC_KEYS is thrown when none of the keys you pass is a valid whpk_ key.

payloadstring

The raw request body, exactly as received.

headersarray|MessageInterface

The request headers, in any casing, as an array or a PSR-7 message.

publicKeysarray

A list of whpk_ public key strings. The delivery is genuine if any signature verifies against any of them.

toleranceSecondsint

Optional. The allowed webhook-timestamp age in either direction, in seconds.

Default: 300
now?int

Optional. Overrides the current Unix time, in seconds. Defaults to the system clock.

Usage:

use Mobiscroll\Connect\WebhookVerifier;

$body = file_get_contents('php://input');

WebhookVerifier::verifyWebhookSignature(
$body,
getallheaders(),
[getenv('MOBISCROLL_WEBHOOK_PUBLIC_KEY')],
);
$delivery = json_decode($body, true);

A pinned key stops verifying when Mobiscroll retires it, so you must replace it on every key rotation.