Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
The goal of this documentation is to show you how to work with Pitchly's APIs from a developer's perspective.
To get started, you will need to register an app with Pitchly by contacting us. Once you have done this, we will send you a Client ID and Client Secret that you can use to acquire access tokens and authenticate requests against our API.
If you have already received your Client ID and Client Secret, proceed to the next page.
Pitchly apps take many different forms. Most of the time, apps exist outside of the Pitchly platform and either make API requests on behalf of the current user or on behalf of the organization that owns the app. But there is another type of app: what we call a "plugin".
A plugin is an app that sits side-by-side with the Pitchly platform in the same window, as shown in the example below. In this example, the Unsplash app allows users to search for stock photos and insert them directly into their table.
The benefit of making your app a plugin inside of Pitchly is that your app can intimately interact with the user's data tables in real time while the user is working inside Pitchly. This is advantageous for data-first workflows, where you expect the user to primarily work inside their database but you would like to extend their workflow with some functionality provided by your app.
The example below shows a more sophisticated workflow with our Elements app, where as the user filters their data and selects records, the elements in the right pane change to reflect the current view and selection. The reverse is also true: when the user selects elements in the right pane, the records are selected in the table on the left.
In order to serve your app as a plugin to Pitchly users, your interface must be publicly accessible at a specific URL you host. You will need to with that URL and let us know that you want to display it as a plugin. When users install your app and click on its button to the right of their workspace, we will load your plugin URL in an iFrame.
Plugins interact with the platform via a series of . In order to display your plugin and start receiving postMessage events from the platform, an initial handshake must first be performed:
When your app is ready to receive messages, the app must send Pitchly the ready message.
In response to the ready message, Pitchly will send your app multiple messages indicating the current state of the UI.
As the user interacts with Pitchly, your app will continue to receive individual messages when there are changes to the UI state.
Important: If your plugin is stuck on the loading page and not displaying, most likely Pitchly hasn't received the ready message from your plugin. We will only display your plugin once we receive the ready message, and we will additionally send back the current UI state when we receive the ready event. This handshake must be performed because Pitchly can't otherwise predict when your app is ready to receive events.
To send the ready event, simply call:
The second parameter ensures that you only send this message to Pitchly and no other websites that may have iFramed your plugin interface. It's important that you include this in order to keep your cross-iFrame communication secure.
On Pitchly's side, we similarly only send messages to the same origin as the plugin URL you originally gave us. This means if you redirect the user inside your iFrame to another origin, you will not be able to receive our postMessages.
Most commonly, when an event happens inside Pitchly, such as a filter being set or a record selection being made, this may trigger something to change inside your plugin's UI. You may expect that when a record is selected, for example, you will get the entire record data delivered to your app via postMessage. However, passing entire record data is not only inefficient but a potential security risk, as we don't yet have proof that your plugin should receive access to this user's data, and we don't want to trust only the browser to make this confirmation.
For this reason, postMessages themselves do not typically contain any sensitive information. You will normally receive only IDs to things, such as records, tables, the current user, etc. At most, you may receive the strings users have typed into filter conditions, for example.
In order to receive the actual data behind these IDs, you must make requests to our , just as you would if your app lived outside of Pitchly. For that to work, you must first possess proof that you have authorized access to this user's data. You do so by obtaining an access token.
Most of the time, the is most appropriate for plugins because the access it offers aligns with the current user who is using your plugin. After redirecting the user to the authorization endpoint and completing the OAuth flow, you can use the resulting access token to make API requests, informed by the postMessage events you receive.
To receive messages from Pitchly into your plugin, you will need to create a message event handler:
To send a message to Pitchly from your plugin, simply call the following:
Importantly, Pitchly will only start sending your plugin messages after you send Pitchly the "ready" message. So most often, it makes sense to call your ready event right after you initialize your message listener, since your message listener is now ready to receive messages.
Below you can find a list of all messages that can be sent between your plugin and Pitchly.
Send this when your plugin is ready to receive messages.
Send with a list of record IDs you would like to select in the current table.
If recordIds is null, indicates that a "select all" should be performed, which will select all records in the current , not just the records showing on the current page.
To clear the current selection, recordIds should be an empty array.
Gets sent with the ID of the current organization.
Gets sent with the ID of the current person logged in.
Gets sent with the ID of the current workspace.
Gets sent with the ID of the current table being viewed.
Gets sent with a boolean indicating whether the current table is "ready".
NOTE: This differs from the event that is sent to Pitchly to indicate the plugin is ready to receive messages. The main purpose of this "tableReady" event is to indicate whether the user has marked this table as "ready" to be accessible by non-admins and apps in the workspace. The intention behind this functionality is to provide admins with the opportunity to configure a table's "default view" before the table is made available more broadly. When a table isn't ready, your plugin will be unable to get data about the table from the GraphQL API, but this event is still provided so that you can accommodate in the UI by, for example, showing a banner to tell the user their table hasn't been marked as "ready" yet.
Gets the current view configuration representing the view of the data currently being displayed on the screen. Note that this does not represent a view being selected necessarily, but instead represents the current view config. So if the user changes filter or sort criteria manually, for example, you will receive another updated view representing that change in state.
When not empty, each property will be an object representing the associated item.
Gets sent with the field and record ID of the currently active data cell (the cell highlighted with a border around it).
Gets sent with a list of record IDs the user has selected in the current table.
If recordIds is null, indicates that a "select all" has been performed, which will select all records in the current , not just the records showing on the current page.
recordIds will be an empty array if no selection is made.
Gets sent with before or after cursor indicating where the current records data begins for the purposes of pagination.
Gets sent when the plugin becomes visible (isFocused true) or hidden (isFocused false). Note that when a plugin is closed in Pitchly, it may remain running in the background and continue to receive postMessages. Detecting the focus state is useful for doing something when the user returns to the plugin, or if you want to stop certain tasks from running while the plugin isn't visible.
One potential edge case to consider when displaying your plugin in Pitchly is that, sometimes, the Pitchly account the user used to log into your plugin may not match the account they're currently logged in with in Pitchly. You can easily check this scenario by verifying whether the we sent you matches the ID of the Person currently logged into your plugin. If they do not match, then you should log the user out of their Pitchly account in your plugin and have them re-login.
Although this isn't a security risk in the traditional sense of giving an unauthorized user access to your user's data, what could happen is one of the user's accounts could inadvertently share data about another account that the same physical user has also logged in with. While confidential data would not be shared, information such as filters used, view criteria, etc. could be shared just by the nature of how postMessages work and deliver information across frames.
While this is only a problem if the same user has multiple accounts, it is best to avoid this scenario altogether by simply checking whether the accounts between your plugin and Pitchly match, and automatically log the user out if they don't.

window.parent.postMessage({ type: "ready" }, "https://platform.pitchly.com");window.addEventListener("message", function(event) {
// Verify the message came from Pitchly
if (event.origin!=="https://platform.pitchly.com") return;
const message = event.data;
// Replace saveInLocalState with your own function to save message data.
// Messages from Pitchly are designed to work consistently with reactive
// frameworks, so you can expect to receive several messages right after
// you send the "ready" message to set the initial local state. Subsequent
// events will trigger only individual messages to be sent as needed,
// updating your local state as changes occur. Every message is guaranteed
// to contain at least a type (string) and data (object).
saveInLocalState(message.type, message.data);
});const message = { type: "ready" }; // replace with your message
window.parent.postMessage(message, "https://platform.pitchly.com");{ type: "ready" }{ type: "selectedRecords", data: { recordIds: [] || null } }{ type: "organization", data: { organizationId: null } }{ type: "person", data: { personId: null } }{ type: "workspace", data: { workspaceId: null } }{ type: "table", data: { tableId: null } }{ type: "tableReady", data: { isTableReady: false } }{ type: "view", data: { filter: null, sort: null, visibleFields: null, fieldOrder: null, columnWidths: null } }{ type: "activeCell", data: { recordId: null, fieldId: null } }{ type: "selectedRecords", data: { recordIds: [] || null } }{ type: "recordPagination", data: { before: null, after: null } }{ type: "focus", data: { isFocused: true } }
The following are all the field types in Pitchly, and sample return values when returned in a recordsConnection query.
Single-line Text: Short single-line text Sample Return Value: {"fieldId":"wksdMHiQmzTt6Dhf9N77|fldpLdoGAFzJae8BfTG7","value":{"val":"Name"},"stringValue":"Name"}
Number: Number with possible decimal Return Value: {"fieldId":"wksdMHiQmzTt6Dhf9N77|fldFawgZ4fMZC7PRhjbE","value":{"val":2300},"stringValue":"2,300"}
Yes/No: Binary yes/no value Return Value: {"fieldId":"wksdMHiQmzTt6Dhf9N77|fldPSYXsMQY9qhLZB2Cg","value":{"val":true},"stringValue":"Yes"}
Multi-line Text: Long multi-line text (can store clickable URLs) Return Value: {"fieldId":"wksdMHiQmzTt6Dhf9N77|fldpLdoGAFzJae8BfTG7","value":{"val":"longer text"},"stringValue":"longer text"}
Dropdown: Picklist of predefined choices (limited to one selection) Return Value: {"fieldId":"wksdMHiQmzTt6Dhf9N77|fldnkQKPuSETPib3CPYm","value":{"val":"Lincoln"},"stringValue":"Lincoln"}
Dropdown Multiple: Picklist of predefined choices (multiple selections available) Return Value: {"fieldId":"wksdMHiQmzTt6Dhf9N77|fldeZsZWHw7yYuBjCgTf","value":{"val":["Lincoln","Omaha"]},"stringValue":["Lincoln","Omaha"]}
Date: Date in the format MM/DD/YYYY Return Value:
Currency: Currency amount with symbol Return Value: {"fieldId":"wksdMHiQmzTt6Dhf9N77|fldxmggqWFyYsdTXjgRw","value":{"val":23045,"currency":"USD"},"stringValue":"$23,045"}
Attachment: Any type of file attachment, including images. type is the mime type of the file, and size is the size of the file in bytes. val is the URL of the file.
Return Value:
{"fieldId":"wksdMHiQmzTt6Dhf9N77|fldxmggqWFyYsdTXjgRw","value":{"val":"https://..., "type":"image/jpeg", size: 50000},"stringValue":"https://..."}
Reference: Creates a relationship/link to one record in another table Return Value: {"fieldId":"wksdMHiQmzTt6Dhf9N77|fldqrYiD9XpWw8AQkhEf","value":{"val":"wksdMHiQmzTt6Dhf9N77 recguqydmvmsWfCkJxHu"},"stringValue":"Jacob"}
Reference Multiple: Creates a relationship/link to multiple records in another table Return Value:
Lookup: Read-only subfield pulled from a referenced table Return Value:
Created At: Displays creation date and time Return Value:
Updated At: Displays date and time of last update Return Value:
Formula: Calculates values based on predefined logic Return Value:
Once you have a Client ID and Client Secret, the next step to making requests to the Pitchly API is to generate an access token that you can use to authenticate requests.
While your Client ID and Client Secret will generally remain the same for a long period of time, access tokens are usually short-lived. This makes your requests more secure.
Different types of access tokens can be generated for different types of use cases. Find your use case below and follow its instructions to learn how to generate an access token.
This is the most common use case for customers who want to access their own data via Pitchly’s API. Generating an access token for this use case is also the most straightforward.
The access token in this case is a JSON Web Token (JWT) containing your Client ID and is signed with your Client Secret. Optionally, you may also set an expiry time on the token, specify which workspaces the token will provide access to, and specify which permissions the token will have.
Choose your specific use case below:
This use case is the most common in situations where users log into your app, and you want to make API requests to Pitchly after the user has connected their Pitchly account.
In this section, you will learn how to authorize a user against Pitchly using OAuth to ultimately acquire an access token that can be used to make API requests against Pitchly.
Once the user is in your app, the first step is to redirect them to our authorization page. The URL will take the following format:
Click on each section below to learn how to populate each variable:
After the user successfully authorizes your app to access their Pitchly data, we will redirect the user to your redirect_uri with a code in the URL query parameters. This code is only valid for a short time and must be exchanged for an access token.
To exchange this code for an access token, you will need to make a POST request to our token endpoint. See the details of that request below.
POST https://platform.pitchly.com/api/oauth/token
Content-Type: application/x-www-form-urlencoded
The response will include an access_token, refresh_token (which you can use to once this one expires), how long the access token will be valid for in seconds via expires_in, and a space-delimited list of permissions this access token has via scope. Note that refresh tokens will remain valid until they are used.
You will receive a 400 HTTP response for any error, other than one caused by passing an invalid Client ID or Client Secret. In this case, you will receive a 401 HTTP response code.
Where you make this request from will depend on the capabilities of your app. Choose your app type below:
Access tokens are only valid for one hour. If you want to continue to access the user's Pitchly data after that time, you will need to get a new access token. You have two options to do that:
Redirect the user back through the OAuth flow again (starting from ).
Get a new access token using the refresh_token provided in the .
The remainder of these instructions will focus on option #2.
Assuming you have held onto the refresh_token returned in the , you can acquire a new access token by making the following request.
POST https://platform.pitchly.com/api/oauth/token
Content-Type: application/x-www-form-urlencoded
Response is identical to the , except you will receive a new access_token and a new refresh_token. Note that the old refresh token will be immediately invalidated, so you will want to replace the old refresh token with this one, so that you can get a new access token again once this one expires. The new refresh token will remain valid up until it is used to get a new access token again.
You will receive a 400 HTTP response for any error, other than one caused by passing an invalid Client ID or Client Secret. In this case, you will receive a 401 HTTP response code.
Each time your access token expires, you can repeat this step again to get a new access token. If this request fails, you can redirect the user back through the OAuth flow (from ) to try to get an access token again. This can happen if the organization has uninstalled your app and it must be approved again.
Most apps generally fall into one of the two use cases listed above. But if you’re trying to get data from an organization other than your own, the carries one significant limitation: it requires a user.
What do you do if you want to access data in an organization that has installed your app but your app doesn’t have a user interface to allow users to sign in, or you simply want to get data from the organization at any time, regardless of whether a user has signed in recently?
Some examples may include:
An analytics app that emails you statistics on your account periodically.
An image tracker that notifies you of images in your tables that are copyrighted.
An integration to an outside system that syncs your data automatically.
This is where the client_credentials grant can come in handy. To get an access token using this grant, make the following request.
POST https://platform.pitchly.com/api/oauth/token
Content-Type: application/x-www-form-urlencoded
Unlike the response you receive for a , this response will not include a refresh_token. Once this access token expires, you can get a new one by simply making this request again.
You will receive a 400 HTTP response for any error, other than one caused by passing an invalid Client ID or Client Secret. In this case, you will receive a 401 HTTP response code.
Using the returned access token (which we call an “install access token”), you may now make API calls to the desired organization on behalf of your app itself instead of on behalf of a specific user.
Compared to , note that install access tokens will differ in the following ways:
Install access tokens will have access to all workspaces the organization approves the app to access. User access tokens, in contrast, only have access to an intersection of workspaces the app has been approved access to + the workspaces the current user is a member of.
Install access tokens will not have access to anything that is private to a specific user, such as private views.
Keep these factors in mind if you plan to use a combination of user access tokens and install access tokens throughout your app, since one won't necessarily have access to the same resources as the other.
Now that you have an access token, you can make API calls against our GraphQL API. Click the link below to learn how.
Below are all possible permissions an app can have. The workspaces an app can access will depend on which workspaces the admin of the requested organization approves the app to access, as well as the type of access token that is generated.
Note: Specifying an empty array for workspace_ids is not the same as not setting the claim. Specifying an empty array means the token should not have access to any workspaces, while not specifying workspace_ids will automatically provide access to all workspaces.
Note: In cases where you want to pass along arbitrary data from before you redirect to Pitchly to after the user has returned to your app, you can sometimes use the state parameter to accomplish this. This can be done by base64-url encoding your data plus a random nonce, for example.
code_challenge):Generate a unique and random string (must be different from state). This will be known as our code_verifier.
Store the code_verifier in cookies, session, or localstorage.
Hash the code_verifier with the function BASE64_URL(SHA256(code_verifier)). This result is the code_challenge, which you will pass in the authorization URL.
Below is browser code (without dependencies) that can be used to generate your code_challenge and store your code_verifier in localstorage. Note that the generateRandomString function has been carried over from the state code example above.
Later, when the user returns to your app and you send us a request to exchange the returned code for an access token (which we will cover in the next step), you will pass the code_verifier from localstorage to that request. So hold onto it until you have an access token!
client_id*
String
The client_id of your app
client_secret*
String
The client_secret of your app. This is required unless your app is a public client - i.e. an SPA, Mobile, or Desktop app that cannot keep a secret. .
scope
String
A space-delimited list of downscoped permissions you want the resulting access token to have. Note that you can only request the access token have fewer permissions than approved by the organization admin, not more.
workspace_ids
String
A space-delimited list of downscoped workspaces you want the resulting access token to have access to. Note that you can only request the access token have access to fewer workspaces than approved by the organization admin, not more.
invalid_client401client_secret or use it in client-side code that could expose it to your end users, unless you want them to have total control of your app!Also note: It's safest to store both the resulting access_token and refresh_token you receive behind your server and proxy API calls using the access token via your server. But if you need to make API calls client side using the access token, you can do so as long as the access token is only shared with the user the access token is for. But remember: the access token will carry the full permissions the admin of the organization approved for your app unless you've requested the access token have fewer permissions. This means it is possible that the access token can do things that the user wouldn't normally be able to; for example, create or update records while the user only has read privilege. To counter this, you can either downscope the access token permissions by providing the scope option in your authorization code request or proxy API calls through your server.
The refresh_token, when possible, should always be stored server-side.
Let us know that your app is a public client, so we can register it accordingly.
If you are making the request from a browser, the page origin that initiated the request must match one of the origins in the list of valid Redirect URLs you gave us. Let us know if you need us to update your list of Redirect URLs.
Note: If you are initiating this request from a public client, make sure to NOT include the client_secret in your request, as this would also expose it to all of your users!
scope
String
A space-delimited list of downscoped permissions you want the resulting access token to have. Note that you can only request the access token have fewer permissions than approved by the organization admin, not more.
workspace_ids
String
A space-delimited list of downscoped workspaces you want the resulting access token to have access to. Note that you can only request the access token have access to fewer workspaces than approved by the organization admin, not more.
scope
String
A space-delimited list of downscoped permissions you want the resulting access token to have. Note that you can only request the access token have fewer permissions than approved by the organization admin, not more.
workspace_ids
String
A space-delimited list of downscoped workspaces you want the resulting access token to have access to. Note that you can only request the access token have access to fewer workspaces than approved by the organization admin, not more.
documents:read
Read content and templates in the Elements app
table:create
Create a new table in a workspace
table:modify
Modify table dropdown field choices
rootTable:create
Create a table in the organization
grant_type*
String
"authorization_code"
code*
String
The code you received
redirect_uri*
String
The same redirect_uri that was sent in step 1
code_verifier*
String
The code_verifier that was created in the process of generating the code_challenge in step 1
grant_type*
String
"refresh_token"
refresh_token*
String
The stored refresh_token
client_id*
String
The client_id of your app
client_secret*
String
The client_secret of your app. This is required unless your app is a public client - i.e. an SPA, Mobile, or Desktop app that cannot keep a secret. More details.
grant_type*
String
"client_credentials"
organization_id*
String
The organization you want to access data from
client_id*
String
The client_id of your app
client_secret*
String
The client_secret of your app
records:create
Create records in workspaces
records:update
Update records in workspaces
records:delete
Delete records in workspaces
workspace_members:read
Read members of workspaces
teams:read
Read teams across the organization
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhcHAzR0FKaUxxaFlYNUZuU041NyIsImV4cCI6MTY4OTA4NzA0MCwiaWF0IjoxNjg5MDgzNDQwfQ.P9Q0d_vpBG0SnylWQU14GmhQrW2t2KXZmLjFR0bi3Q8HMACSHA256(
base64UrlEncode({ // header
"alg": "HS256",
"typ": "JWT"
}) + "." +
base64UrlEncode({ // payload
"iss": "app3GAJiLqhYX5FnSN57", // client_id
"exp": 1689087040,
"iat": 1689083440
}),
"plys_h5qScFLwuT7zTGKIftVT_EeWrUpv3lIlC5Mc_xClDQ5" // signature - client_secret
){
// Your Client ID
"iss": "<CLIENT_ID>",
// Unix timestamp representing when this token was created.
// Many prebuilt libraries add this claim automatically.
"iat": 1689083440,
// Optional. Unix timestamp representing when this token will expire.
// If not given, the token will be valid as long as the Client Secret
// is valid.
"exp": 1689087040,
// Optional. A list of workspace IDs this token should have access to.
// If not set, this token will have access to all workspaces.
"workspace_ids": ["wks..."],
// Optional. A list of permissions this token should have.
// If not set, this token will have no permissions aside from read.
"permissions": ["..."]
}https://platform.pitchly.com/oauth/authorize?
response_type=code
&client_id=<CLIENT_ID>
&redirect_uri=<REDIRECT_URI>
&state=<STATE>
&code_challenge_method=S256
&code_challenge=<CODE_CHALLENGE>// Generate a secure random string using the browser crypto functions
function generateRandomString() {
var array = new Uint32Array(28);
window.crypto.getRandomValues(array);
return Array.from(array, dec => ('0' + dec.toString(16)).substr(-2)).join('');
}
// Create and store a random "state" value
var state = generateRandomString();
localStorage.setItem("state", state);{
"access_token": "plyu_BASyd6ePeMAR1RHVbpWOxU7Bda9ze9i-rFWuk-dEAZW",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "plyr_UQb-_2dHYpL7ExpHDkrLaTOrmUMYx27NYSDgeVNMdMo",
"scope": "records:create records:update"
}{
"error": "invalid_grant",
"error_description": "Please provide a valid 'code'."
}{
"access_token": "plyu_EcH6g3bSVdjHtAAYuUOy4pbvWYIPxhDD97FBEmw-PT9",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "plyr_M5abea-_6JCE2kMRAHDjAWSISAbIXwWNEGwJlaUkbeq",
"scope": "records:create records:update"
}{
"error": "invalid_grant",
"error_description": "Please provide a valid 'refresh_token'."
}{
"access_token": "plyi_T4_71q61OKQzyJMRwlF8LH6KQ5wfuqJIcruT6OS-_a9",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "records:create records:update"
}{
"error": "invalid_grant",
"error_description": "Please provide a valid 'organization_id'."
}{
"error": "invalid_client",
"error_description": "Please provide a valid 'client_id' (this may also be called your 'App ID')."
}// Generate a secure random string using the browser crypto functions
function generateRandomString() {
var array = new Uint32Array(28);
window.crypto.getRandomValues(array);
return Array.from(array, dec => ('0' + dec.toString(16)).substr(-2)).join('');
}
// Calculate the SHA256 hash of the input text.
// Returns a promise that resolves to an ArrayBuffer
function sha256(plain) {
const encoder = new TextEncoder();
const data = encoder.encode(plain);
return window.crypto.subtle.digest('SHA-256', data);
}
// Base64-urlencodes the input string
function base64urlencode(str) {
// Convert the ArrayBuffer to string using Uint8 array to conver to what btoa accepts.
// btoa accepts chars only within ascii 0-255 and base64 encodes them.
// Then convert the base64 encoded to base64url encoded
// (replace + with -, replace / with _, trim trailing =)
return btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
// Return the base64-urlencoded sha256 hash for the PKCE challenge
async function pkceChallengeFromVerifier(v) {
hashed = await sha256(v);
return base64urlencode(hashed);
}
// Create and store a new PKCE code_verifier (the plaintext random secret)
var code_verifier = generateRandomString();
localStorage.setItem("pkce_code_verifier", code_verifier);
// Hash and base64-urlencode the secret to use as the challenge
var code_challenge = await pkceChallengeFromVerifier(code_verifier);{
"error": "invalid_client",
"error_description": "Please provide a valid 'client_id' (this may also be called your 'App ID')."
}{
"error": "invalid_client",
"error_description": "Please provide a valid 'client_id' (this may also be called your 'App ID')."
}














Filters are used in the recordsConnection query to specify criteria for specific records to be returned. They consist of a series of one or more conditions with a logical operator of "and" or "or." Filters can contain condition groups: nested conditions that take place of each condition in a filter. The primary example shown below of a request body and filter contains a condition group to illustrate the use of this grouping, but it is not necessary to use in your filter. The easiest way to create a filter is to go to the platform UI and create a filter, and then copy that filter criteria to use in your query. Note that reference subfield filters cannot be constructed this way and are only available in the API, so please refer to the documentation below for them. More about each filter type, including the reference subfield filters, is shown below this section.
Follow these steps to copy the filter from your UI: To go to the UI, open up the platform, go into a workspace table, and click the filter button here.
Once you have created a filter in the UI, you can copy the filter from the browser console and use it in a graphQL request inside the variables for that request. Here is an example of a browser console log showing the filter: And here is an example of a graphQL query body consistent with the example request shown in GraphQL API to filter a set of records. Note that it includes the filter along with any other variables pertinent to the query. It also includes a condition group to illustrate how this is used:
The single-line text filter can be created in the console log as explained above. It supports the of "contains," "does-not-contain," "is," "is-not," "starts-with," "ends-with," "is-empty," "has-any-value" operators. A sample is given here for a "contains" filter:
The number filter can be created in the console log as explained above. It supports "is," "is-not," "is-more-than," "is-less-than," "is-empty," "has-any-value". A sample is given here for a "is-more-than" filter:
The currency filter can be created in the console log as explained above. Similar to the number filter, it supports "is," "is-not," "is-more-than," "is-less-than," "is-empty," "has-any-value". Unlike the number filter, the currency is included in the right object. Without matching the currency, the row will not be returned for this filter. A sample is given here for a "is-more-than" filter:
The dropdown filter can be created in the console log as explained above. It supports the of "has-any-of," "is", 'has-none-of", "is-empty," "has-any-value" operators. A sample is given here for a "has-any-of" filter:
The dropdown multiple filter can be created in the console log as explained above. It supports the of "has-any-of," "has-all-of," "is", 'has-none-of", "is-empty," "has-any-value" operators. A sample is given here for a "has-any-of" filter:
The reference multiple filter can be created in the console log as explained above. Similar to dropdown multiple it supports the of "has-any-of," "has-all-of," "is", 'has-none-of", "is-empty," "has-any-value" operators. The values specified are the recordID. A sample is given here for a "has-any-of" filter:
The reference filter can be created in the console log as explained above. It supports "is," "is-not," "is-empty," "has-any-value". The value specified is the recordID. A sample is given here for an "is" filter:
Reference subfield filters allow you to set a filter on a reference field in the current table that includes a filter for the referenced row (usually in another table). It uses the dot notation for fields to separate the reference field ID and the ID of the field in the referenced table. The field in the other table is filtered according to its type and operators available for that field. These filters are *only* available in the API, and not in the platform UI. They cannot contain more than one dot and so therefore cannot follow a reference of a reference. A sample is given here for a contains filter on a single-line text subfield:
The attachment filter can be created in the console log as explained above. It supports the "is-empty" and "has-any-value" operators. A sample is given here for a "is-empty" filter:
The formula and rollup filters can be created in the console log as explained above. They support the "contains," "does-not-contain," "is," "is-not," "starts-with," "ends-with," "is-empty," "has-any-value" operators, as well as the "is-more-than," and "is-less-than" operators. For brevity examples are not given, but please refer to the single-line text and number filters for explicit examples.
If the type of filter needed isn't listed here, please contruct the filter in the platform UI with and use that as an example to build your filter.
to view our full GraphQL schema documentation and all query options.
to access the GraphQL Explorer, a web interface where you can test GraphQL requests against our API straight from your browser!
to consult the full list of pitchly field types and sample return values for each type.
body: JSON.stringify({
query: `
query ExampleQuery($tableId: ID!, $filter: JSON) {
recordsConnection(tableId: $tableId, filter: $filter) {
edges {
node {
id
fields {
fieldId
value
stringValue
}
}
}
}
}
`,
variables: {
"tableId": "wksZbcuvFwooPY2JSbRD|tblDAfBe745NNcTHswrQ",
"filter": {
"conditions": [
{
"left": {
"type": "field",
"value": "wksARcgrLaJkqc2zvzKA|fldDXnDqrwoz2PoPF3ft"
},
"comparison": {
"operator": "contains"
},
"right": {
"type": "input",
"value": "uy"
}
},
{
"conditionGroup": [
{
"left": {
"type": "field",
"value": "wksARcgrLaJkqc2zvzKA|fldDXnDqrwoz2PoPF3ft"
},
"comparison": {
"operator": "contains"
},
"right": {
"type": "input",
"value": "a"
}
},
{
"left": {
"type": "field",
"value": "wksARcgrLaJkqc2zvzKA|fldDXnDqrwoz2PoPF3ft"
},
"comparison": {
"operator": "contains"
},
"right": {
"type": "input",
"value": "mp"
}
}
],
"logicalOperator": "or"
}
],
"logicalOperator": "or"
}
}
})

{
"tableId": "wksARcgrLaJkqc2zvzKA|tblnJkpPvq9RaJcgd3Fn",
"filter": {
"conditions": [
{
"left": {
"type": "field",
"value": "wksARcgrLaJkqc2zvzKA|fldDXnDqrwoz2PoPF3ft"
},
"comparison": {
"operator": "contains"
},
"right": {
"type": "input",
"value": "asdf"
}
}
],
"logicalOperator": "and"
}
}{
"tableId": "wksARcgrLaJkqc2zvzKA|tblnJkpPvq9RaJcgd3Gn",
"filter": {
"conditions": [
{
"left": {
"type": "field",
"value": "wksARcgrLaJkqc2zvzKA|fldDXnDqrwoz2PoPF3fx"
},
"comparison": {
"operator": "is-more-than"
},
"right": {
"type": "input",
"value": 3000
}
}
],
"logicalOperator": "and"
}
}{
"tableId": "wksARcgrLaJkqc2zvzKA|tblnJkpPvq9RaJcgd3Gn",
"filter": {
"conditions": [
{
"left": {
"type": "field",
"value": "wksARcgrLaJkqc2zvzKA|fldDXnDqrwoz2PoPF3fx"
},
"comparison": {
"operator": "is-more-than"
},
"right": {
"type": "input",
"value": 3000
"currency": "USD"
}
}
],
"logicalOperator": "and"
}
}{
"tableId": "wksARcgrLaJkqc2zvzKA|tblnJkpPvq9RaJcgd3Fn",
"filter": {
"conditions": [
{
"left": {
"type": "field",
"value": "wksARcgrLaJkqc2zvzKA|fldDXnDqrwoz2PoPF3ft"
},
"comparison": {
"operator": "has-any-of"
},
"right": {
"type": "input",
"value": ['Lincoln']
}
}
],
"logicalOperator": "and"
}
}{
"tableId": "wksARcgrLaJkqc2zvzKA|tblnJkpPvq9RaJcgd3Fn",
"filter": {
"conditions": [
{
"left": {
"type": "field",
"value": "wksARcgrLaJkqc2zvzKA|fldDXnDqrwoz2PoPF3ft"
},
"comparison": {
"operator": "has-any-of"
},
"right": {
"type": "input",
"value": ['Lincoln', 'Omaha']
}
}
],
"logicalOperator": "and"
}
}{
"tableId": "wksARcgrLaJkqc2zvzKA|tblnJkpPvq9RaJcgd3Fn",
"filter": {
"conditions": [
{
"left": {
"type": "field",
"value": "wksARcgrLaJkqc2zvzKA|fldDXnDqrwoz2PoPF3ft"
},
"comparison": {
"operator": "has-any-of"
},
"right": {
"type": "input",
"value": ['wksARcgrLaJkqc2zvzKA|recDRQYYXT6FLAiatC7x', 'wksARcgrLaJkqc2zvzKA|recFYinCfj79cmsJHaCN']
}
}
],
"logicalOperator": "and"
}
}{
"tableId": "wksARcgrLaJkqc2zvzKA|tblnJkpPvq9RaJcgd3Fn",
"filter": {
"conditions": [
{
"left": {
"type": "field",
"value": "wksARcgrLaJkqc2zvzKA|fldDXnDqrwoz2PoPF3ft"
},
"comparison": {
"operator": "is"
},
"right": {
"type": "input",
"value": "wksARcgrLaJkqc2zvzKA|recDRQYYXT6FLAiatC7x"
}
}
],
"logicalOperator": "and"
}
}{
"tableId": "wksARcgrLaJkqc2zvzKA|tblnJkpPvq9RaJcgd3Fn",
"filter": {
"conditions": [
{
"left": {
"type": "field",
"value": "wksARcgrLaJkqc2zvzKA|fldRLwBvjeqHZTEJXASe.wksARcgrLaJkqc2zvzKA|fldDXnDqrwoz2PoPF3ft"
},
"comparison": {
"operator": "contains"
},
"right": {
"type": "input",
"value": "Bo"
}
}
],
"logicalOperator": "and"
}
}{
"tableId": "wksARcgrLaJkqc2zvzKA|tblnJkpPvq9RaJcgd3Fn",
"filter": {
"conditions": [
{
"left": {
"type": "field",
"value": "wksARcgrLaJkqc2zvzKA|fldDXnDqrwoz2PoPF3fz"
},
"comparison": {
"operator": "is-empty"
},
"right": {}
}
],
"logicalOperator": "and"
}
}Equipped with an access token, you may now use it to make requests to our GraphQL API.
If you've never used GraphQL before, below you will find everything you need to make requests. If you're already familiar with GraphQL or are ready to start making GraphQL requests, you can head over to our GraphQL schema documentation to find example queries and test them using the Explorer.
We chose to deliver our API with GraphQL because, compared to traditional REST endpoints, GraphQL allows you to specify exactly the data you want returned, and it enables fetching complex data structures in one request instead of many smaller requests. GraphQL also supports real-time updates via GraphQL subscriptions with websockets out of the box.
No. Our GraphQL API can be called in the same way you make REST API calls, by just sending a request to an HTTP endpoint.
Compared to typical REST APIs, all requests to GraphQL are POST requests to the following endpoint:
In the headers of your request, you must include:
Replace ${accessToken} with your .
Next, the body of your request should be a JSON-encoded object containing your GraphQL query and any applicable variables in the following format.
When you receive a response back to your request, the response will take the following form:
Note: Unlike most REST APIs, GraphQL will always return a 200 OK HTTP status code, even if your query failed. You can instead determine if an error occurred by checking whether data in the response is null.
The following example demonstrates how you would get a list of tables in a specific workspace using the Pitchly GraphQL API in Node.js.
A successful response for this request may look something like this:
If the request had failed due to an invalid access token, for example, data would have looked something like this (and you still would have received a 200 OK HTTP status code):
to view our full GraphQL schema documentation and all query options.
to access the GraphQL Explorer, a web interface where you can test GraphQL requests against our API straight from your browser!
to consult the full list of pitchly field types and sample return values for each type.
https://api.pitchly.com/graphql{
"Content-Type": "application/json",
"Authorization": `Bearer ${accessToken}`
}{
query: "...", // your GraphQL query as a string
variables: { ... } // any applicable variables to include in the query (optional)
}// if successful
{
"data": { ... }
}
// if error
{
"errors": [ ... ],
"data": null
}// third-party fetch package for making HTTP calls
import fetch from "node-fetch";
const response = await fetch("https://api.pitchly.com/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json", // must be included in GraphQL requests
"Authorization": "Bearer " + accessToken
},
body: JSON.stringify({
query: `
query workspace($id: ID!) {
workspace(id: $id) {
tables {
id
name
}
}
}
`,
variables: {
id: workspaceId
}
})
});
// GraphQL will always return a 200 OK status code,
// even when there is an error. So first check that
// the HTTP status code is 200, and then check that
// "data" exists in the response, indicating success.
if (!response.ok) {
throw new Error("HTTP request returned error status code.");
}
// decode response json
const data = await response.json();
if (!data.data) {
throw new Error("GraphQL query failed.");
}
console.log(data);{
"data": {
"workspace": {
"tables": [
{
"id": "wksEtbAYd6bcTKigHGm4|tbloJnfkHi85WscsjTvG",
"name": "Companies"
},
{
"id": "wksEtbAYd6bcTKigHGm4|tblR59H2hbufDMpp5BTp",
"name": "Matters"
},
{
"id": "wksEtbAYd6bcTKigHGm4|tblqafbaJW9ZqeiZbJ9Q",
"name": "Employees"
}
]
}
}
}{
"errors": [
{
"message": "You must be authenticated to access this resource. Please provide a valid Bearer Token in the Authorization header.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"workspace"
],
"extensions": {
"code": "UNAUTHENTICATED"
}
}
],
"data": null
}