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. API calls should only be made from your app’s back-end, exposing your API key in your front-end application would be a security risk.
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(response.data); // response.data 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:
Prefix | Example |
---|---|
list | listClients |
retrieve | retrieveClient |
download | downloadFile |
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:
Prefix | Example |
---|---|
create | createClient |
delete | deleteClient |
request | requestFormResponse |
send | sendMessage |
update | updateClient |
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.
Client-server networking: Server Actions vs. REST
As you develop your app, you’ll need to consider your data fetching strategy. Our custom app base comes with React Server Actions set up and ready to use. If you prefer, you can opt for using fetch()
to call a more traditional REST API.
Whichever approach you choose, it’s important that you pass the ephemeral session token passed to your app. The following sections will provide guidance for doing that with each networking approach.
Server Actions
React Server Actions are a feature introduced in Next.js 13 that allow you to run server-side logic directly from your React components. They provide a way to handle tasks like data fetching, form submissions, and database updates without needing to write separate API endpoints. You can find the Next.js documentation on Server Actions here.
The Copilot SDK can be used in server components directly. This is modeled in the Copilot Custom App Base, see the example below:
Example component:
import { getTagsFields } from '@/actions/fields-message';
async function Content({ searchParams }: { searchParams: SearchParams }) {
const inputToken: string | string[] | undefined = searchParams.token;
const tokenValue = typeof inputToken === 'string' ? inputToken : undefined;
const tagsFields = await getTagsFields(tokenValue);
return <div>Hello, World!</div>;
}
Example function:
export async function getTagsFields (token: string | undefined) {
const copilot = copilotApi({
apiKey: apiKey,
token: token
});
const data: {
customFields: Awaited<ReturnType<typeof copilot.listCustomFields>>;
} = {
customFields: await copilot.listCustomFields(),
};
return data
}
Passing tokens to REST endpoints
You can alternatively choose to make calls using the Copilot SDK in your RESTful API routes. When instantiating the Copilot SDK, it is necessary to pass the API key and the token obtained from the URL params. If you are using API routes in your app to make calls using the Copilot SDK, you will want to be sure to pass the token when making a request to your API route.
Example request:
const searchParams = useSearchParams();
const token = searchParams.get('token');
const response = await fetch('/api/getTagsFromField', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ fieldId: value, token: token }),
});
Example route:
import { NextRequest, NextResponse } from 'next/server';
import { getTagsFromField } from '@/actions/fields-message';
export async function POST(request: NextRequest) {
try {
const { fieldId, token } = await request.json(); // Extract `fieldId` and `token` from the request body
if (!fieldId) {
return NextResponse.json({ error: 'Invalid fieldId' }, { status: 400 });
}
const tags = await getTagsFromField(fieldId, token);
return NextResponse.json(tags, { status: 200 });
} catch (error) {
return NextResponse.json({ error: 'Failed to fetch tags' }, { status: 500 });
}
}
Example API call using the Copilot SDK:
export async function getTagsFromField(fieldId: string, token: string | undefined) {
const copilot = copilotApi({
apiKey: apiKey,
token: token
});
const data: {
customFieldOptions: Awaited<ReturnType<typeof copilot.listCustomFieldOptions>>;
} = {
customFieldOptions: await copilot.listCustomFieldOptions({id: fieldId})
}
return data.customFieldOptions
}
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:
- If the
internalUserId
value is present, that’s the currently logged-in user regardless of the value of clientId. - If the
internalUserId
is not present andclientId
is, theclientId
is the currently logged in user.
When processing tokens on webhook URLs, there will be no user IDs in the token.
Content Security Policy
The content security policy in the Copilot Custom App Base can be set in src/middleware.ts
. You’ll want to include https://dashboard.copilot.com
and https://*.copilot.app.
Note: If you have a custom domain you should include this as well.
Example below:
const cspHeader = `
frame-ancestors https://dashboard.copilot.com/ https://*.copilot.app/
https://*.yourcustomdomain.com
...
`;
If you’re not using our Custom App Base, you can find instructions for setting the content security policy in NextJS here. For other app development frameworks, please refer to their documentation.
Clients
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:
- By fetching a clients’ list and accessing the custom fields on each client object.
- 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.
Notifications
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.
Target | Description |
---|---|
In-Product | In 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. |
Email 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:
Type | Description |
---|---|
Staff | Staff 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 |
Admin | Admin 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:
Property | Type | Description |
---|---|---|
isClientAccessLimited | boolean | If false, the IU has access to all clients. If true, the IU only has access to clients within certain companies. |
companyAccessList | string[] | 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 = response.data.filter((iu) => {
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.
Webhooks
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.
Field | Description |
---|---|
Webhook URL | The URL of the POST endpoint that you’ve created in your app. |
Webhook Events | A 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:
window.parent.postMessage({
type: 'history.push',
route: 'files'
}, 'https://portalName.copilot.app');
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 dashboard.copilot.com
.
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:
window.parent.postMessage({
id: '<appId>',
type: 'history.push',
route: 'apps'
}, 'https://portalName.copilot.app');
Here’s a list of all the routes we support redirecting to:
Route | Supports ID Deep Linking? |
---|---|
apps | Yes |
billing | No |
contracts | No |
files | Yes |
forms | Yes |
helpdesk | Yes |
messages | Yes |
notifications | Yes |
settings | No |
settings.billing | No |
settings.profile | No |
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() {
noStore();
const api = await 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.
Updated about 2 months ago