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:
-
Introduction to GraphQL - Official GraphQL documentation
-
How to GraphQL - Free tutorial for all skill levels
-
GraphQL Playground - Interactive GraphQL IDE for testing queries
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.
- The integrator token is issued by PCVisit during the integration setup process
- It should be stored securely and never exposed in client-side code or public repositories
- This token is used to authenticate your integration’s requests to the API
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.
- Users generate this token through their account settings in the PCVisit webapp
- Each user has their own unique user API token
- The token links your integration to a specific user’s account and permissions
- Users can revoke or regenerate their user API token at any time through the webapp
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:
- JWT Tokens - Obtained via the
SiRetrieveSignedJwtquery - 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:
- The integrator identity
- Allowed API paths
- User permissions
- Token expiration time
Important Notes:
- JWT tokens have a limited lifetime. You should implement token refresh logic to obtain new tokens before expiration.
- For JWT tokens, always use the
Bearerprefix in both REST and WebSocket connections. - For API tokens, use the
x-api-keyheader in REST requests, or pass directly asauthToken(withoutBearer) in WebSocket connection parameters. - The API will automatically detect whether you’re using a JWT token (starts with
Bearer) or an API token based on the format.
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:
-
Authentication Errors:
AccessDenied: Invalid or missing JWT tokenTokenExpired: JWT token has expiredInvalidValue: Invalid API tokens provided
-
Authorization Errors:
InsufficientRights: User lacks required permissionsAccessDenied: Access denied to the requested resource
-
Validation Errors:
InvalidValue: Invalid input parametersMissingValue: Required parameters are missingDoesNotExist: Requested resource does not exist
-
Server Errors:
ApplicationFailure: Internal application errorBackendFailure: Backend service error
-
Rate Limiting Errors:
TryAgainLater: Rate limit exceeded (for integrator API calls)- The
comment_for_devfield contains HTTP-styleRetry-Afterinformation - 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_devalso includes the configured rate limit (e.g., “Rate limit: 100 requests per minute”) - Important: When receiving a
TryAgainLatererror due to rate limiting, wait for the specified duration before retrying the request
- The
Error Handling Best Practices:
- Always check for the
Failuretype in union responses - Log the
comment_for_devfor debugging purposes - Display
comment_for_userto end users - Implement retry logic for transient errors:
- For
TimeOuterrors due to rate limiting, parse theRetry-Aftervalue fromcomment_for_devand wait before retrying - Extract the retry delay (in seconds) from the
Retry-Afterheader format - Use exponential backoff for other transient errors
- For
- Handle token expiration by refreshing the JWT
- Rate Limiting: Integrators are subject to per-integrator rate limits configured in the system. When a rate limit is exceeded, the API returns a
TimeOuterror with retry information in thecomment_for_devfield. Always respect theRetry-Aftertiming 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:
- Structure:
SessionProtocolEdge→nodes: [SessionProtocolNode]→value: SessionProtocol - Access Pattern: You must access session fields through
nodes { value { ... } }- you cannot query fields directly on the node - Common
SessionProtocolfields:id: TaskUuid- Unique session identifiername: SessionName- Session name/descriptionsessionCreatedAt: UnixTimeMs- When the session was created (Unix timestamp in milliseconds)sessionFinishedAt: UnixTimeMs- When the session finished (null if still active)sessionCompanyUuid: CustomerUuid- Company UUID associated with the sessionstate: SessionState- Current state of the session (e.g., “Open”, “Closed”)type: SessionType- Type of session (e.g., “Adhoc”, “RemoteHost”)duration: DurationMs- Calculated duration in millisecondscustomDuration: DurationMs- Custom duration override if setinvoiceState: TaskInvoiceStatus- Invoice statusparticipants: [SessionProtocolParticipant]- Array of session participantstarget: SessionProtocolMainTarget- Target computer/asset informationclientCompany: ClientCompany- Client company information if applicable
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:
company(required): The customer UUID of the company (type:CustomerUuid)timeFrame(optional): Filter sessions by time range (type:TimeFrame)startDate: Unix timestamp in milliseconds (inclusive)endDate: Unix timestamp in milliseconds (inclusive)- Note: Both timestamps must be in milliseconds since Unix epoch (January 1, 1970). For example, to query the last 7 days: calculate
endDateas current time in milliseconds, andstartDateas current time minus 7 days in milliseconds.
filter(optional): Additional filtering criteria (type:Filter)sort(optional): Array of sort criteria (type:Sort)search(optional): Text search parameters (type:Search)pagination(optional): Pagination parameters (type:Pagination)lengthStartingFromOffset(required): Number of items to retrieve starting from the offsetoffset(optional): Starting position for pagination (defaults to 0 if not provided)cursor(optional): Cursor for pagination (alternative to offset)
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:
- Initial Request: Make your first request without a cursor (or with
offset: 0) - Extract Cursor: Each node in the response contains a
cursorfield - use the cursor from the last node of the current page - Next Page: Use that cursor in the
pagination.cursorfield for the next request - Repeat: Continue using the cursor from the last node of each page until you’ve retrieved all data
Key Advantages of Cursor Pagination:
- Stability: Cursors remain valid even if new data is added or existing data is modified
- Performance: More efficient for large datasets as it doesn’t require counting through all previous items
- Consistency: Avoids duplicate or skipped items when data changes between page requests
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:
- Cursor vs Offset: You can use either
cursororoffset, but not both. If you provide acursor, theoffsetis ignored. - Cursor Format: The cursor is an opaque string - you should not try to parse or modify it. Always use it exactly as returned.
- Cursor Validity: Cursors are valid for the specific query parameters (sort order, filters, etc.). If you change sort order or filters, you must start from the beginning (without cursor).
- Always Include Cursor in Query: Make sure to include
cursorin your GraphQL query selection so you can retrieve it from the response. - Last Page: When the number of returned nodes is less than
lengthStartingFromOffset, you’ve reached the last page.
Return Types:
The query returns either:
SessionProtocolEdge- Contains the session data with pagination informationFailure- Contains error information if the query fails
For detailed work history reference, see: Work History Reference