Skip to content

Pagination

For performance reasons, queries that return multiple statements return only the first 100 statements matched by the query selection criteria. If you wish to retrieve more than 100 statements, you must page through the statements using the techniques described below. The OtterTax API uses a modified version of the connection specification defined by the Relay projecct.

Pagination is defined on all queries which return statements, both the generic statement object and the various distinct statement objects. These queries are:

  • getStatements
  • getF1098Statements
  • getF1098eStatements
  • getF1098tStatements
  • getF1099divStatements
  • getF1099intStatements
  • getF1099miscStatements
  • getF1099necStatements
  • getF1099rStatements
  • getF3921Statements
  • getF3922Statements
  • getF5498Statements
  • getFw2Statements

The following discussion uses the getStatements query to explain pagination, but the concepts apply to all of the queries in the preceding list. The discussion assumes that your query does not include any parameters and that you are therefore retrieving all statements. See the documentation for reviewing all statement types for information about limiting the scope of your query.

Query Outline

The format of the getStatements query is shown below. For the sake of readability, only the OtterTax ID of each statement is returned, but you can, obviously, return any other fields available on the statement.

query {
  getStatements {
    errors
    statements {
      pageInfo {
        hasNextPage
        endCursor
        hasPreviousPage
        startCursor
      }
      nodes {
        otxId
      }
      edges {
        cursor
        node {
          otxId
        }
      }
    }
  }
}
The query returns an errors object (which is returned by all OtterTax queries and mutations) along with a statements object.

The statements Object

The statements object is a connection object which accepts pagination parameters and returns a matching list of statements as both nodes and edges.

Statement Parameters

The statements object accepts four parameters: after, before, first, and last. Using a combination of these parameters allows you to page forward or backward through the list of matching statements.

Statement Return Values

The statements object returns the following three fields:

  • A pageInfo object which contains information about paging through the dataset.
  • A list of nodes, which, in this query, are statements.
  • A list of edges. Each edge contains a node along with a cursor for that node.

The PageInfo Object

The pageInfo object contains metadata required to page through the statements. It has four elements:

  • endCursor The cursor of the last statement returned in the list. When paging forward, use the value in this field as the after parameter in the next call.
  • hasNextPage Used when paging forward. True if there are additional statements to be retrieved, false otherwise.
  • hasPreviousPage Used when paging backward. True if there are additional statements to be retrieved, false otherwise.
  • startCursor The cursor of the first statement returned in the list. When paging backward, use the value in this field as the before parameter in the next call.

The Nodes List

The list of nodes returned by a call to getStatements is an array of statement objects. If you don't need cursor information for statements, use the nodes list to read statements.

The Edges List

The list of edges includes all of the information in the nodes list. It also includes a cursor value for every statement returned.

Pagination Methodology

To page through the list of statements, use the data returned in the pageInfo object to scroll through the dataset, either backward or forward.

Forward Pagination

To page forward through the statements, supply provide values for the first and after parameters. The value for first should be a non-negative integer indicating the number of statementst to return, and the value of after should be a string identifying the cursor of the statement after which the query should start returning statements. The statement denoted by the cursor will not be returned by the query. Use the pageInfo object or the cursor returned in the edges list to determine a value to supply to the after parameter.

Example

Supplying values of 75 and "MQ7" retrieves 75 statements after the statement with an associated cursor of "MQ7."

Backward Pagination

To page backward through the statements, supply provide values for the last and before parameters. The value for last should be a non-negative integer indicating the number of statementst to return, and the value of before should be a string identifying the cursor of the statement before which the query should start returning statements. The statement denoted by the cursor will not be returned by the query. Use the pageInfo object or the cursor returned in the edges list to determine a value to supply to the before parameter.

Example

Supplying values of 90 and "zR68" retrieves 90 statements before the statement with an associated cursor of "zR68."

Code Examples: Paging Through Records

The pseudocode below illustrates paging forward through a list of statements. It retrieves records in groups of 50, but the API supports batch sizes of up to 100.

set moreStatements = true
set endCursor = null
until moreStatements = false
  get 50 records
  get endCursor
  set moreStatements = hasNextPage
end

The code snippets below demonstrate various implementations of this pseudocode. See the documentation on authentication for information about obtaining the credential data passed in the header.

import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;

// This example uses version 2.9.1 of the
// open source gson library from Google.
// See https://github.com/google/gson.
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

// This example uses httpclient (version 4.5.13)
// and httpcore (version 4.4.13) libraries of the
// open source Apache HttpComponents project.
// See https://hc.apache.org/index.html.
import org.apache.http.entity.StringEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;

// Download the OtterTax java classes for the GraphQL
// query and mutation responses at
// https://github.com/OtterTax/graphql-java-classes
import com.ottertax.support.Statement;
import com.ottertax.support.GetStatementsResponse;

class Paginator {
  private String endpoint = "https://sandbox.ottertax.com/v2/graphql";
  private String accessToken = "YOUR ACCESS TOKEN";
  private String client = "YOUR CLIENT ID";
  private String uid = "YOUR UID";

  private String buildGQL(String endCursor) {
    StringBuilder sb = new StringBuilder();
    sb.append("query {\n" +
              "  getStatements {\n" +
              "    errors\n" +
              "    statements( first: 50");
    if(endCursor != null) {
      sb.append( " after: \"" + endCursor + "\"" );
    }
    sb.append(" ) {\n" +
              "      pageInfo {\n" +
              "        hasNextPage\n" +
              "        endCursor\n" +
              "        hasPreviousPage\n" +
              "        startCursor\n" +
              "      }\n" +
              "      nodes {\n" +
              "        otxId\n" +
              "      }\n" +
              "    }\n" +
              "  }\n" +
              "}\n");
    return(sb.toString());
  }

  private String querify(String rawGraphql) {
    Gson gson = new Gson();
    return("{\"query\":" + gson.toJson(rawGraphql) + "}");
  }

  private String getBatch(String endCursor) {
    CloseableHttpClient httpClient = HttpClients.createDefault();
    String response = "";

    try {
      HttpPost httpPost = new HttpPost(endpoint);
      httpPost.addHeader("content-type", "application/json");
      httpPost.addHeader("access-token", accessToken);
      httpPost.addHeader("client", client);
      httpPost.addHeader("uid", uid);

      String gql = buildGQL(endCursor);
      StringEntity stringEntity = new StringEntity(querify(gql));
      httpPost.setEntity(stringEntity);
      CloseableHttpResponse httpResponse = httpClient.execute(httpPost);

      BufferedReader reader = new BufferedReader(new InputStreamReader(
          httpResponse.getEntity().getContent()));
      StringBuffer responseBuffer = new StringBuffer();
      String inputLine;
      while ((inputLine = reader.readLine()) != null) {
        responseBuffer.append(inputLine);
      }
      reader.close();
      response = responseBuffer.toString();
      int responseCode = httpResponse.getStatusLine().getStatusCode();
      if( responseCode != 200  ) {
        System.out.println("Response code from server was " + String.valueOf(responseCode) + ".");
      }
      httpResponse.close();
      httpClient.close();
    } catch(IOException e) {
      System.out.println("Error posting GraphQL.\nExiting");
      System.exit(1);
    }
    return(response);
  }

  private void get() {
    int statementCount = 0;
    boolean moreStatements = true;
    String endCursor = null;
    Gson gson = new GsonBuilder().setPrettyPrinting().create();

    while(moreStatements == true) {
      String response = getBatch(endCursor);
      GetStatementsResponse getStatementsResponse = gson.fromJson(response, GetStatementsResponse.class);
      Statement[] statements = getStatementsResponse.getData().getGetStatements().getStatements().getNodes();
      statementCount += statements.length;
      endCursor = getStatementsResponse.getData().getGetStatements().getStatements().getPageInfo().getEndCursor();
      moreStatements = getStatementsResponse.getData().getGetStatements().getStatements().getPageInfo().getHasNextPage();
    }
    System.out.println("Retrieved " + String.valueOf(statementCount) + " statements.");
  }


  public static void main(String[] args) {
    Paginator paginator = new Paginator();
    paginator.get();
  }
}
// Using graphql-request from
// https://github.com/prisma-labs/graphql-request
// Example tested with node version 14.16.0
import { GraphQLClient, gql } from 'graphql-request'

async function main() {
  const endpoint = 'https://sandbox.ottertax.com/v2/graphql'

  const graphQLClient = new GraphQLClient(endpoint, {
    headers: {
      'access-token': 'YOUR ACCESS TOKEN',
      'client':       'YOUR CLIENT ID',
      'uid':          'YOUR UID'
    },
  })


  let statementCount = 0
  let moreStatements = true
  let endCursor = null

  while(moreStatements) {
    let parameters = ['first: 50']
    if(endCursor != null) {
      parameters.push( `after: "${endCursor}"` )
    }
    const query = gql`
      query {
        getStatements {
          errors
          statements( ${parameters.join( ' ' )} ) {
            pageInfo {
              hasNextPage
              endCursor
              hasPreviousPage
              startCursor
            }
            nodes {
              otxId
            }
          }
        }
      }
    `

    let data = await graphQLClient.request(query)
    let statements = data.getStatements.statements.nodes
    moreStatements = data.getStatements.statements.pageInfo.hasNextPage
    endCursor = data.getStatements.statements.pageInfo.endCursor
    statementCount += statements.length
  }
  console.log(`Retrieved ${statementCount} statements.`)
}

main().catch((error) => console.error(error))
<?php
// Tested with php-cli version 8.0.5.
$statement_count = 0;
$more_statements = TRUE;
$end_cursor = NULL;

while($more_statements == TRUE) {
  $parameters = array("first: 50");
  if($end_cursor != NULL) {
    array_push($parameters, "after: \"{$end_cursor}\"");
  }
  $argument_list = join( ' ', $parameters );
  $query =<<<"END_DATA"
    query {
      getStatements {
        errors
        statements( {$argument_list} ) {
          pageInfo {
            hasNextPage
            endCursor
            hasPreviousPage
            startCursor
          }
          nodes {
            otxId
          }
        }
      }
    }
  END_DATA;

  $payload = array ('query' => $query);
  $options = array(
    'http' => array(
      'method'  => 'POST',
      'content' => json_encode( $payload ),
      'header'=>  "Content-Type: application/json\r\n" .
                  "access-token: YOUR ACCESS TOKEN\r\n" .
                  "client:       YOUR CLIENT ID\r\n" .
                  "uid:          YOUR UID\r\n"
      )
  );

  $context  = stream_context_create( $options );
  $response = file_get_contents( 'https://sandbox.ottertax.com/v2/graphql',
                                false, $context );
  if( $response === FALSE ) {
    echo "Call to server failed.\n";
  } else {
    $json = json_decode( $response, true );
    $statements = $json['data']['getStatements']['statements']['nodes'];
    $more_statements = $json['data']['getStatements']['statements']['pageInfo']['hasNextPage'];
    $end_cursor = $json['data']['getStatements']['statements']['pageInfo']['endCursor'];
    $statement_count += count($statements);
  }
}
echo "Retrieved {$statement_count} statements.\n";
?>
# Using GQL from
# https://github.com/graphql-python/gql
# Tested using python version 3.8.8
from gql import gql, Client
from gql.transport.aiohttp import AIOHTTPTransport

transport = AIOHTTPTransport(url="https://sandbox.ottertax.com/v2/graphql",
                            headers={ 'access-token': 'YOUR ACCESS TOKEN',
                                      'client':       'YOUR CLIENT ID',
                                      'uid':          'YOUR UID' })
client = Client(transport=transport, fetch_schema_from_transport=True)
statement_count = 0
more_statements = True
end_cursor = None

while more_statements == True:
  parameters = ['first: 50']
  if not end_cursor == None:
    parameters.append(f'after: "{end_cursor}"')
  query = gql(
      """
        query {
          getStatements {
            errors
            statements( %s ) {
              pageInfo {
                hasNextPage
                endCursor
                hasPreviousPage
                startCursor
              }
              nodes {
                otxId
              }
            }
          }
        }
      """ % (" ".join(parameters))
  )

  result = client.execute(query)
  statements = result['getStatements']['statements']['nodes']
  more_statements = result['getStatements']['statements']['pageInfo']['hasNextPage']
  end_cursor = result['getStatements']['statements']['pageInfo']['endCursor']
  statement_count += len(statements)
print(f'Retrieved {statement_count} statements.')
# If you wish to use a library instead, see
# https://github.com/github/graphql-client
# Tested using ruby 2.7.2.
require( 'net/http' )
require( 'uri' )
require( 'json' )

uri = URI( "https://sandbox.ottertax.com/v2/graphql" )
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
headers = { 'Content-Type': 'application/json',
            'access-token': 'YOUR ACCESS TOKEN',
            'client':       'YOUR CLIENT ID',
            'uid':          'YOUR UID' }

statement_count = 0
more_statements = true
end_cursor = nil

while( more_statements ) do
  parameters = ['first: 50']
  if( end_cursor )
    parameters << "after: \"#{end_cursor}\""
  end
  query = <<-END_DATA
    query {
      getStatements {
        errors
        statements( #{parameters.join( ' ' )} ) {
          pageInfo {
            hasNextPage
            endCursor
            hasPreviousPage
            startCursor
          }
          nodes {
            otxId
          }
        }
      }
    }
  END_DATA
  request = Net::HTTP::Post.new(uri.request_uri, headers )
  request.body = {query: query}.to_json
  response = http.request(request)
  if( response.code == '200' )
    payload = JSON.parse( response.body )
    errors   = payload.dig( 'data', 'getStatements', 'errors' )
    if( errors.any? )
      STDOUT.puts( "Errors:\n#{errors.inspect}" )
    end
    statements = payload.dig( 'data', 'getStatements', 'statements', 'nodes' )
    more_statements = payload.dig( 'data', 'getStatements', 'statements', 'pageInfo', 'hasNextPage' )
    end_cursor = payload.dig( 'data', 'getStatements', 'statements', 'pageInfo', 'endCursor' )
    statement_count += statements.size
  else
    STDOUT.puts( "Response code was #{response.code}:\n#{response.inspect}" )
  end
end
STDOUT.puts( "Retrieved #{statement_count} statements." )