Skip to main content

.NET SDK

The Mobiscroll Connect .NET SDK provides a convenient way to integrate Mobiscroll Connect in .NET backend applications. It targets .NET 8 and runs on .NET Core, making it compatible with Linux, macOS, and Windows.

Setup​

Install the package using NuGet:

dotnet add package Mobiscroll.Connect

The package depends on NSec.Cryptography (25.4.x), which ships a native libsodium library and checks webhook signatures. NuGet installs it automatically with the package.

Client Initialization​

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

Class: Mobiscroll.Connect.MobiscrollConnectClient

constructorMobiscrollConnectClient

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.

WebhookPublicKeystring?

Optional. A pinned whpk_ public key for Webhooks.VerifyWebhookAsync. 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. Set it on MobiscrollConnectConfig, which you pass to the MobiscrollConnectClient(MobiscrollConnectConfig config, ...) constructor or configure in AddMobiscrollConnect; the three-argument constructor does not take it.

Usage:

using Mobiscroll.Connect;

var client = new MobiscrollConnectClient(
clientId: "YOUR_CLIENT_ID",
clientSecret: "YOUR_CLIENT_SECRET",
redirectUri: "YOUR_REDIRECT_URI"
);

To pin a webhook public key, configure MobiscrollConnectConfig instead. With ASP.NET Core dependency injection:

using Mobiscroll.Connect.DependencyInjection;

builder.Services.AddMobiscrollConnect(options =>
{
options.ClientId = builder.Configuration["Mobiscroll:ClientId"]!;
options.ClientSecret = builder.Configuration["Mobiscroll:ClientSecret"]!;
options.RedirectUri = builder.Configuration["Mobiscroll:RedirectUri"]!;
options.WebhookPublicKey = builder.Configuration["Mobiscroll:WebhookPublicKey"]; // whpk_...
});

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.GetTokenAsync(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)

callbackAction<TokenResponse>

An action that receives the updated TokenResponse after a successful automatic token refresh.

Token Refresh​

The .NET 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.

client.OnTokensRefreshed(updatedTokens =>
{
// Persist updatedTokens in your database or session store
// so the new access_token and refresh_token survive future requests
HttpContext.Session.SetString("access_token", updatedTokens.AccessToken);
HttpContext.Session.SetString("refresh_token", updatedTokens.RefreshToken ?? "");
});

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

Running more than one instance​

Concurrent calls on the same client that hit an expired token wait for one shared refresh. That covers one client object in one process. It does not reach across processes: separate containers, cluster workers or serverless invocations each refresh on their own, from the tokens they hold in memory.

Connect accepts several processes refreshing the same token at once, but a process 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 instance reads, and
  • when a process starts a job or handles a request, call SetCredentials with the current tokens from that storage instead of relying on the copy it loaded at startup.

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 StatusCodeString
AuthenticationException401, 403AUTHENTICATION_ERROR
CalendarPermissionException403CALENDAR_PERMISSION_REQUIRED
ValidationException400, 422VALIDATION_ERROR
NotFoundException404NOT_FOUND_ERROR
RateLimitException429RATE_LIMIT_ERROR
ServerException5xxSERVER_ERROR
NetworkException—NETWORK_ERROR
WebhookVerificationException—WEBHOOK_VERIFICATION_ERROR

ValidationException exposes a Details property with field-level validation errors. RateLimitException exposes RetryAfter (seconds) and ServerException exposes StatusCode. WebhookVerificationException is thrown by Webhooks.VerifyWebhookAsync and WebhookVerifier.VerifyWebhookSignature, not by API calls; its Reason (a WebhookVerificationReason value) says why the delivery was rejected.

using Mobiscroll.Connect.Exceptions;

try
{
var response = await client.Events.ListAsync(new EventListParams
{
Start = DateTime.UtcNow,
End = DateTime.UtcNow.AddMonths(1)
});
}
catch (AuthenticationException)
{
// Token expired and refresh failed — re-authorize the user
}
catch (ValidationException ex)
{
// Invalid request parameters
var details = ex.Details;
}
catch (RateLimitException ex)
{
var retryAfter = ex.RetryAfter; // seconds
}
catch (MobiscrollConnectException)
{
// Catch-all for any other SDK error
}

Auth API​

The examples below assume using Mobiscroll.Connect; (the client and the Provider enum) and using Mobiscroll.Connect.Models; (the parameter and response classes).

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 GetConnectionStatusAsync 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 CalendarPermissionException — Accounts names the accounts that must reconnect. It subclasses AuthenticationException, so existing handlers keep working.

To localize the Connect pages, pass an optional Lng to GenerateAuthUrl, e.g. new AuthorizeParams { 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(AuthorizeParams authParams)

Returns: string

authParamsAuthorizeParams

UserId is required: your identifier for the user. Scope, State and Lng are optional.

Usage:

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

// Redirect the user to authUrl
// return Redirect(authUrl);

Auth.GetTokenAsync​

Exchanges the authorization code from your redirect URI for an access token and a refresh token. The client stores the returned tokens and uses them for the calls that follow. See POST /token.

Method: client.Auth.GetTokenAsync(string code, CancellationToken ct = default)

Returns: Task<TokenResponse>

codestring

The authorization code received on your redirect URI.

Usage:

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

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.ListAsync​

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

Method: client.Calendars.ListAsync(CancellationToken ct = default)

Returns: Task<IReadOnlyList<Calendar>>

Usage:

var calendars = await client.Calendars.ListAsync();

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.ListAsync​

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.ListAsync(EventListParams? listParams = null, CancellationToken ct = default)

Returns: Task<EventsListResponse>

listParamsEventListParams

All properties are optional: Start, End, CalendarIds (calendar IDs grouped by provider), PageSize, NextPageToken and SingleEvents.

Usage:

// Fetch initial events with date range filter
var response = await client.Events.ListAsync(new EventListParams
{
PageSize = 50,
Start = new DateTime(2025, 10, 1, 0, 0, 0, DateTimeKind.Utc),
End = new DateTime(2025, 10, 31, 23, 59, 59, DateTimeKind.Utc),
});

// Load more events using NextPageToken
var nextResponse = await client.Events.ListAsync(new EventListParams
{
PageSize = 50,
NextPageToken = response.NextPageToken,
});

// Filter by specific calendars
var filteredEvents = await client.Events.ListAsync(new EventListParams
{
PageSize = 25,
CalendarIds = new Dictionary<string, List<string>>
{
{ "google", new List<string> { "personal@gmail.com", "work@company.com" } }
},
});

// Get recurring event series masters (not expanded)
var masters = await client.Events.ListAsync(new EventListParams
{
PageSize = 50,
SingleEvents = false,
});

Events.CreateAsync​

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

Method: client.Events.CreateAsync(EventCreateData data, CancellationToken ct = default)

Returns: Task<CalendarEvent>

Provider, CalendarId, Title, Start and End are required. Optional properties include Description, Location, AllDay and Recurrence. Pass Start and End as UTC values (DateTimeKind.Utc).

Usage:

// Create a simple event
var newEvent = await client.Events.CreateAsync(new EventCreateData
{
Provider = Provider.Google,
CalendarId = "primary",
Title = "Team Meeting",
Description = "Discuss project updates",
Start = new DateTime(2025, 11, 1, 10, 0, 0, DateTimeKind.Utc),
End = new DateTime(2025, 11, 1, 11, 0, 0, DateTimeKind.Utc),
Location = "Conference Room A",
});

// Create a recurring event
var recurringEvent = await client.Events.CreateAsync(new EventCreateData
{
Provider = Provider.Microsoft,
CalendarId = "AAMkAGVmMDEz...",
Title = "Weekly Standup",
Start = new DateTime(2025, 11, 1, 9, 0, 0, DateTimeKind.Utc),
End = new DateTime(2025, 11, 1, 9, 30, 0, DateTimeKind.Utc),
AllDay = false,
Recurrence = new RecurrenceRule
{
Frequency = "WEEKLY",
Interval = 1,
Count = 10,
ByDay = new List<string> { "MO", "WE", "FR" },
},
});

// Create an all-day event
var allDayEvent = await client.Events.CreateAsync(new EventCreateData
{
Provider = Provider.Apple,
CalendarId = "https://caldav.icloud.com/.../calendars/...",
Title = "Conference",
Start = new DateTime(2025, 11, 15, 0, 0, 0, DateTimeKind.Utc),
End = new DateTime(2025, 11, 16, 0, 0, 0, DateTimeKind.Utc),
AllDay = true,
Description = "Annual tech conference",
});

Events.UpdateAsync​

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.UpdateAsync(EventUpdateData data, CancellationToken ct = default)

Returns: Task<CalendarEvent>

Provider, CalendarId and EventId are required, plus RecurringEventId and UpdateMode for recurring events. EventUpdateData extends EventCreateData, whose Title, Start and End are not nullable and are always sent — set them to the values the event should keep, even when you only change other properties.

Usage:

// Update a simple event
var updatedEvent = await client.Events.UpdateAsync(new EventUpdateData
{
Provider = Provider.Google,
CalendarId = "primary",
EventId = "event123abc",
Title = "Updated Team Meeting",
Start = new DateTime(2025, 11, 1, 14, 0, 0, DateTimeKind.Utc),
End = new DateTime(2025, 11, 1, 15, 0, 0, DateTimeKind.Utc),
});

// Update a single instance of a recurring event
var updatedInstance = await client.Events.UpdateAsync(new EventUpdateData
{
Provider = Provider.Microsoft,
EventId = "instance456",
RecurringEventId = "series123",
CalendarId = "AAMkAGVmMDEz...",
UpdateMode = "this",
Title = "Standup - Special Topic Today",
Start = new DateTime(2025, 11, 1, 9, 0, 0, DateTimeKind.Utc),
End = new DateTime(2025, 11, 1, 10, 0, 0, DateTimeKind.Utc),
});

// Update all future occurrences
var updatedFuture = await client.Events.UpdateAsync(new EventUpdateData
{
Provider = Provider.Apple,
EventId = "recurring-event-id",
CalendarId = "https://caldav.icloud.com/.../calendars/...",
UpdateMode = "following",
Location = "New Conference Room B",
// Title, Start and End are always sent, so pass the event's current values
Title = "Weekly Standup",
Start = new DateTime(2025, 11, 1, 9, 0, 0, DateTimeKind.Utc),
End = new DateTime(2025, 11, 1, 9, 30, 0, DateTimeKind.Utc),
});

Events.DeleteAsync​

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.DeleteAsync(EventDeleteParams deleteParams, CancellationToken ct = default)

Returns: Task

deleteParamsEventDeleteParams

Provider, CalendarId and EventId are required. RecurringEventId and DeleteMode are optional.

Usage:

// Delete a simple event
await client.Events.DeleteAsync(new EventDeleteParams
{
Provider = Provider.Google,
CalendarId = "primary",
EventId = "event123abc",
});

// Delete a single instance of a recurring event
await client.Events.DeleteAsync(new EventDeleteParams
{
Provider = Provider.Microsoft,
CalendarId = "AAMkAGVmMDEz...",
EventId = "instance456",
RecurringEventId = "series123",
DeleteMode = "this",
});

// Delete entire recurring series
await client.Events.DeleteAsync(new EventDeleteParams
{
Provider = 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.SubscribeWebhookAsync​

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

Method: client.Webhooks.SubscribeWebhookAsync(WebhookSubscribeData data, CancellationToken ct = default)

Returns: Task<WebhookSubscribeResponse>

Provider and CalendarId are required. ChannelId is optional and generated by the server when omitted. Expiration is an optional Unix timestamp in milliseconds.

Usage:

var subscription = await client.Webhooks.SubscribeWebhookAsync(new WebhookSubscribeData
{
Provider = Provider.Google,
CalendarId = "work@company.com",
ChannelId = "my-channel-123",
});

Webhooks.UnsubscribeWebhookAsync​

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

Method: client.Webhooks.UnsubscribeWebhookAsync(WebhookUnsubscribeData data, CancellationToken ct = default)

Returns: Task<WebhookUnsubscribeResponse>

Provider and ChannelId are required. ResourceId is the provider resource ID returned by SubscribeWebhookAsync, where the provider uses one.

Usage:

await client.Webhooks.UnsubscribeWebhookAsync(new WebhookUnsubscribeData
{
Provider = Provider.Google,
ChannelId = "my-channel-123",
ResourceId = "resource-abc",
});

Webhooks.VerifyWebhookAsync​

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.VerifyWebhookAsync(payload, headers, CancellationToken ct = default)

Returns: Task<WebhookDelivery>

Throws: WebhookVerificationException when the delivery is not genuine. Respond with a 4xx and do not process it.

payloadReadOnlyMemory<byte> | string

The raw request body, exactly as received. A byte[] converts to ReadOnlyMemory<byte> implicitly. Do not pass JSON that was deserialized and serialized again, and do not rely on model binding: both change the bytes, and every signature check fails.

headersIEnumerable<KeyValuePair<string, string>> | IEnumerable<KeyValuePair<string, TValues>>

The request headers, in any casing. Must include webhook-id, webhook-timestamp and webhook-signature. Pass a dictionary of header names to values, or any multi-valued collection whose values are IEnumerable<string?>, such as ASP.NET Core's IHeaderDictionary or HttpHeaders; the first value of each header is used.

ctCancellationToken

Cancels waiting for the public keys. A key fetch shared with other callers keeps running.

Default: default

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.

exception.Reason is a WebhookVerificationReason enum value. Compare it against these members:

ReasonMeaning
MissingHeaderswebhook-id, webhook-timestamp or webhook-signature is missing or empty
InvalidTimestampwebhook-timestamp is not a whole number of Unix seconds
TimestampOutOfTolerancewebhook-timestamp is more than 5 minutes from your server's clock
NoPublicKeysNo valid key could be fetched and no valid WebhookPublicKey is configured
NoMatchingSignatureNo signature matches any key — a forged or modified delivery
InvalidPayloadThe body is not the raw delivery, or is not valid delivery JSON

Usage:

using Mobiscroll.Connect.Exceptions;
using Mobiscroll.Connect.Models;

app.MapPost("/webhooks/mobiscroll", async (HttpRequest request, MobiscrollConnectClient client) =>
{
using var body = new MemoryStream();
await request.Body.CopyToAsync(body);

WebhookDelivery delivery;
try
{
delivery = await client.Webhooks.VerifyWebhookAsync(body.ToArray(), request.Headers);
}
catch (WebhookVerificationException e)
{
return e.Reason == WebhookVerificationReason.NoPublicKeys
? Results.StatusCode(503)
: Results.Unauthorized();
}

await HandleDeliveryAsync(delivery);
return Results.NoContent();
});

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.VerifyWebhookAsync, which keeps the keys current for you. Available from version 1.6.0.

Method: WebhookVerifier.VerifyWebhookSignature(payload, headers, publicKeys, WebhookVerificationOptions? options = null)

WebhookVerifier is a static class in the Mobiscroll.Connect namespace. payload and headers accept the same types as in Webhooks.VerifyWebhookAsync.

Returns: void

Throws: WebhookVerificationException, with the same Reason values as Webhooks.VerifyWebhookAsync.

payloadReadOnlyMemory<byte> | string

The raw request body, exactly as received.

headersIEnumerable<KeyValuePair<string, string>> | IEnumerable<KeyValuePair<string, TValues>>

The request headers, in any casing.

publicKeysIEnumerable<string>

whpk_ public keys. The delivery is genuine if any signature verifies against any of them.

optionsWebhookVerificationOptions?

Optional. ToleranceSeconds sets the allowed webhook-timestamp age in either direction (default 300); Now is a long? that overrides the current Unix time in seconds.

Usage:

using Mobiscroll.Connect;
using Mobiscroll.Connect.Exceptions;

app.MapPost("/webhooks/mobiscroll", async (HttpRequest request) =>
{
using var body = new MemoryStream();
await request.Body.CopyToAsync(body);
var rawBody = body.ToArray();

try
{
WebhookVerifier.VerifyWebhookSignature(rawBody, request.Headers, new[] { pinnedPublicKey });
}
catch (WebhookVerificationException)
{
return Results.Unauthorized();
}

// The method only verifies. Parse rawBody yourself.
return Results.NoContent();
});

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