Only this pageAll pages
Powered by GitBook
1 of 6

Pitchly Developer Documentation

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Introduction

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.

Plugin API

What is a plugin?

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 Unsplash app as a plugin (seen right)

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.

Prerequisites

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.

Initialization

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:

  1. When your app is ready to receive messages, the app must send Pitchly the ready message.

  2. In response to the ready message, Pitchly will send your app multiple messages indicating the current state of the UI.

  3. 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.

Workflow

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.

How to receive and send messages

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.

Plugin -> Pitchly messages

Ready

Send this when your plugin is ready to receive messages.

Selected records

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.

Pitchly -> Plugin messages

Current organization

Gets sent with the ID of the current organization.

Current person

Gets sent with the ID of the current person logged in.

Current workspace

Gets sent with the ID of the current workspace.

Current table

Gets sent with the ID of the current table being viewed.

Current table ready

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.

Current view

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.

Currently active data cell

Gets sent with the field and record ID of the currently active data cell (the cell highlighted with a border around it).

If there are no records in the current view, recordId and fieldId will both be null. If at least one data cell becomes available again, the first data cell will automatically become active again.

Selected records

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.

Record pagination

Gets sent with before or after cursor indicating where the current records data begins for the purposes of pagination.

Plugin focus state

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.

Security precautions

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.

Note that if you keep receiving errors such as "You do not have access to this resource" from your API calls when making GraphQL queries about the IDs you're receiving via postMessages, being logged into Pitchly and your plugin with different accounts can often be the cause.

provide us
postMessage events
GraphQL API
per-user OAuth flow
view
ready
view
personId
Interaction between Elements and Pitchly
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 } }

Pitchly Field Types/Returns

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:

Authentication

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.

Accessing data in your own organization

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:

I am generating a one-time access token to paste into other tools

The best way to generate a one-time access token is to use the online .

In the payload of your token, paste your Client ID into the iss claim. Then, where it says "your-256-bit-secret", paste in your Client Secret. Do not check "secret base64 encoded".

The resulting "Encoded" token on the left is your access token. Copy and paste it into your tool of choice or .

Note that, unless you add the exp claim to your payload, your access token will remain valid for as long as your Client Secret remains valid. In order to expire the token, you must rotate your Client Secret.

I am generating an access token programmatically

Many programming languages and frameworks offer prebuilt that make generating JWTs easy, such as or for Node.js.

Whether you use a prebuilt library or generate a JWT yourself, the JWT can be generated using the below pseudocode. Obviously, you will replace the client_id and client_secret with your own Client ID and Client Secret.

The resulting access token should look something like:

to view this JWT in the JWT Debugger.

Below are the supported claims that you can include in your payload:

Accessing data on behalf of a user

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.

Step #1: Redirect the user to our authorization page

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:

CLIENT_ID

This is the Client ID we gave you after registering your app.

REDIRECT_URI

This is the Redirect URL you gave us prior to registering your app. If you did not provide one, please so that we can register it with your app.

The Redirect URL must be the URL you wish to redirect users to after they have authorized your app with Pitchly. When we redirect to it, we will append a code to its query parameters, which you will eventually .

STATE

This should be a unique, randomly generated, non-guessable string. Before redirecting the user, put this value in a cookie, session, or localstorage in order to remember it when the user returns to your app.

When we redirect the user back to your app, we will append the state you gave us to the URL as a query parameter. Verify that this state parameter matches the value you have stored in cookies, session, or localstorage. If it matches, it means this is a valid request. If it doesn't match, it means this request was either unsolicited or forged, and you should not continue to the .

Below is browser code (without dependencies) that you can use to generate a secure random string and store it in localstorage:

CODE_CHALLENGE

While verifies that the same user who initiated the redirect from your app to Pitchly is the same user who returns to your app after we redirect back, the code_challenge parameter solves a similar but different problem.

We want to verify that the same user who initiated the redirect from your app to Pitchly is also the same user who ultimately attempts to exchange the returned code for an access token (which we will cover in the ).

To do this, we use a common standard called "" (pronounced "pixie"), which stands for "Proof Key for Code Exchange". This is an extension of the OAuth standard and aims to prevent forgery attacks.

Step #2: Exchange authorization code for an access token

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.

Exchange the authorization code for an access token

POST https://platform.pitchly.com/api/oauth/token

Content-Type: application/x-www-form-urlencoded

Request Body

Name
Type
Description

Response

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.

You will receive an

Where you make this request from will depend on the capabilities of your app. Choose your app type below:

My app has a server backend that can securely keep a secret

Your app is known as a "confidential client"

If your app has a server backend and can securely keep a secret, this request should be made on your server backend and you should include the client_secret we gave you in your request. This is the assumption of most apps by default.

My app is an SPA, Mobile, or Desktop app that cannot keep a secret

Your app is known as a "public client"

If your app is an SPA, Mobile, or Desktop app that cannot keep a secret, you can make this request from the frontend of your SPA or in your core application code without including a client_secret in the request. However, you must do the following:

Step #3: Refresh access token after expiration (optional)

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:

  1. Redirect the user back through the OAuth flow again (starting from ).

  2. 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.

Get a new access token using the refresh token

POST https://platform.pitchly.com/api/oauth/token

Content-Type: application/x-www-form-urlencoded

Request Body

Name
Type
Description

Response

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.

You will receive an invalid_client error and 401

Just like in , if your app has a server backend and can keep a secret, you must include your client_secret in the request. If your app cannot keep a secret, you must exclude the client_secret from the request.

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.

Accessing data in an organization that has installed your app, on behalf of your app (instead of a user)

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.

Get an access token to an organization on behalf of your app

POST https://platform.pitchly.com/api/oauth/token

Content-Type: application/x-www-form-urlencoded

Request Body

Name
Type
Description

Response

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.

You will receive an invalid_client error and 401 HTTP response code if you provided an invalid Client ID or Client Secret.

This request may only be made from a secure environment, such as a server, because it requires a client_secret. Public clients, such as SPAs, Mobile, or Desktop apps that do not have a server backend are prohibited from using this grant.

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.

Next: Make API calls with GraphQL

Now that you have an access token, you can make API calls against our GraphQL API. Click the link below to learn how.

Resources

Permissions

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.

Permission
Description
Click here for a list of all possible permissions.

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.

In a nutshell, PKCE requires these things (which at the end provides us a code_challenge):
  1. Generate a unique and random string (must be different from state). This will be known as our code_verifier.

  2. Store the code_verifier in cookies, session, or localstorage.

  3. 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.

View source

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_client
error and
401
HTTP response code if you provided an invalid Client ID or Client Secret.
Just a friendly reminder: Never share your client_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.

    HTTP response code if you provided an invalid Client ID or Client 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.

    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

    JWT Debugger
    learn how to make API requests with your access token
    packages and libraries
    this one
    this one
    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhcHAzR0FKaUxxaFlYNUZuU041NyIsImV4cCI6MTY4OTA4NzA0MCwiaWF0IjoxNjg5MDgzNDQwfQ.P9Q0d_vpBG0SnylWQU14GmhQrW2t2KXZmLjFR0bi3Q8
    Click here
    let us know
    exchange for an access token
    next step
    state
    next step
    PKCE
    get a new access token
    step 1
    authorization code response
    authorization code response
    authorization code response
    step 2
    step 1
    user flow
    user access token
    user access tokens
    GraphQL API
    HMACSHA256(
      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": ["..."]
    
    }
    View source
    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')."
    }
    More details

    GraphQL Filters

    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:

    Examples of Filters for Each Type

    Single-line Text Filter

    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:

    Number 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:

    Currency 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:

    Dropdown 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:

    Dropdown Multiple 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:

    Reference Multiple 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:

    Single Reference 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

    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:

    Attachment Filter

    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:

    Formula and Rollup Filters

    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.

    Other Filter Types

    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.

    Resources

    GraphQL Schema Documentation

    to view our full GraphQL schema documentation and all query options.

    GraphQL Explorer

    to access the GraphQL Explorer, a web interface where you can test GraphQL requests against our API straight from your browser!

    Pitchly Field Types/Returns

    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"
        }
        
        }
      })
    the instructions above
    Click here
    Click here
    Click here
    {
          "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"
        }
        
        }

    GraphQL API

    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.

    Why GraphQL?

    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.

    Do I need special software to make GraphQL calls?

    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.

    How do I make GraphQL calls?

    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.

    Example request

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

    Resources

    GraphQL Schema Documentation

    to view our full GraphQL schema documentation and all query options.

    GraphQL Explorer

    to access the GraphQL Explorer, a web interface where you can test GraphQL requests against our API straight from your browser!

    Pitchly Field Types/Returns

    to consult the full list of pitchly field types and sample return values for each type.

    access token
    Click here
    Click here
    Click here
    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
    }