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 JWT Debugger.

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 learn how to make API requests with your access token.

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 packages and libraries that make generating JWTs easy, such as this one or this one 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.

HMACSHA256(
  base64UrlEncode({ // header
    "alg": "HS256",
    "typ": "JWT"
  }) + "." +
  base64UrlEncode({ // payload
    "iss": "app3GAJiLqhYX5FnSN57", // client_id
    "exp": 1689087040,
    "iat": 1689083440
  }),
  "plys_h5qScFLwuT7zTGKIftVT_EeWrUpv3lIlC5Mc_xClDQ5" // signature - client_secret
)

The resulting access token should look something like:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhcHAzR0FKaUxxaFlYNUZuU041NyIsImV4cCI6MTY4OTA4NzA0MCwiaWF0IjoxNjg5MDgzNDQwfQ.P9Q0d_vpBG0SnylWQU14GmhQrW2t2KXZmLjFR0bi3Q8

Click here to view this JWT in the JWT Debugger.

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

{

    // 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": ["..."]

}

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.

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:

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>

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 let us know 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 exchange for an access token.

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 next step.

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

// 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);

View source

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

While state 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 next step).

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

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.

// 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);

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!

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

grant_type*

String

"authorization_code"

code*

String

The code you received

redirect_uri*

String

code_verifier*

String

client_id*

String

The client_id of your app

client_secret*

String

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.

Response

The response will include an access_token, refresh_token (which you can use to get a new access token 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.

{
    "access_token": "plyu_BASyd6ePeMAR1RHVbpWOxU7Bda9ze9i-rFWuk-dEAZW",
    "token_type": "Bearer",
    "expires_in": 3600,
    "refresh_token": "plyr_UQb-_2dHYpL7ExpHDkrLaTOrmUMYx27NYSDgeVNMdMo",
    "scope": "records:create records:update"
}

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.

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.

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:

  1. Let us know that your app is a public client, so we can register it accordingly.

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

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 step 1).

  2. Get a new access token using the refresh_token provided in the authorization code response.

The remainder of these instructions will focus on option #2.

Assuming you have held onto the refresh_token returned in the authorization code response, 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

grant_type*

String

"refresh_token"

refresh_token*

String

The stored refresh_token

client_id*

String

The client_id of your app

client_secret*

String

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.

Response

Response is identical to the authorization code response, 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.

{
    "access_token": "plyu_EcH6g3bSVdjHtAAYuUOy4pbvWYIPxhDD97FBEmw-PT9",
    "token_type": "Bearer",
    "expires_in": 3600,
    "refresh_token": "plyr_M5abea-_6JCE2kMRAHDjAWSISAbIXwWNEGwJlaUkbeq",
    "scope": "records:create records:update"
}

Just like in step 2, 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 step 1) 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 user flow 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

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

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.

Response

Unlike the response you receive for a user access token, 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.

{
    "access_token": "plyi_T4_71q61OKQzyJMRwlF8LH6KQ5wfuqJIcruT6OS-_a9",
    "token_type": "Bearer",
    "expires_in": 3600,
    "scope": "records:create records:update"
}

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 user access tokens, 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

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

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

Last updated