V2 Migration Guide

If you have previously used Pitchly's V2 API and would like to learn how to move to the V4 API, this guide can act as a cheatsheet to help you during that migration process, whether you're adding support for V4 or replacing your current V2 implementation with V4.

If you have any questions about migrating from V2 to V4, please don't hesitate to reach out to us. Our staff is happy to help in any way we can.

Authentication

In V2, you generate a Client ID and Secret by registering a new key set. In V4, you will need to contact us to get a Client ID and Secret, since we are rolling out our V4 API on a controlled basis temporarily.

Once you have a Client ID and Secret, the way you authenticate will depend on your desired flow:

I only want to use the API to access my own organization's data

In V2, this was previously accomplished by just passing your Client Secret as the secretKey parameter in your GraphQL variables.

In V4, we now use JSON Web Tokens (JWTs) to authenticate requests in cases where you want to use a one-time access token to your own organization. You will eventually pass this JWT as a Bearer token in the Authorization header of your GraphQL requests, as shown in the following steps. The benefits of using JWTs are:

  1. You can embed permission data in the access token itself

  2. You can customize usage details of the token, such as when the access token expires, by using claims embedded in the token

  3. Unlike secret keys, if your JWT is compromised, the attacker still wouldn't have access to your secret key, so they would be unable to generate new access tokens once your token expires or use your Client Secret for other nefarious purposes.

Click here to learn how to generate a JWT access token to access your own organization's data.

I want to use the API to access data in a client's organization

In both V2 and V4, you would typically use the OAuth authorization code flow to get an access token on a per-user basis, and then use that access token to get data from the user's organization. The process is nearly the same between both V2 and V4, with some exceptions:

  1. The authorization code flow in V4 now requires PKCE. This is an extension to the OAuth standard that helps prevent forgery attacks. To use PKCE, you must now include a code_challenge_method and code_challenge parameter in the initial authorization redirect, and the subsequent code exchange request will now require a code_verifier parameter. Click here for more details.

  2. The redirect_uri parameter is now required (in both the initial redirect and code exchange request), consistent with OAuth standards. Before, if not provided, we would use the first registered Redirect URI.

  3. The state parameter is now required in the initial authorize redirect.

  4. The organization_id and force_prompt parameters are no longer respected. The user has the ability to choose the account they want to authenticate with. If their organization has already approved access to your app, the user will be automatically authorized without being prompted.

In V2, access tokens would remain valid for two hours. In V4, this has been changed to one hour.

In V4, the authorization code flow can now be used regardless or whether you have a server backend or not, which was not the case in V2. To learn how to use it when you don't have a server backend (or your app otherwise can't securely keep a secret), click here.

Click here to learn how to use the authorization code flow in V4.

If your app used the implicit OAuth flow in V2 (response_type=token), this flow has been deprecated in accordance with updated OAuth standards in favor of the authorization code flow. In V4, the authorization code flow can now be used regardless of whether you have a server backend or not. In the past, the implicit flow was the only option if your app did not have a server backend. Learn more about how to implement the authorization code flow in V4.

How to pass access tokens

Once you have an access token, it's now time to use it.

In V2, depending on whether you were accessing data in your own organization or opted to use the OAuth authorization code flow, you would pass either your Client Secret directly in the secretKey parameter or your access token in the accessToken parameter in the variables of your GraphQL request.

In V4, you now pass all access tokens as a Bearer token in the Authorization header of every request, as shown below. See a full example request.

const headers = {
    "Content-Type": "application/json",
    "Authorization": `Bearer ${accessToken}`
};

The location of the GraphQL endpoint has also changed in V4. It is now:

https://api.pitchly.com/graphql

Error handling & token refresh

In V4, errors are also now delivered differently. Errors now take the following form:

{
  "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
}

In V2, you would normally receive a token-invalid error whenever a token was no longer valid. In V4, you will now receive an UNAUTHENTICATED error in extensions.code of the error response whenever the current access token is invalid.

If you are using the OAuth authorization code flow, when you receive an UNAUTHENTICATED error response, you have the option to refresh the user's access token by calling the "refresh_token" grant.

The process of refreshing access tokens is nearly identical from V2 to V4. The only thing to note is that, if you downscope access tokens using the scope parameter, the list of permissions you can pass have changed in V4. See the entire list of possible permissions.

GraphQL requests

While the process of using our GraphQL API is very similar between V2 and V4 and many of the core objects remain the same, the structure of requests have changed significantly to reflect the new capabilities and data structures in V4. This is also due in part to accommodate for scale. The V4 API utilizes more of the advantages of GraphQL, such as nested objects instead of only providing IDs to objects, which then must be separately queried, like in V2. You are generally able to get more data for less effort in V4.

Here, we include only a few common requests to show how they have changed from V2 to V4. To see the full documentation of our V4 API, visit our schema reference.

Getting Database/Table fields

Note that IDs are now represented by "id" instead of "_id". Also note that instead of only providing the database ID of reference fields, we now return the actual table object that reference fields refer to in V4.

Note: The terminology has changed between V2 and V4. We now refer to Databases as Tables in V4. See all terminology changes.

V2

query($secretKey: String!, $id: ID!) {
  database(secretKey: $secretKey, id: $id) {
    _id
    name
    fields {
      _id
      name
      description
      type
      primary
      required
      restrict
      database
    }
  }
}

V4

query($id: ID!) {
  table(id: $id) {
    id
    name
    fields {
      id
      name
      description
      type
      primary
      required
      ... on EnumField {
        choices
      }
      ... on ReferenceField {
        table {
          id
          name
        }
      }
    }
  }
}
Getting Records in a Database/Table

In V4, any property ending in "Connection" will return its data in a consistent pagination format. This format is a GraphQL standard for pagination. Note that the pagination strategy has been changed from skip/limit to a cursor-based pagination strategy that now uses first/after. first functions similarly to limit. after should generally be the last cursor of the previous result. recordsConnection will return results that come after the after cursor you specify.

If you don't want to return all the actual record data but would like to just get the end cursor, you can use pageInfo.endCursor as a shortcut to the last cursor on the current page. hasNextPage and hasPreviousPage are also boolean values that indicate whether or not there are more results before or after the current page. Note that you can also paginate backwards by using the parameters last and before, which function similarly to first and after except they allow you to paginate backwards.

totalCount also returns the total results matching the result set, not just the number of results on the current page. You can use this to get a total record count instead of having to make a separate request to dataCount.

Note that not all options for recordsConnection are shown here. Click here to see all parameter options.

V2

query($secretKey: String!, $databaseId: ID!, $filter: JSON, $sort: JSON, $limit: Int, $skip: Int, $in: [String], $fields: [String], $removedAt: Boolean, $search: String) {
  data(secretKey: $secretKey, databaseId: $databaseId, filter: $filter, sort: $sort, limit: $limit, skip: $skip, in: $in, fields: $fields, removedAt: $removedAt, $search: String) {
    _id
    cols {
      fieldId
      value
    }
  }
}

V4

query($tableId: ID!, $first: Int, $after: String) {
  recordsConnection(tableId: $tableId, first: $first, after: $after) {
    edges {
      node {
        id
        fields {
          fieldId
          stringValue
          value
        }
      }
      cursor
    }
    pageInfo {
      hasNextPage
      hasPreviousPage
      startCursor
      endCursor
    }
    totalCount
  }
}
Get a list of available Templates from Documents/Elements app

In V4, templates are now identified by ID instead of a path.

V2

query($secretKey: String!, $id: ID!) {
  database(secretKey: $secretKey, id: $id) {
    _id
    templates {
      path
      name
    }
  }
}

V4

query($id: ID!) {
  table(id: $id) {
    id
    ... on WorkspaceTable {
      contentTemplates {
        id
        name
      }
    }
  }
}
Get generated Content from the Documents/Elements app

In V2, the content for all templates were previously returned. In V4, you must specify the template. This change was made to increase the performance of results returned. If you wish to get the content for all templates still, you can iterate over all of the contentTemplates and get their associated content.

Notably, content is now returned as part of record data instead of being a standalone endpoint that replicated a lot of the functionality of records. This means you can now more easily get record data and content together in the same record object without making multiple requests, if you choose, and you can use all of the same parameters offered by recordsConnection.

There is also no longer a "default" template because the template name is no longer stored in the database in V4. We now also provide a "hash", which is guaranteed to be the same until either the template or data changes, resulting in new content. "path" was also changed to a template ID.

V2

query($databaseId: ID!, $secretKey: String!, $filter: JSON, $limit: Int, $skip: Int, $in: [String], $sort: JSON, $search: String, $templatePaths: [String]) {
  content(databaseId: $databaseId, secretKey: $secretKey, filter: $filter, limit: $limit, skip: $skip, in: $in, sort: $sort, search: $search, templatePaths: $templatePaths) {
    _id
    templates {
      name
      path
      pptxUrl
      imageUrl  
      default          
    }
  }
}

V4

query($tableId: ID!, $templateId: ID!) {
  recordsConnection(tableId: $tableId) {
    edges {
      node {
        id
        content(templateId: $templateId) {
          pptxURL
          hash
          imageURLs {
            original
          }
        }
      }
    }
  }
}

Terminology changes

Below are common product terms and their associated changes between V2 and V4.

Help is available

If you have any questions about migrating from V2 to V4, please don't hesitate to reach out to us. We are happy to help in any way we can, and we are with you every step of the way.

Last updated