Developer Manual for API Integrators

Overview

This API provides a GraphQL-based interface for integrators to interact with the system. The API supports authentication via JWT tokens and provides access to various resources including work history, company data, and session information.

The API is accessible via a GraphQL endpoint, allowing integrators to query and mutate data according to their configured permissions.

API Endpoint: /graphql

For detailed API reference documentation, see: API Reference

Getting Started with GraphQL

If you’re new to GraphQL, here are some helpful resources:

GraphQL is a query language that allows you to request exactly the data you need. Unlike REST APIs, you can fetch multiple resources in a single request and avoid over-fetching data. This makes it ideal for building efficient prototypes and production applications.

Authentication

API Token Sources

Before you can authenticate with the API, you need to understand where the required tokens come from:

Integrator API Token

The integrator API token is provided by PCVisit directly in a secure manner. This token identifies your integration and grants access to the API on behalf of your organization.

User API Token

The user API token is created by the end user themselves using the PCVisit web application. This token represents the user’s consent to allow your integration to access their data.

Important: Both tokens are required to obtain a JWT token. The integrator token authenticates your integration, while the user token ensures you’re acting on behalf of a specific user with their explicit consent.

How to Query for a JWT

To authenticate with the API, integrators must first obtain a JWT token using their API tokens. This is done through the SiRetrieveSignedJwt GraphQL query.

GraphQL Query:

query GetJWT($userApiToken: UserApiToken!, $integratorApiToken: IntegratorApiToken!) {
  SiRetrieveSignedJwt(
    userApiToken: $userApiToken
    integratorApiToken: $integratorApiToken
  ) {
    ... on SignedJwtWasRetrieved {
      token
      customerUuid
    }
    ... on Failure {
      error
      comment_for_dev
      comment_for_user
    }
  }
}

cURL Example (with API token authentication):

curl -X POST https://api.example.com/graphql \
  -H "Content-Type: application/json" \
  -H "x-api-key: your-integrator-api-token" \
  -d '{
    "query": "query GetJWT($userApiToken: UserApiToken!, $integratorApiToken: IntegratorApiToken!) { SiRetrieveSignedJwt(userApiToken: $userApiToken, integratorApiToken: $integratorApiToken) { ... on SignedJwtWasRetrieved { token customerUuid } ... on Failure { error comment_for_dev comment_for_user } } }",
    "variables": {
      "userApiToken": "your-user-api-token",
      "integratorApiToken": "your-integrator-api-token"
    }
  }'

Note: The SiRetrieveSignedJwt query itself requires authentication. You can authenticate using your API tokens via the x-api-key header, or if you already have a JWT token, you can use the Authorization: Bearer header.

Alternative cURL Example (with existing JWT token):

curl -X POST https://api.example.com/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -d '{
    "query": "query GetJWT($userApiToken: UserApiToken!, $integratorApiToken: IntegratorApiToken!) { SiRetrieveSignedJwt(userApiToken: $userApiToken, integratorApiToken: $integratorApiToken) { ... on SignedJwtWasRetrieved { token customerUuid } ... on Failure { error comment_for_dev comment_for_user } } }",
    "variables": {
      "userApiToken": "your-user-api-token",
      "integratorApiToken": "your-integrator-api-token"
    }
  }'

Response:

{
  "data": {
    "SiRetrieveSignedJwt": {
      "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
      "customerUuid": "customer-uuid-here"
    }
  }
}

For detailed authentication reference, see: Authentication Reference

How to Use Authentication Tokens

The API supports two authentication methods:

  1. JWT Tokens - Obtained via the SiRetrieveSignedJwt query
  2. API Tokens - User API token and integrator API token (used to obtain JWT tokens)

Both methods can be used with REST/HTTP requests and WebSocket connections.

REST/HTTP Authentication

Using JWT Tokens

For REST/HTTP requests, include the JWT token in the Authorization header with the Bearer prefix:

Header Format:

Authorization: Bearer <your-jwt-token>

cURL Example:

curl -X POST https://api.example.com/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -d '{
    "query": "query { ... }"
  }'

JavaScript Example:

fetch('https://api.example.com/graphql', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${jwtToken}`
  },
  body: JSON.stringify({
    query: 'query { ... }'
  })
});
Using API Tokens

For REST/HTTP requests, include the API token in the x-api-key header:

Header Format:

x-api-key: <your-api-token>

Note: API tokens can also be passed as query parameters, but using the header is recommended for security.

cURL Example:

curl -X POST https://api.example.com/graphql \
  -H "Content-Type: application/json" \
  -H "x-api-key: your-api-token" \
  -d '{
    "query": "query { ... }"
  }'

JavaScript Example:

fetch('https://api.example.com/graphql', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': apiToken
  },
  body: JSON.stringify({
    query: 'query { ... }'
  })
});

WebSocket Authentication

For WebSocket connections (GraphQL subscriptions), authentication is provided via connection parameters during the initial handshake.

Using JWT Tokens with WebSocket

When establishing a WebSocket connection, include the JWT token in the connectionParams with the Bearer prefix:

Connection Parameters:

{
  authToken: "Bearer <your-jwt-token>"
}

JavaScript Example (Apollo Client):

import { SubscriptionClient } from 'subscriptions-transport-ws';

const client = new SubscriptionClient('wss://api.example.com/graphql', {
  reconnect: true,
  connectionParams: () => ({
    authToken: `Bearer ${jwtToken}`
  })
});

Python Example:

from gql import Client, gql
from gql.transport.websockets import WebsocketsTransport

transport = WebsocketsTransport(
    url='wss://api.example.com/graphql',
    init_payload={'authToken': f'Bearer {jwt_token}'}
)
client = Client(transport=transport, fetch_schema_from_transport=True)
Using API Tokens with WebSocket

When establishing a WebSocket connection, include the API token in the connectionParams without the Bearer prefix:

Connection Parameters:

{
  authToken: "<your-api-token>"
}

JavaScript Example (Apollo Client):

import { SubscriptionClient } from 'subscriptions-transport-ws';

const client = new SubscriptionClient('wss://api.example.com/graphql', {
  reconnect: true,
  connectionParams: () => ({
    authToken: apiToken  // No "Bearer" prefix for API tokens
  })
});

Python Example:

from gql import Client, gql
from gql.transport.websockets import WebsocketsTransport

transport = WebsocketsTransport(
    url='wss://api.example.com/graphql',
    init_payload={'authToken': api_token}  # No "Bearer" prefix
)
client = Client(transport=transport, fetch_schema_from_transport=True)

Token Information

JWT Token Claims: The JWT token contains claims that define:

Important Notes:

Error Handling

Principles

The API uses a consistent error response format across all operations. All errors are returned as part of the GraphQL response using the Failure type.

Error Response Structure:

{
  "data": {
    "YourQuery": {
      "error": "ErrorIDs.AccessDenied",
      "comment_for_dev": "Detailed error message for developers",
      "comment_for_user": "User-friendly error message"
    }
  }
}

Common Error Scenarios:

  1. Authentication Errors:

    • AccessDenied: Invalid or missing JWT token
    • TokenExpired: JWT token has expired
    • InvalidValue: Invalid API tokens provided
  2. Authorization Errors:

    • InsufficientRights: User lacks required permissions
    • AccessDenied: Access denied to the requested resource
  3. Validation Errors:

    • InvalidValue: Invalid input parameters
    • MissingValue: Required parameters are missing
    • DoesNotExist: Requested resource does not exist
  4. Server Errors:

    • ApplicationFailure: Internal application error
    • BackendFailure: Backend service error
  5. Rate Limiting Errors:

    • TryAgainLater: Rate limit exceeded (for integrator API calls)
      • The comment_for_dev field contains HTTP-style Retry-After information
      • Format: Retry-After: <seconds> seconds (until <UTC date>)
      • Example: Retry-After: 5 seconds (until Mon, 01 Jan 2024 12:00:00 GMT)
      • The comment_for_dev also includes the configured rate limit (e.g., “Rate limit: 100 requests per minute”)
      • Important: When receiving a TryAgainLater error due to rate limiting, wait for the specified duration before retrying the request

Error Handling Best Practices:

  1. Always check for the Failure type in union responses
  2. Log the comment_for_dev for debugging purposes
  3. Display comment_for_user to end users
  4. Implement retry logic for transient errors:
    • For TimeOut errors due to rate limiting, parse the Retry-After value from comment_for_dev and wait before retrying
    • Extract the retry delay (in seconds) from the Retry-After header format
    • Use exponential backoff for other transient errors
  5. Handle token expiration by refreshing the JWT
  6. Rate Limiting: Integrators are subject to per-integrator rate limits configured in the system. When a rate limit is exceeded, the API returns a TimeOut error with retry information in the comment_for_dev field. Always respect the Retry-After timing to avoid further rate limit violations.

For detailed error reference, see: Error Reference

Functionality

How to Get Work History

The work history (session history) can be retrieved using the AcQuerySessionsHistory GraphQL query. This query returns a paginated list of session protocols for a given company.

GraphQL Query:

query GetWorkHistory(
  $company: CustomerUuid!
  $timeFrame: TimeFrame
  $filter: Filter
  $sort: [Sort!]
  $search: Search
  $pagination: Pagination
) {
  AcQuerySessionsHistory(
    company: $company
    timeFrame: $timeFrame
    filter: $filter
    sort: $sort
    search: $search
    pagination: $pagination
  ) {
    ... on SessionProtocolEdge {
      nodes {
        cursor
        value {
          id
          name
          sessionCreatedAt
          sessionFinishedAt
          sessionCompanyUuid
          state
          type
          duration
          customDuration
          invoiceState
          participants {
            supporterIdentity {
              id
            }
            supporterContact {
              email
            }
            isSessionCreator
          }
          target {
            computerId
            computerDisplayName
          }
          clientCompany {
            clientId
            clientName
          }
        }
      }
      totalCount
      batchLoadingIsStillActive
      reportingActiveSince
    }
    ... on Failure {
      error
      comment_for_dev
      comment_for_user
    }
  }
}

Important Schema Notes:

The AcQuerySessionsHistory query returns a SessionProtocolEdge which contains a nodes array. Each node is of type SessionProtocolNode, which wraps the actual session data in a value field:

cURL Example:

curl -X POST https://api.example.com/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -d '{
    "query": "query GetWorkHistory($company: CustomerUuid!, $timeFrame: TimeFrame, $pagination: Pagination) { AcQuerySessionsHistory(company: $company, timeFrame: $timeFrame, pagination: $pagination) { ... on SessionProtocolEdge { nodes { value { id name sessionCreatedAt sessionFinishedAt sessionCompanyUuid state } } totalCount batchLoadingIsStillActive reportingActiveSince } ... on Failure { error comment_for_dev comment_for_user } } }",
    "variables": {
      "company": "12345678-1234-1234-1234-123456789012",
      "timeFrame": {
        "startDate": 1609459200000,
        "endDate": 1640995200000
      },
      "pagination": {
        "offset": 0,
        "lengthStartingFromOffset": 10
      }
    }
  }'

Note: The Pagination type requires lengthStartingFromOffset (required) and optionally accepts offset and cursor. The first and after fields are not part of this API’s pagination schema.

Response:

{
  "data": {
    "AcQuerySessionsHistory": {
      "nodes": [
        {
          "cursor": "cursor_abc123",
          "value": {
            "id": "session-uuid-1",
            "name": "Support Session",
            "sessionCreatedAt": 1609459200000,
            "sessionFinishedAt": 1609462800000,
            "sessionCompanyUuid": "12345678-1234-1234-1234-123456789012",
            "state": "Closed",
            "type": "Adhoc",
            "duration": 3600000,
            "customDuration": null,
            "invoiceState": "Invoiced"
          }
        }
      ],
      "totalCount": 100,
      "batchLoadingIsStillActive": false,
      "reportingActiveSince": 1609459200000
    }
  }
}

Error Response Example:

{
  "data": {
    "AcQuerySessionsHistory": {
      "error": "AccessDenied",
      "comment_for_dev": "no matching allowed relationships found",
      "comment_for_user": "Sie haben nicht die nötige Berechtigung, um diese Funktion zu nutzen!"
    }
  }
}

Query Parameters:

Cursor-Based Pagination

The API supports cursor-based pagination, which is more efficient and reliable than offset-based pagination, especially for large datasets. Cursor pagination uses an opaque string (cursor) that represents a specific position in the sorted dataset.

How Cursor Pagination Works:

  1. Initial Request: Make your first request without a cursor (or with offset: 0)
  2. Extract Cursor: Each node in the response contains a cursor field - use the cursor from the last node of the current page
  3. Next Page: Use that cursor in the pagination.cursor field for the next request
  4. Repeat: Continue using the cursor from the last node of each page until you’ve retrieved all data

Key Advantages of Cursor Pagination:

Example: Using Cursor Pagination

First Request (Get first 10 items):

query GetWorkHistory($company: CustomerUuid!, $pagination: Pagination) {
  AcQuerySessionsHistory(
    company: $company
    pagination: $pagination
  ) {
    ... on SessionProtocolEdge {
      nodes {
        cursor
        value {
          id
          name
          sessionCreatedAt
        }
      }
      totalCount
    }
  }
}

Variables:

{
  "company": "12345678-1234-1234-1234-123456789012",
  "pagination": {
    "lengthStartingFromOffset": 10
  }
}

Response:

{
  "data": {
    "AcQuerySessionsHistory": {
      "nodes": [
        {
          "cursor": "cursor_abc123",
          "value": { "id": "session-1", "name": "Session 1", ... }
        },
        {
          "cursor": "cursor_def456",
          "value": { "id": "session-2", "name": "Session 2", ... }
        },
        // ... 8 more nodes
        {
          "cursor": "cursor_xyz789",
          "value": { "id": "session-10", "name": "Session 10", ... }
        }
      ],
      "totalCount": 100
    }
  }
}

Next Request (Get next 10 items using cursor):

{
  "company": "12345678-1234-1234-1234-123456789012",
  "pagination": {
    "cursor": "cursor_xyz789",  // Use cursor from the last node of previous page
    "lengthStartingFromOffset": 10
  }
}

cURL Example with Cursor:

# First page
curl -X POST https://api.example.com/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -d '{
    "query": "query GetWorkHistory($company: CustomerUuid!, $pagination: Pagination) { AcQuerySessionsHistory(company: $company, pagination: $pagination) { ... on SessionProtocolEdge { nodes { cursor value { id name } } totalCount } } }",
    "variables": {
      "company": "12345678-1234-1234-1234-123456789012",
      "pagination": {
        "lengthStartingFromOffset": 10
      }
    }
  }'

# Next page (using cursor from previous response)
curl -X POST https://api.example.com/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -d '{
    "query": "query GetWorkHistory($company: CustomerUuid!, $pagination: Pagination) { AcQuerySessionsHistory(company: $company, pagination: $pagination) { ... on SessionProtocolEdge { nodes { cursor value { id name } } totalCount } } }",
    "variables": {
      "company": "12345678-1234-1234-1234-123456789012",
      "pagination": {
        "cursor": "cursor_xyz789",
        "lengthStartingFromOffset": 10
      }
    }
  }'

JavaScript Example:

async function fetchAllSessions(companyUuid, jwtToken) {
  let allSessions = [];
  let cursor = null;
  let hasMore = true;

  while (hasMore) {
    const query = `
      query GetWorkHistory($company: CustomerUuid!, $pagination: Pagination) {
        AcQuerySessionsHistory(company: $company, pagination: $pagination) {
          ... on SessionProtocolEdge {
            nodes {
              cursor
              value {
                id
                name
                sessionCreatedAt
              }
            }
            totalCount
          }
        }
      }
    `;

    const variables = {
      company: companyUuid,
      pagination: {
        lengthStartingFromOffset: 50,
        ...(cursor && { cursor })
      }
    };

    const response = await fetch('https://api.example.com/graphql', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${jwtToken}`
      },
      body: JSON.stringify({ query, variables })
    });

    const result = await response.json();
    const edge = result.data.AcQuerySessionsHistory;

    if (edge.nodes && edge.nodes.length > 0) {
      allSessions.push(...edge.nodes.map(node => node.value));
      // Get cursor from the last node
      cursor = edge.nodes[edge.nodes.length - 1].cursor;
      // Check if we've retrieved all items
      hasMore = allSessions.length < edge.totalCount;
    } else {
      hasMore = false;
    }
  }

  return allSessions;
}

Important Notes:

Return Types:

The query returns either:

For detailed work history reference, see: Work History Reference