Skip to main content

Go SDK

The Mobiscroll Connect Go SDK provides a convenient way to integrate Mobiscroll Connect in Go backend applications. It requires Go 1.22 or higher and is built on the standard library net/http, so it works in any Go service (plain HTTP servers, Gin, Echo, Fiber, gRPC gateways, etc.).

Setup​

Add the module using go get:

go get github.com/acidb/mobiscroll-connect-sdks/sdks/go@latest

Then import it under the short package name mobiscroll:

import mobiscroll "github.com/acidb/mobiscroll-connect-sdks/sdks/go"

Client Initialization​

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

Constructor: mobiscroll.NewClient(clientID, clientSecret, redirectURI string, opts ...ClientOption) *Client

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.

opts...ClientOption

Optional functional options: WithBaseURL, WithTimeout, WithHTTPClient, WithTokensRefreshedCallback, WithWebhookPublicKey.

WithWebhookPublicKey(key string) pins a 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.

Usage:

import (
"time"

mobiscroll "github.com/acidb/mobiscroll-connect-sdks/sdks/go"
)

client := mobiscroll.NewClient(
"YOUR_CLIENT_ID",
"YOUR_CLIENT_SECRET",
"YOUR_REDIRECT_URI",
)

For a custom base URL, HTTP timeout, custom *http.Client, or a token-refresh callback for persistence, pass options to the constructor:

client := mobiscroll.NewClient(
"YOUR_CLIENT_ID",
"YOUR_CLIENT_SECRET",
"YOUR_REDIRECT_URI",
mobiscroll.WithBaseURL("https://connect.mobiscroll.com/api"),
mobiscroll.WithTimeout(60*time.Second),
mobiscroll.WithTokensRefreshedCallback(func(updated *mobiscroll.TokenResponse) {
// Persist updated.AccessToken / updated.RefreshToken
}),
)

Methods​

SetCredentials​

Stores a token pair the SDK will use on subsequent requests. Typically called after Auth().GetToken or when restoring credentials from persistent storage.

Method: client.SetCredentials(tokens *TokenResponse)

tokens*TokenResponse

The token response returned by client.Auth().GetToken(ctx, 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. Overrides any callback supplied via WithTokensRefreshedCallback.

Method: client.OnTokensRefreshed(cb func(*TokenResponse))

cbfunc(*TokenResponse)

A function that receives the updated TokenResponse after a successful automatic token refresh. Pass nil to clear the callback.

Credentials​

Returns the currently stored credentials, or nil if none.

Method: client.Credentials() *TokenResponse

Returns: *TokenResponse

Token Refresh​

The Go SDK handles token refresh automatically. When any API call returns a 401 Unauthorized response and the client has a refresh_token stored, the SDK silently exchanges it for a new access token and retries 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 process restarts.

client.OnTokensRefreshed(func(updated *mobiscroll.TokenResponse) {
// Persist updated in your database or session store
// so the new access_token and refresh_token survive future requests
session.Set("access_token", updated.AccessToken)
session.Set("refresh_token", updated.RefreshToken)
})

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

Running more than one instance​

Concurrent calls on the same client that hit an expired token deduplicate into a single refresh via golang.org/x/sync/singleflight. 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 errors satisfy the mobiscroll.MobiscrollError interface. Use errors.As to extract the concrete type.

Error typeHTTP StatusExtra field
*AuthenticationError401, 403— (returned after refresh + retry has been exhausted)
*CalendarPermissionError403Accounts — the accounts that must reconnect; unwraps to *AuthenticationError
*ValidationError400, 422Details (json.RawMessage)
*NotFoundError404—
*RateLimitError429RetryAfter (int, seconds)
*ServerError5xxStatusCode (int)
*NetworkError—wraps the underlying transport error (Unwrap)
*WebhookVerificationError—Reason (WebhookVerificationReason)

*WebhookVerificationError is returned by Webhooks().VerifyWebhook and VerifyWebhookSignature, not by API calls; its Reason says why the delivery was rejected.

import (
"context"
"errors"
"log"
"time"

mobiscroll "github.com/acidb/mobiscroll-connect-sdks/sdks/go"
)

ctx := context.Background()
start := time.Now()
end := start.AddDate(0, 1, 0)

_, err := client.Events().List(ctx, &mobiscroll.EventListParams{
Start: &start,
End: &end,
})
if err != nil {
var authErr *mobiscroll.AuthenticationError
var valErr *mobiscroll.ValidationError
var rlErr *mobiscroll.RateLimitError
var mErr mobiscroll.MobiscrollError

switch {
case errors.As(err, &authErr):
// Token expired and refresh failed — re-authorize the user
case errors.As(err, &valErr):
log.Printf("validation: %s", valErr.Details)
case errors.As(err, &rlErr):
time.Sleep(time.Duration(rlErr.RetryAfter) * time.Second)
case errors.As(err, &mErr):
// 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 — Accounts names the accounts that must reconnect. It unwraps to *AuthenticationError, so existing handlers keep working.

To localize the Connect pages, pass an optional Lng to GenerateAuthURL, e.g. &mobiscroll.AuthURLParams{ 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(p *AuthURLParams) string

Returns: string

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

Usage:

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

// Redirect the user to authURL
// http.Redirect(w, r, authURL, http.StatusFound)

Auth().GetToken​

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().GetToken(ctx context.Context, code string) (*TokenResponse, error)

Returns: *TokenResponse, error

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

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(ctx context.Context) ([]Calendar, error)

Returns: []Calendar, error

Usage:

calendars, err := client.Calendars().List(ctx)

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(ctx context.Context, p *EventListParams) (*EventsListResponse, error)

Returns: *EventsListResponse, error

All fields are optional: Start, End, CalendarIDs (calendar IDs grouped by provider), PageSize, NextPageToken and SingleEvents. Use mobiscroll.Ptr for the pointer fields.

Usage:

import (
"time"

mobiscroll "github.com/acidb/mobiscroll-connect-sdks/sdks/go"
)

start, _ := time.Parse(time.RFC3339, "2025-10-01T00:00:00Z")
end, _ := time.Parse(time.RFC3339, "2025-10-31T23:59:59Z")

// Fetch initial events with date range filter
response, err := client.Events().List(ctx, &mobiscroll.EventListParams{
PageSize: mobiscroll.Ptr(50),
Start: &start,
End: &end,
})
if err != nil {
// handle error
return
}

// Load more events using NextPageToken
nextResponse, err := client.Events().List(ctx, &mobiscroll.EventListParams{
PageSize: mobiscroll.Ptr(50),
NextPageToken: response.NextPageToken,
})

// Filter by specific calendars
filtered, err := client.Events().List(ctx, &mobiscroll.EventListParams{
PageSize: mobiscroll.Ptr(25),
CalendarIDs: map[mobiscroll.Provider][]string{
mobiscroll.ProviderGoogle: {"personal@gmail.com", "work@company.com"},
},
})

// Get recurring event series masters (not expanded)
masters, err := client.Events().List(ctx, &mobiscroll.EventListParams{
PageSize: mobiscroll.Ptr(50),
SingleEvents: mobiscroll.Ptr(false),
})

Events().Create​

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

Method: client.Events().Create(ctx context.Context, data *EventCreateData) (*CalendarEvent, error)

Returns: *CalendarEvent, error

Provider, CalendarID, Title, Start and End are required. Optional fields include Description, Location, AllDay and Recurrence.

Usage:

import (
"time"

mobiscroll "github.com/acidb/mobiscroll-connect-sdks/sdks/go"
)

start, _ := time.Parse(time.RFC3339, "2025-11-01T10:00:00Z")
end, _ := time.Parse(time.RFC3339, "2025-11-01T11:00:00Z")

// Create a simple event
event, err := client.Events().Create(ctx, &mobiscroll.EventCreateData{
Provider: mobiscroll.ProviderGoogle,
CalendarID: "primary",
Title: "Team Meeting",
Description: "Discuss project updates",
Start: start,
End: end,
Location: "Conference Room A",
})

// Create a recurring event
recurringStart, _ := time.Parse(time.RFC3339, "2025-11-01T09:00:00Z")
recurringEnd, _ := time.Parse(time.RFC3339, "2025-11-01T09:30:00Z")
recurringEvent, err := client.Events().Create(ctx, &mobiscroll.EventCreateData{
Provider: mobiscroll.ProviderMicrosoft,
CalendarID: "AAMkAGVmMDEz...",
Title: "Weekly Standup",
Start: recurringStart,
End: recurringEnd,
AllDay: mobiscroll.Ptr(false),
Recurrence: &mobiscroll.RecurrenceRule{
Frequency: "WEEKLY",
Interval: mobiscroll.Ptr(1),
Count: mobiscroll.Ptr(10),
ByDay: []string{"MO", "WE", "FR"},
},
})

// Create an all-day event
allDayStart, _ := time.Parse(time.RFC3339, "2025-11-15T00:00:00Z")
allDayEnd, _ := time.Parse(time.RFC3339, "2025-11-16T00:00:00Z")
allDayEvent, err := client.Events().Create(ctx, &mobiscroll.EventCreateData{
Provider: mobiscroll.ProviderApple,
CalendarID: "https://caldav.icloud.com/.../calendars/...",
Title: "Conference",
Start: allDayStart,
End: allDayEnd,
AllDay: mobiscroll.Ptr(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(ctx context.Context, data *EventUpdateData) (*CalendarEvent, error)

Returns: *CalendarEvent, error

Provider, CalendarID and EventID are required. Set only the fields you want to change — Start and End are pointers here — plus RecurringEventID and UpdateMode for recurring events.

Usage:

import (
"time"

mobiscroll "github.com/acidb/mobiscroll-connect-sdks/sdks/go"
)

start, _ := time.Parse(time.RFC3339, "2025-11-01T14:00:00Z")
end, _ := time.Parse(time.RFC3339, "2025-11-01T15:00:00Z")

// Update a simple event
updatedEvent, err := client.Events().Update(ctx, &mobiscroll.EventUpdateData{
Provider: mobiscroll.ProviderGoogle,
CalendarID: "primary",
EventID: "event123abc",
Title: "Updated Team Meeting",
Start: &start,
End: &end,
})

// Update a single instance of a recurring event
instanceStart, _ := time.Parse(time.RFC3339, "2025-11-01T09:00:00Z")
instanceEnd, _ := time.Parse(time.RFC3339, "2025-11-01T10:00:00Z")
updatedInstance, err := client.Events().Update(ctx, &mobiscroll.EventUpdateData{
Provider: mobiscroll.ProviderMicrosoft,
EventID: "instance456",
RecurringEventID: "series123",
CalendarID: "AAMkAGVmMDEz...",
UpdateMode: "this",
Title: "Standup - Special Topic Today",
Start: &instanceStart,
End: &instanceEnd,
})

// Update all future occurrences
updatedFuture, err := client.Events().Update(ctx, &mobiscroll.EventUpdateData{
Provider: mobiscroll.ProviderApple,
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(ctx context.Context, p *EventDeleteParams) error

Returns: error

Provider, CalendarID and EventID are required. RecurringEventID and DeleteMode are optional.

Usage:

// Delete a simple event
err := client.Events().Delete(ctx, &mobiscroll.EventDeleteParams{
Provider: mobiscroll.ProviderGoogle,
CalendarID: "primary",
EventID: "event123abc",
})

// Delete a single instance of a recurring event
err = client.Events().Delete(ctx, &mobiscroll.EventDeleteParams{
Provider: mobiscroll.ProviderMicrosoft,
CalendarID: "AAMkAGVmMDEz...",
EventID: "instance456",
RecurringEventID: "series123",
DeleteMode: "this",
})

// Delete entire recurring series
err = client.Events().Delete(ctx, &mobiscroll.EventDeleteParams{
Provider: mobiscroll.ProviderApple,
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(ctx context.Context, p *SubscribeWebhookParams) (*SubscribeWebhookResponse, error)

Returns: *SubscribeWebhookResponse, error

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

Usage:

import mobiscroll "github.com/acidb/mobiscroll-connect-sdks/sdks/go"

subscription, err := client.Webhooks().SubscribeWebhook(ctx, &mobiscroll.SubscribeWebhookParams{
Provider: mobiscroll.ProviderGoogle,
CalendarID: "work@company.com",
ChannelID: "my-channel-123",
})

Webhooks().UnsubscribeWebhook​

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

Method: client.Webhooks().UnsubscribeWebhook(ctx context.Context, p *UnsubscribeWebhookParams) (*UnsubscribeWebhookResponse, error)

Returns: *UnsubscribeWebhookResponse, error

Provider and ChannelID are required. ResourceID is the provider resource ID returned by SubscribeWebhook, where the provider uses one.

Usage:

import mobiscroll "github.com/acidb/mobiscroll-connect-sdks/sdks/go"

_, err := client.Webhooks().UnsubscribeWebhook(ctx, &mobiscroll.UnsubscribeWebhookParams{
Provider: mobiscroll.ProviderGoogle,
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(ctx context.Context, payload []byte, headers http.Header) (*WebhookDelivery, error)

Returns: *WebhookDelivery, error

Errors: *WebhookVerificationError when the delivery is not genuine. Respond with a 4xx and do not process it. If ctx ends while the SDK waits for the keys, the error is ctx.Err().

ctxcontext.Context

The request context. It bounds the wait for the public keys.

payload[]byte

The raw request body, exactly as received. Do not decode the JSON and encode it again: that changes the bytes, and every signature check fails.

headershttp.Header

The request headers, in any casing. 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. The keys URL is built from the origin of the client's base URL. It refreshes the keys 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 key pinned with WithWebhookPublicKey is used only when no keys could be fetched at all.

Reason on the returned *WebhookVerificationError is a WebhookVerificationReason. Compare it against these constants:

ConstantValueMeaning
WebhookMissingHeadersmissing_headerswebhook-id, webhook-timestamp or webhook-signature is missing or empty
WebhookInvalidTimestampinvalid_timestampwebhook-timestamp is not a Unix time in seconds
WebhookTimestampOutOfTolerancetimestamp_out_of_tolerancewebhook-timestamp is more than 5 minutes from your server's clock
WebhookNoPublicKeysno_public_keysNo keys could be fetched and no valid WithWebhookPublicKey is configured
WebhookNoMatchingSignatureno_matching_signatureNo signature matches any key — a forged or modified delivery
WebhookInvalidPayloadinvalid_payloadThe signature is valid, but the body is not valid JSON

Usage:

import (
"errors"
"io"
"net/http"

mobiscroll "github.com/acidb/mobiscroll-connect-sdks/sdks/go"
)

http.HandleFunc("/webhooks/mobiscroll", func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}

delivery, err := client.Webhooks().VerifyWebhook(r.Context(), body, r.Header)
var ve *mobiscroll.WebhookVerificationError
if errors.As(err, &ve) {
if ve.Reason == mobiscroll.WebhookNoPublicKeys {
http.Error(w, "keys unavailable", http.StatusServiceUnavailable) // Connect retries
} else {
http.Error(w, "invalid signature", http.StatusUnauthorized) // final
}
return
} else if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusNoContent)
go handleDelivery(delivery)
})

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.

Function: mobiscroll.VerifyWebhookSignature(payload []byte, headers http.Header, publicKeys []string, opts ...VerifyWebhookOption) error

Returns: error — nil when the delivery is genuine, otherwise a *WebhookVerificationError with the same Reason constants as Webhooks().VerifyWebhook. It never returns WebhookInvalidPayload, because it does not parse the body.

payload[]byte

The raw request body, exactly as received.

headershttp.Header

The request headers, in any casing.

publicKeys[]string

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

opts...VerifyWebhookOption

Optional functional options. WithWebhookTolerance(d time.Duration) sets the allowed webhook-timestamp distance from the current time in either direction (default 5 minutes). WithWebhookNow(t time.Time) sets the time to check against instead of the system clock, which is useful in tests.

Usage:

import (
"io"
"os"

mobiscroll "github.com/acidb/mobiscroll-connect-sdks/sdks/go"
)

body, _ := io.ReadAll(r.Body)
err := mobiscroll.VerifyWebhookSignature(body, r.Header, []string{os.Getenv("MOBISCROLL_WEBHOOK_PUBLIC_KEY")})

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