Developing a Custom App

At this point in the guide you should have a Copilot app project set up, an app API key created and added to your project, and the SDK initialized. In this guide we will walk through how use the SDK to do the various things you might like to do in a Copilot app.

For the examples on this page we’re going to assume you’ve got an instance of the Copilot SDK in scope saved to a const called copilot.

Making API Calls

In this section we’ll look at how to fetch and mutate data using the Copilot SDK. We’ll be selecting a few endpoints to use as examples.

For both fetch and mutation functions, the parameters and return values can be found with your IDE’s Intellisense features or by referencing the API reference documentation.

Fetching data

In order to fetch data associated with your Copilot workspace you can select one of the functions from the SDK. For this example, we will choose the listClients function. The simplest call to listClients will look like this:

const response = await copilot.listClients();
console.log(; // is a list of Clients.

This particular endpoint takes a configuration object as an argument that allows you to filter the results. Let’s say, for example, you’d like to fetch all clients with the same family name. That function call would look like this:

const response = await copilot.listClients({ familyName: 'Name' });

You can identify fetch functions by their prefixes:


Mutating data

In order to mutate data in your Copilot workspace, you’ll again select a function to call. For this example let’s take a look at the createClient mutation::

const client = await copilot.createClient({
  requestBody: {
    givenName: 'Test',
    familyName: 'Client',
    email: '[email protected]',

You can identify mutation functions by their prefixes:


Handling Errors

Most endpoints return a uniform error object that includes a code as well as a message. For information on the specific errors you’ll need to handle in your app, our REST API documentation includes 400-level response objects on a per-endpoint basis.

Currently Logged-in User

When your app is rendered by Copilot we’ll pass in an encrypted token that gives you information about the current session. For a detailed breakdown of what’s in that token, see the Setting up the SDK page.

Based on the Token contents you can determine the ID of the currently logged-in user. The logic is as follows:

  1. If the internalUserId value is present, that’s the currently logged-in user regardless of the value of clientId.
  2. If the internalUserId is not present and clientId is, the clientId is the currently logged in user.

When processing tokens on webhook URLs, there will be no user IDs in the token.


Relationship to companies

If you have companies enabled in your workspace, you can associate multiple clients to one company. This is typical for businesses that serve other businesses, where your customer might have multiple individuals you’re working with.

A client-facing Custom App will also receive context via the token on the currently logged in client user’s company. This allows you to tailor the user experience based on the company instead of client. Many client-facing Custom Apps will tailor their UX such that all clients within a company see the same content. Whether you choose to do this depends on whether individual clients within a company should have different experiences across the portal more broadly.

Custom fields

Custom fields are a powerful way to associate arbitrary data with a client. You can add custom fields on the Clients page and you can fetch custom fields in two ways:

  1. By fetching a clients’ list and accessing the custom fields on each client object.
  2. By fetching the list of custom fields to see which fields are available in the workspace.

Custom fields can only be created in the UI, they cannot be created via the API.


Apps can send notifications to users. Notifications are a great way to make your app more sticky and alert users when an action needs to be taken. Let’s say, for example, a client is logged in and finishes an action. We want to alert an internal user that action has been completed. Here’s how we might approach that in code.

await copilot.createNotification({
	senderId: '<clientId>',
	recipientId: '<internalUserId>',
	deliveryTargets: {
		inProduct: {
		  title: 'Action completed',
	    body: 'An action was completed in your workspace.',
		email: {
			subject: 'Email subject line',
			body: 'This is the body of the email notification',

In this example, it’s simple to determine the clientId from the session token. To determine which internalUserId to use as the recipientId we’ll have to use the Internal Users API to figure out which internal users have access to that client. More on that in the next section.

Internal user vs. Client notifications

Internal users have a notification center in the Copilot Dashboard. There, they can see a detailed view of all their notifications, enabling them to manage “read” vs. “unread” status, and preview the resources associated with the notifications.

We do not have a notification center for clients. Instead, clients will see a notification count badge next to the Custom App’s sidebar navigation item that represents the sum of unread notifications.

Delivery targets

In the example above you’ll notice that there are multiple delivery targets you can send notifications to. Over time this list will grow as we add support for additional delivery targets.

In-ProductIn product notifications display for internal users on the Notifications page in the dashboard. For client users, notifications display a badge on the navigation item for the app the notification is associated with.
EmailEmail notifications are sent to the recipient’s email inbox.

Internal user permissions

Internal user roles

Internal users are the employees of a service business, these users have the ability to manage the clients and content in your workspace. There are two types of internal users:

StaffStaff users do not have access to certain settings pages, like Billing, API, plans, and clients. This type of user can manage clients that are assigned to them
AdminAdmin users can access all pages within your workspace, they can create Apps and API keys, and manage client access settings.

For more information, see the Guide article on internal user roles.

Client access

At this point we’ve covered that a Copilot workspace has two types of users, internal users and clients. In the context of a service business, an internal user will typically be an employee of the service business and they will have access to some percentage of the clients that business services. You can control which clients internal users can access on the Settings > Team page. By default client access for each internal user is set to “Full access” but you can change it to “Limited” and then select the specific clients/companies that the internal user has access to.

In order to determine if an internal user has access to a client there are two properties to consider:

isClientAccessLimitedbooleanIf false, the IU has access to all clients. If true, the IU only has access to clients within certain companies.
companyAccessListstring[]A list of companies the IU has access to.

Let’s look at an example where we’ll call the List Internal Users endpoint and filter the results to include only the users that have access to a certain client.

const client = {} // Imagine we've got a Client object in scope
const response = await copilot.listInternalUsers();
const internalUsersWithAccess = => {
  if (!iu.isClientAccessLimited) return true;
  return iu.companyAccessList.includes(client.companyId);

The resulting list would be all the internal users that have access to the client.


So far all the examples we’ve looked at have been in the context of a user session that visits an app. However, apps can respond to actions even when they’re not being interacted with using webhooks.

In order to create a webhook you’ll need to create a POST endpoint. On the app setup page there are configuration options for webhooks.

Webhook URLThe URL of the POST endpoint that you’ve created in your app.
Webhook EventsA list of events you’d like to subscribe to.

The payload that’s sent to your Webhook URL endpoint looks like this::

  eventType: "client.updated", // For example
  data: {...}, // A resource

For a complete list of webhook events as well as the resources they send, see the Webhook Events documentation.

App Bridge

The Copilot App Bridge lets apps that are rendered in an iframe within a copilot workspace control the URL of the parent frame. This unlocks apps navigating to other pages with the workspace.

The way to send a navigation request is to send a structured post message to the parent frame of your app. The Copilot front-end application will subscribe to messages sent from your frame and execute redirects on the app’s behalf. Here’s what it looks like in code:

  type: 'history.push',
  route: 'files'
}, '');

This code snippet would trigger a redirect to the files app from a client portal. It’s important to note, your app is responsible for curating internal user vs. client experiences and the second argument you pass to postMessage will change depending on what surface the user is currently on. If you’re building an internal user experience, the hostname should be

Some routes accept an optional ID parameter to enable linking to a specific item within the route, here's an example of including an ID:

  id: '<appId>',
  type: 'history.push',
  route: 'apps'
}, '');

Here’s a list of all the routes we support redirecting to:

RouteSupports ID Deep Linking?

Data Caching

If you’re following along building your custom app with Next.js, you may notice some of the data returned from Copilot endpoints are cached. The caching layer is a performance feature of Next.js.

The choice of which data to cache and for how long depends on the nature of your application. There’s a constant push and pull between performance and availability. If your application has a strong need for realtime data integrity, you may choose to disable caching of Copilot endpoints altogether. On the other hand, if you’d like your app to render instantly with the understanding that it’s okay if the data takes some time to revalidate, you may choose to set a Time to Live (TTL) that feels right for your application.

In this guide we’ll offer some simple suggestions as well as code samples from the apps we’ve build to show how we manage caching in Next.JS. For more in-depth information about caching in Next.js, please consult the documentation.

How to disable caching:

When we built our Exporter app we had a few use cases in mind like law firms pulling communications data or exporting your Copilot data in an effort to migrate away from our platform. Each of these use cases requires realtime data integrity so we opted to disable caching entirely for our Copilot API calls.

That app uses server actions for its backend, so we’re able to disable caching in each server action by calling a function from the next/cache package. In your src/app/actions/<file>.ts you can add the following function call:

'use server';

import { unstable_noStore as noStore } from 'next/cache';

export async function makeApiCall() {
  const api = copilot.retrieveWorkspace();

Here’s a link to the file with noStore() in the Exporter app.

How to configure a TTL:

Some applications can accept a delay in revalidating cached data. The TTL you choose depends on your needs, it can range from 0 to Infinity (the default setting). In order to adjust the TTL, open your app’s global layout.tsx file or a page.tsx file and add the following line of code:

export const revalidate = 60; // One minute, in seconds

In our custom-app-base, the default TTL is 3 minutes. This can be configured per page to whatever value you want.

Note: In the future we plan to add per route caching options by allowing you to customize the fetch options used in the copilot-node-sdk.

Consult the Next.JS docs for more information on revalidate.