Skip to main content

Custom Widgets Developer Guide: Advanced

  • April 2, 2026
  • 0 replies
  • 13 views

Nurul Ghafar

This supplemental guide covers how to build custom widgets that integrate with third-party services using Passports — Appspace's mechanism for managing authentication with external providers like Google, Microsoft, Zoom, Slack, and more.

 

In this article:

 


 

Prerequisites:

 

Before reading this guide, complete the Custom Widget Developer Guide to understand the basics of widget creation, the Widget API, and schema configuration.

 


 

What Are Passports?

 

A Passport in Appspace is a secure authentication connector that allows your widget to communicate with third-party services. Instead of managing OAuth tokens directly in your widget code, Passports handle the entire authentication lifecycle:

  1. Administrator connects the Passport (OAuth popup)

  2. Appspace stores the credentials securely

  3. Your widget calls callAuthenticatedAPI() — the Console injects the correct auth headers automatically

  4. Token refresh is handled transparently by Appspace

Your widget code never sees the actual credentials. It only sends API requests by name, and the Console proxies them with the correct authentication.

 


 

Two Approaches to Authentication

 

When your widget needs to talk to a third-party service, you have two options:

 

Approach When to Use What Happens

Built-in Passport

The service already has an Appspace Passport (Google, Microsoft, Zoom, etc.)

Administrator selects an existing Passport from a dropdown in the widget configurator

Custom (Embedded) Passport

The service is not in Appspace's built-in list, or you need custom OAuth configuration

You bundle a passport/*.json file in your widget ZIP; Appspace auto-creates the Passport on upload

 

Both approaches use the same Widget API method (callAuthenticatedAPI) at runtime. The only difference is how the Passport is provisioned.

 


 

Approach 1: Using Built-in Passports

 

If the third-party service already has an Appspace Passport, your widget can reference it without bundling any passport file. The administrator will select an existing Passport when configuring the widget.

 

Step 1: Add authentications to schema.json

 

{
"authentications": [
{
"label": "Google Calendar",
"authenticationType": "passport",
"authKey": "googleCalAuth",
"metadata": {
"passportProvider": "google"
}
}
]
}

 

The passportProvider value tells the widget configurator which Passports to show in the dropdown. See the Built-in Passport Reference for all available values.

 

Step 2: Add authenticatedApis to schema.json

 

{
"authenticatedApis": [
{
"label": "Get Calendar Events",
"name": "getCalendarEvents",
"url": "https://www.googleapis.com/calendar/v3/calendars/primary/events",
"method": "GET",
"authenticationKey": "googleCalAuth",
"headers": {
"Authorization": "Bearer {authorization.googleCalAuth.token}"
}
}
]
}

 

Step 3: Call the API from your widget

 

var response = await window.appspace.widgetApi.callAuthenticatedAPI('getCalendarEvents', {
params: { maxResults: '10', orderBy: 'startTime', singleEvents: 'true' }
});

if (response.status === 200 && response.data && response.data.isSuccess) {
var events = response.data.body; // Parsed JSON
}

 

Important: Only pass params. Do not pass method, headers, or body — these are defined in the schema and handled by the proxy.

 

What the administrator sees

 

When configuring the widget, the administrator sees a dropdown listing all Passports matching the passportProvider value. They select one (or create a new one) and connect it.

 


 

Approach 2: Bundling a Custom Passport

 

When your target service is not in the built-in list, or you need specific OAuth scopes, bundle a Passport JSON file inside your widget ZIP.

 

Step 1: Create the passport JSON file

 

Create a file in the passport/ directory of your widget project.

 

OAuth Examplepassport/my-api-connector.json:

 

{
"name": "My API Connector",
"applicationName": "custom",
"authenticationType": "OAuth",
"isCustomApplication": true,
"customApplicationInfo": {
"name": "my-api-connector",
"displayName": "My API Connector",
"description": "OAuth connector for My API service",
"categoryType": "Custom",
"authenticationType": "OAuth",
"parentName": "custom",
"metadata": {
"clientId": "<your-client-id>",
"clientSecret": "<your-client-secret>",
"authenticationUrl": "https://api.example.com/oauth/authorize",
"tokenUrl": "https://api.example.com/oauth/token",
"profileUrl": "https://api.example.com/api/me",
"profileAccountEmailPath": "$.email",
"profileAccountIdPath": "$.id",
"profileAccountNamePath": "$.name",
"scope": "read write"
}
},
"metadata": {}
}

 

Step 2: Reference in schema.json

 

Add the passport property to your schema and wire up authentications and authenticatedApis:

 

{
"passport": {
"filePath": "passport/my-api-connector.json"
},
"authentications": [
{
"label": "My API Authentication",
"authenticationType": "passport",
"authKey": "myApiAuth",
"metadata": {
"passportProvider": "custom:my-api-connector"
}
}
],
"authenticatedApis": [
{
"label": "Get Data",
"name": "getData",
"url": "https://api.example.com/v1/data",
"method": "GET",
"authenticationKey": "myApiAuth",
"headers": {
"Authorization": "Bearer {authorization.myApiAuth.token}"
}
}
]
}

 

Critical: The passportProvider value must follow the pattern custom:<name> where <name> matches customApplicationInfo.name in your passport JSON file.

 

Step 3: Include passport in your ZIP

 

Ensure your packaging script includes the passport/ directory:

 

// In scripts/package-widget.js
if (fs.existsSync('passport')) {
archive.directory('passport/', 'passport');
}

 

Or if zipping manually, include the passport/ folder in the ZIP alongside widget.html and schema.json.

 

What happens on upload

 

When you upload the widget ZIP to Appspace:

  1. The backend reads schema.json and finds passport.filePath

  2. It extracts the passport JSON from the ZIP

  3. It automatically creates a custom Passport in the Appspace passport service

  4. The widget configurator shows the passport pre-selected for the administrator

  5. The administrator completes the OAuth connection flow

 

What the administrator sees

 

Instead of a dropdown, the administrator sees a dedicated "Connect" button for the bundled passport. After connecting (OAuth popup), the button shows the connected status with options to reconnect or disconnect.

 


 

Schema Configuration for Authentication

 

The three schema properties work together:

 

passport (optional)          → Bundles a passport JSON for auto-creation

authentications (required) → Declares what authentication the widget needs

authenticatedApis (required) → Defines the actual API endpoints to call

 

The passport property

 

Field Type Required Description

filePath

string

Yes

Relative path to passport JSON inside the widget ZIP

 

Only needed for custom (embedded) passports. Omit this entirely when using built-in passports.

 

The authentications array

 

Each entry declares one authentication requirement:

 

Field Type Required Description

label

string

No

Display label in widget configurator

authenticationType

string

Yes

Must be "passport"

authKey

string

Yes

Unique key to reference this auth (used by authenticatedApis)

metadata.passportProvider

string

No

Filters which passports are shown. Use "custom:<name>" for bundled, or a built-in provider value

metadata.passportType

string

No

Informational: "oauth2"

metadata.adminSitePermission

boolean

No

Whether admin-level site permissions are required

 

 

The authenticatedApis array

 

Each entry defines one API endpoint:

 

Field Type Required Description

label

string

Yes

Human-readable label

name

string

Yes

Unique name — used in callAuthenticatedAPI(name)

url

string

Yes

Full URL with optional {param.*} template variables

method

string

Yes

"GET", "POST", "PUT", or "DELETE"

authenticationKey

string

Yes

Must match an authKey from authentications

headers

object

No

Static headers. Use {authorization.<authKey>.token} for auth

data

object

No

Static request body for POST/PUT

 

 


 

Making Authenticated API Calls

 

Once authentication is configured, making API calls is straightforward:

 

// Call by the 'name' defined in authenticatedApis
var response = await window.appspace.widgetApi.callAuthenticatedAPI('getData', {
params: {
userId: '12345',
limit: '50'
}
});

// Response structure
// response.status — HTTP status code (e.g., 200)
// response.statusText — HTTP status text (e.g., "OK")
// response.data.statusCode — Backend status code
// response.data.body — Response body (auto-parsed if JSON)
// response.data.headers — Response headers
// response.data.contentType — Response content type
// response.data.isSuccess — Boolean indicating success

if (response.status === 200 && response.data && response.data.isSuccess) {
var data = response.data.body;
// Use the data...
}

 

Key rules:

  • Only pass params — do not pass method, headers, or body

  • The params object is used for URL template variable substitution and query parameters

  • The Console proxy handles authentication header injection, URL construction, and the actual HTTP request

  • If the API returns JSON, response.data.body is automatically parsed

 


 

Template Variables in API URLs

 

API URLs support template variables that are resolved at call time:

 

{
"url": "https://api.example.com/users/{param.userId}/posts?page={param.page}",
"method": "GET",
"authenticationKey": "myApiAuth"
}

 

When calling:

 

await window.appspace.widgetApi.callAuthenticatedAPI('getUserPosts', {
params: { userId: '42', page: '2' }
});

 

The resolved URL becomes: https://api.example.com/users/42/posts?page=2

 

Special variables

 

Variable Pattern Source

{param.*}

From the params object passed to callAuthenticatedAPI()

{authorization.<authKey>.token}

OAuth token for the specified auth key (injected by proxy)

{system.userId}

Current user's ID (injected by host)

 

 


 

Built-in Passport Reference

 

Google

 

Passport passportProvider Auth Type Category

Google Calendar

"google"

OAuth

Calendar

Google Drive

"google"

OAuth

Library

Google Meet Conferencing

"google"

OAuth

Conference

 

 

Microsoft

 

Passport passportProvider Auth Type Category

Microsoft 365

"microsoft"

OAuth

Calendar

Microsoft Teams

"microsoft"

OAuth

Publishing

Microsoft Teams Conferencing

"microsoft"

OAuth

Conference

Power BI

"microsoft"

OAuth

Dashboard

SharePoint

"microsoft"

OAuth

Library

SharePoint Publishing

"microsoft"

OAuth

Publishing

Microsoft Viva Engage

"microsoft"

OAuth

Publishing

Microsoft Entra ID

"microsoft"

OAuth

Org Chart

Microsoft 365 Presence

"microsoft"

OAuth

Presence

Microsoft 365 Calendar Permission

"microsoft"

OAuth

Delegations

Appspace SharePoint Intranet

"microsoft"

OAuth

Library

 

 

Zoom

 

Passport passportProvider Auth Type Category

Zoom Meetings

"zoom"

OAuth

Library

Zoom Conferencing

"zoom"

OAuth

Conference

 

 

Webex

 

Passport passportProvider Auth Type Category

Webex Teams

"webex"

OAuth

Publishing

Webex Meetings

"webex"

OAuth

Library

Webex Conferencing

"webex"

OAuth

Conference

 

 

Social & Publishing

 

Passport passportProvider Auth Type Category

Facebook

"facebook"

OAuth

Social

Instagram

"facebook"

OAuth

Social

Slack

"slack"

OAuth

Publishing

Workplace (Facebook)

"workplace"

OAuth

Publishing

 

 

Other Services

 

Passport passportProvider Auth Type Category

Salesforce

"salesforce"

OAuth

Dashboard

Tableau

"tableau"

Custom Auth

Dashboard

ServiceNow

"servicenow"

OAuth

Library

Box

"box"

OAuth

Library

 

 

Note:

When using a built-in passport, you do not need a passport property or passport/ directory in your widget ZIP. The administrator selects an existing Passport from the dropdown when configuring the widget.

 


 

Custom Passport JSON Reference

 

When bundling a custom passport, the JSON file must follow this structure:

 

Required Fields

 

Field Value Description

name

string

Display name for the passport

applicationName

"custom"

Must be "custom" for widget-bundled passports

authenticationType

"OAuth"

Authentication method

isCustomApplication

true

Must be true for widget-bundled passports

customApplicationInfo.name

string

Short identifier (used in passportProvider: "custom:<name>")

customApplicationInfo.displayName

string

Human-readable name

customApplicationInfo.description

string

Description text

customApplicationInfo.categoryType

string

Category (typically "Custom")

customApplicationInfo.authenticationType

"OAuth"

Must match parent authenticationType

customApplicationInfo.parentName

"custom"

Must be "custom" for widget-bundled passports

customApplicationInfo.metadata

object

Auth-specific credentials (see below)

metadata

{}

Additional metadata (typically empty)

 

 

OAuth Metadata Fields

 

Field Value Description

clientId

Yes

OAuth application client ID

clientSecret

Yes

OAuth application client secret

authenticationUrl

Yes

OAuth authorization endpoint

tokenUrl

Yes

OAuth token exchange endpoint

profileUrl

No

User profile endpoint (for account display)

profileAccountEmailPath

No

JSONPath to extract email from profile (e.g., "$.email")

profileAccountIdPath

No

JSONPath to extract user ID (e.g., "$.id")

profileAccountNamePath

No

JSONPath to extract display name (e.g., "$.name")

scope

No

OAuth scopes (space-separated)

scopeSeparator

No

Scope separator character (default: " ")

referenceApplicationName

No

Inherit OAuth URLs from an existing built-in provider

refreshType

No

"beforeExpiryInMinutes" or "periodicallyInMinutes"

refreshValue

No

Refresh interval in minutes

 


 

Complete Examples

 

Example 1: Microsoft Graph Widget (Built-in Passport)

 

schema.json (relevant sections):

 

{
"authentications": [
{
"label": "Microsoft 365",
"authenticationType": "passport",
"authKey": "msGraph",
"metadata": {
"passportProvider": "microsoft",
"passportType": "oauth2"
}
}
],
"authenticatedApis": [
{
"label": "Get Calendar Events",
"name": "getEvents",
"url": "https://graph.microsoft.com/v1.0/me/calendarview?startDateTime={param.startDate}&endDateTime={param.endDate}",
"method": "GET",
"authenticationKey": "msGraph",
"headers": {
"Authorization": "Bearer {authorization.msGraph.token}"
}
}
]
}

 

Widget code:

 

var now = new Date();
var nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);

var response = await window.appspace.widgetApi.callAuthenticatedAPI('getEvents', {
params: {
startDate: now.toISOString(),
endDate: nextWeek.toISOString()
}
});

if (response.status === 200 && response.data && response.data.isSuccess) {
var events = response.data.body.value;
events.forEach(function(event) {
console.log(event.subject, event.start.dateTime);
});
}

 

Example 2: Custom OAuth Widget (Bundled OAuth Passport)

 

A widget that integrates with a custom SaaS API using OAuth.

 

passport/my-saas-connector.json:

 

{
"name": "My SaaS Platform",
"applicationName": "custom",
"authenticationType": "OAuth",
"isCustomApplication": true,
"customApplicationInfo": {
"name": "my-saas-connector",
"displayName": "My SaaS Platform",
"description": "OAuth connector for My SaaS Platform API",
"categoryType": "Custom",
"authenticationType": "OAuth",
"parentName": "custom",
"metadata": {
"clientId": "your-client-id-here",
"clientSecret": "your-client-secret-here",
"authenticationUrl": "https://auth.my-saas.com/oauth/authorize",
"tokenUrl": "https://auth.my-saas.com/oauth/token",
"profileUrl": "https://api.my-saas.com/v1/me",
"profileAccountEmailPath": "$.email",
"profileAccountIdPath": "$.id",
"profileAccountNamePath": "$.display_name",
"scope": "read:data write:data"
}
},
"metadata": {}
}

 


 

Troubleshooting

 

Passport not appearing in widget configurator

 

Symptom: No passport dropdown or connect button shown.

 

Solutions:

  1. Ensure authentications array exists in schema.json with at least one entry

  2. Verify authenticationType is "passport" (not "custom")

  3. For built-in passports: verify the passportProvider value matches a known provider

  4. For bundled passports: verify passport.filePath points to a valid JSON file in the ZIP

 

"Authentication required" error on API call

 

Symptom: callAuthenticatedAPI() returns an error.

 

Solutions:

  1. Ensure the administrator has connected the passport

  2. Verify authenticationKey in authenticatedApis matches an authKey in authentications

  3. Check that the passport is active (not expired or revoked)

 

OAuth connection fails

 

Symptom: OAuth popup closes without completing.

 

Solutions:

  1. Verify clientId and clientSecret in your passport JSON are correct

  2. Ensure authenticationUrl and tokenUrl are valid endpoints

  3. Check that the OAuth app's redirect URI includes your Appspace domain

  4. Verify the scope is valid for the target API

 

API call returns unexpected data

 

Symptom: response.data.body is empty or malformed.

 

Solutions:

  1. Verify the API URL is correct (check template variable substitution)

  2. Ensure you're only passing params — not method, headers, or body

  3. Check that the OAuth scope includes permission for the requested endpoint

  4. Look at response.data.statusCode and response.data.headers for clues

 

Custom passport not auto-created on upload

 

Symptom: Widget uploads but passport doesn't appear.

 

Solutions:

  1. Verify the passport/ directory is included in the widget ZIP at the root level

  2. Check that passport.filePath in schema.json matches the actual file path

  3. Ensure the passport JSON follows the required structure (all required fields present)

  4. Verify isCustomApplication: true is set in the passport JSON

  5. Check that applicationName and parentName are both "custom"

 


 

This topic has been closed for replies.