API V3 Documentation

Every call of the PQFORCE REST and JavaScript API V3, by module, with its data types.

PQFORCE 4.26.6 Preview

Preview: this documents PQFORCE 4.26.6, which has not been released yet.

Introduction

This article explains the core concepts of the PQFORCE API and shows how to authenticate and make your first calls. PQFORCE is a multi-tenant, cloud-based SaaS platform for project, resource and portfolio management, and virtually every capability offered by the web client is also reachable programmatically through this API. The API is a REST API served over HTTPS that exchanges data as JSON, so it can be consumed from any modern programming language or integration platform. This document describes version 3 (V3) of the API, whose endpoints all live under the /API/V3/ path.

The API can be accessed in four different ways, each suited to a particular scenario: the client API used by the PQFORCE browser client, the public API for third-party systems and integrations, the maintenance API for tenant-independent administrative tasks, and the JavaScript API available to server-internal automations. Each access kind has its own authentication mechanism, summarized in the table below and detailed in the following chapters. Because PQFORCE is multi-tenant, most requests are executed in the context of a specific tenant, which is identified by its tenant alias.

Access to data and endpoints is governed by a fine-grained permission model, and every call runs with the permissions of the user it is bound to. For security reasons, error responses deliberately contain only coded references rather than detailed messages; the Error codes chapter explains how to resolve these codes. A machine-readable description of the whole API is also available in the OpenApi format, which you can use to generate a client library automatically.

The remainder of this document is organized into three parts. It first covers the general concepts — authentication, parameters, error handling and security considerations — that apply to all endpoints. It then documents each endpoint, grouped by module, including its HTTP syntax, required permissions, parameters and result type. Finally, it describes the data types exchanged by those endpoints. If you only need to integrate a third-party system, focus on the public API chapter; if you are writing server-side automations, refer to the JavaScript API chapter.

The different kinds of REST API

The client API

This is the way how the browser accesses the PQFORCE API. It calls the initial redirect to set the tenant and does interactive login. During both calls, the session cookie will be updated. Authorization is then done through the session cookie. Note: PUT, POST and DELETE calls must be done by adding the Csrf-Token header to protect the session cookie against cross-site request forgery attacks.

The public API

This is the way how customer systems or interfaces should access the PQFORCE API. A token is created and registered. By passing the token in the Authorization: Bearer header, the call is authorized.

The maintenance API

This is the way how tenant-independent maintenance calls are being made. A user-specific certificate and its asymmetric key pair are used to secure the exchange: the request body is signed with the client's private key (and verified by the server against the certificate), while the response body is encrypted by the server with the client's public key so that only the client can read it. The base85 encoded certificate is added to the request by using the Pqforce-Certificate header. The result of a challenge that has to be solved by the client to call maintenance API endpoints must be present in the Pqforce-Challenge header.

The JavaScript API

Almost all parts of the API are accessible from within server-internal JavaScript automations. This API is internal and not accessible by HTTP.

HTTP Header presence

Client Public Maintenance
Cookie ✔️
Authorization ❌ ✔️ ❌
Pqforce-Certificate ❌ ❌ ✔️
Pqforce-Challenge ❌ ❌ ✔️
Csrf-Token (✔️)

✔️ = Required (✔️) = Required only for PUT, POST and DELETE calls ❌ = Forbidden

Using the public API by 3rd party systems

The API concept

PQFORCE provides a REST API over HTTPS with JSON serialization. REST means Representational State Transfer and is a stateless, well-known technology. Refer to different documentations on the internet about these kinds of APIs.

Machine-readable documentation

A documentation following the OpenApi 3.1 standard may be downloaded at https://cloud.pqforce.com/API/V3/MAIN/ApiDescription/OpenApi/Json (JSON format) or https://cloud.pqforce.com/API/V3/MAIN/ApiDescription/OpenApi/Yaml (YAML format). You may replace the host name by your host name if you're running a different version of PQFORCE on a dedicated instance or on premises. This documentation may be used to automatically provide an interface to PQFORCE in your favourite programming language. Refer to the documentation of your IDE (your programming environment) for further information how to do this.

Note: By adding ?modules=modulename (i.e. https://cloud.pqforce.com/API/V3/MAIN/ApiDescription/OpenApi/Yaml?modules=pm), it is possible to export everything you need to access a single module's endpoints. Multiple modules should be passed with double quotes and separated by a comma (i.e. https://cloud.pqforce.com/API/V3/MAIN/ApiDescription/OpenApi/Yaml?modules="pm","res")

The authentication concept

PQFORCE uses a bearer token authentication. By doing so, PQFORCE follows a standard way for API authentication that is widely used. The token can be retrieved by the consumer of the API (i.e. your application) and must be kept secret. Your app gets also a URL which allows a user to authorize the token and therefore opens access to PQFORCE by the API consumer (i.e. your application) owning that specifc token.

For security reasons, PQFORCE also offers the possibility to unauthorize a previously authorized token to deny further access (i.e. if the token might have been stolen or if a computing device was lost).

As a token is bound to the user that authorized it, the consumer (i.e. your application) can use the whole power of PQFORCE with the permissions of that user. Changes are annotated with the user's name as if he/she initiated these changes over the web client.

Retrieving and authenticating a token

  • Getting current PQFORCE version (optional, to check compatibility): GET API/V3/MAIN/Versions

  • Retrieving a new token: POST API/V3/ACM/ApiToken/Create?tenantAlias={tnt}.

    You have to replace {tnt} by the alias of your tenant. That means, if you're starting PQFORCE with the URL https://cloud.pqforce.com/mycompany, then mycompany would be your tenant alias.

    You have to provide information about the consumer (i.e. your app). This information will show up in PQFORCE when authorizing a token. Your request should look like the example below:

{
  "application": "MyApplication",
  "appVersion": "0.1",
  "appCompany": "MyCompany"
}
  • You should receive a HTTP 201 return code saying that a new token was created. You receive a JSON structure with the secret token, an authorization URL and an authorization deadline:
    {
      "apiToken": "thisismyverysecretapitoken",
      "authorizationUrl": "https://cloud.pqforce.com/mycompany?selid=9F7F48D73CDB4678B9F33AB1CFC637A2&authtokenid=8D22AFA7CEA049EFB8981B8993ADF1C4&seltype=ApiTokenList&id=ADM&type=Module",
      "authorizationDeadline": "2022-11-30T13:17:27.006Z"
    }
  • Open a browser and navigate to the given authorization URL (the consumer, i.e. your application, may open the browser automatically).
  • Authorize the token by checking the contents of the messagebox that appears and clicking 'Authorize':
  • Your token should now appear in the list of tokens.

Now, your consumer is ready to use the token.

Note: To unauthorize, just drag the list entry (the first cell) of your token onto the waste bin.

Using the token

Following the HTTP standard, the consumer (i.e. your application) must now add the secret token to every REST-API request (HTTP) by setting the Header Authorization to Bearer {yourtoken} (replace {yourtoken} by the secret token received earlier by calling POST API/V3/ACM/ApiToken/Create?tenantAlias={tnt}). Example HTTP request:

GET /API/V3/ACM/CurrentUser HTTP/1.1
Authorization: Bearer thisismyverysecretapitoken

WebSockets

Besides the REST endpoints, PQFORCE pushes notifications over WebSockets. A third party system gets one from POST API/V3/PF/ChangeNotification/CreateNew: it answers PF.WebSocketInfo with the URL to connect to, which is valid for a limited time only. On the WebSocket, the server then informs you about changed objects. Every message, in either direction, is a JSON object.

Keeping a WebSocket open

The server closes a WebSocket on which no message has been exchanged for 75 seconds; proxies between you and the server may close it even sooner. The server does not send keep-alive messages itself, so a quiet connection is closed even though nothing is wrong with it. To keep the WebSocket open, your client must send an empty message, the empty JSON object {}, at regular intervals; every 30 seconds is a safe choice. The server accepts and ignores it, and an empty text frame is tolerated just as well. A text that is not JSON at all, however, makes the server close the WebSocket (see below). A client should nevertheless be prepared for the connection to drop, and reconnect with a new URL when it does.

WebSocket close codes

When the server closes a WebSocket, the close frame carries one of these codes:

Code Meaning
1000 Regular closure: the server ended the WebSocket, or it confirms a close the client initiated
1002 Protocol error: the client violated RFC 6455, e.g. with an unmasked frame or a continuation frame that does not continue anything
1003 Unacceptable data: a message could not be parsed as JSON
1006 Abnormal closure: never sent by the server, but reported by your WebSocket library when the connection ended without a close frame, e.g. on the idle timeout above, a network failure or a server restart
1009 Message too big: a frame exceeded the maximum payload length of the server (64 KiB by default)
1011 Server error: the server failed while serving the WebSocket, e.g. because the client did not read the notifications as fast as the server had to send them

After any of them, request a new URL and reconnect; the URL of a closed WebSocket cannot be reused. If the reconnect itself fails, the server answers the handshake with an HTTP return code instead of a close code: 404 if the URL is unknown or has expired, and 403 if the request comes from a different origin or a different client than the one that requested the URL.

Parameters

Language

The language of a request is the first of these that is available:

  1. the Accept-Language HTTP header sent with the request,
  2. the language stored in the preferences of the user the token belongs to,
  3. en.

Only the first entry of the header is read, and it is read verbatim. Send a single language tag such as de — not a quality-value list such as de;q=0.9, en;q=0.8, which is taken as the literal language de;q=0.9, matches no translation and therefore falls back to English. A tag that carries a region, such as de-CH, falls back to de and then to en when no translation exists for it.

The header applies only to the request that carries it. It never changes the stored preference of the user.

The client API resolves the language the other way round: the stored preference of the signed-in user wins, and Accept-Language is consulted only when that user has none.

Time zone

The time zone of a request is the first of these that is available:

  1. the X-Timezone HTTP header sent with the request,
  2. the time zone stored in the preferences of the user the token belongs to,
  3. the time zone of the server.

It decides how the server turns points in time into calendar days: which day an entry is counted on, where the slot boundaries of a timeline at Day granularity fall, and so on. Two requests that differ only in this header therefore report the same data cut at different day boundaries.

Like the language, the header applies only to the request that carries it and never changes the stored preference of the user.

The client API resolves the time zone the other way round: the stored preference of the signed-in user wins, and X-Timezone is consulted only when that user has none.

You may specify a time zone as follows:

  • If the zone ID equals Z, the server uses UTC.
  • If the zone ID consists of a single letter, a 400 error code will be returned.
  • If the zone ID starts with + or -, an offset is used.
  • If the zone ID equals GMT, UTC or UT then the server uses UTC.
  • If the zone ID starts with UTC+, UTC-, GMT+, GMT-, UT+ or UT- then the zone is a prefixed offset-based zone.
  • All other IDs are parsed as region-based zones. Region IDs must match the regular expression [A-Za-z][A-Za-z0-9~/._+-]+ otherwise a 400 error code will be returned.

Assertions

Many parameters and request-body members carry an assertion — a small constraint the server enforces on the supplied value. Where present, the assertion is shown for each parameter in the Assertion column of the endpoint reference below. If a value violates its assertion, the request is rejected with a 422 return code before it is processed. Assertions are documentation only: they are deliberately not reflected as constraints in the OpenApi schema or generated client types, so you should validate values against them yourself.

Read an assertion as follows:

  • Presence. non-null means the value is mandatory. null (or no assertion at all) means it is optional — any further check applies only when a value is supplied. A & combines a presence rule with a further check, e.g. non-null & matches '...'.
  • Numeric ranges. > is exclusive and >= is inclusive; the value under test is a placeholder letter between the bounds. n > 0 means greater than zero; 100 >= n >= 0 means between 0 and 100 inclusive (the maximum is written first). A dotted name such as pqforce.performance.bulk.max is a server-configured limit, whose current value you can read (see Server limits).
  • Date, time and duration ranges. date, datetime and duration values take the very same > / >= spelling, with each bound written as a quoted literal in the format that type uses everywhere else in this API: '2024-12-31' >= d >= '2024-01-01', '2024-12-31T23:59:59.999Z' >= at, 'PT8H' >= t > 'PT0S'. A dotted name is again a server-configured limit (see Server limits).
  • Patterns. matches '<regex>' requires the value to fully match the given regular expression (a doubled '' denotes a literal quote), e.g. matches '[0-9A-F]{32}' for a UUID.
  • Arrays. A ; separates an array-level rule from a per-element rule. pqforce.performance.bulk.max >= length > 0;non-null means a non-empty array within the configured maximum size, none of whose elements may be null.

An array-valued query parameter takes either a single value (?sort=name) or several values in double quotes, separated by a comma (?sort="name:desc","code").

Sorting

Endpoints that return a list usually accept an optional sort parameter. It is an array of sort criteria, applied in the given order; each element is propertyname, propertyname:asc or propertyname:desc, and a criterion without a direction sorts ascending. The description of each sort parameter in the endpoint reference below ends with Possible properties: followed by the property names that endpoint accepts. Where it also mentions dynamic properties, the ID of a property definition with one of the sortable semantics listed there may be used as a property name as well.

Property names are case-sensitive. An unknown property is rejected with a 422 return code, as is a request with more criteria than the configured pqforce.performance.sort.max (see Server limits).

Enumerations

Enumeration values are exchanged as strings and are case-sensitive: send them exactly as they are listed with their data type below. Most enumerations use PascalCase values (e.g. Day); a few keep an older spelling. Any other spelling is rejected with a 422 return code.

Server limits

A few of the limits that apply to your requests are not fixed by the API, but configured for the instance you are talking to. They may therefore differ on a dedicated or on-premises installation and they may change with a release. Read them once at start-up and use the values you receive; do not compile them into your application. Three endpoints provide them, because the limits have three different origins:

Limit Endpoint Result
A configuration value shared by the whole instance GET API/V3/PF/ServerConfig/{configKey} The value as text/plain, or a 404 return code if the key is not published
The requests you may send in parallel GET API/V3/PF/Limits/MaxParallelRequests PF.ParallelRequestsLimit
How long a deleted object stays restorable GET API/V3/PF/UndeleteLimit PF.UndeleteLimit, or a 402 return code if the undelete feature is not licensed

Published server configuration values

GET API/V3/PF/ServerConfig/{configKey} answers with the value of one configuration key, as plain text. Only the keys listed below are published; every other key is answered with a 404 return code, so this endpoint is not a way of reading arbitrary server configuration. The defaults given are those of the base configuration. Every instance is free to override any of them, which is exactly why you should read the values instead of assuming them, and a key an instance does not configure at all is not published either - a 404 return code on a key from this list therefore means "not configured here", not "wrong key".

Key Default Meaning
pqforce.performance.bulk.max 100 Maximum number of elements in one bulk parameter array
pqforce.performance.sort.max 8 Maximum number of sort criteria in one request
pqforce.performance.attachment.max 20971520 Maximum size of an uploaded attachment, in bytes
pqforce.performance.ganttprojectlimit 500 Maximum number of projects in one Gantt chart
pqforce.planningrange.from / pqforce.planningrange.to -2 / 2 Years, relative to the current one, in which allocations may be planned
pqforce.dayrange.from / pqforce.dayrange.to -10 / 10 Years, relative to the current one, in which day based planning is available
pqforce.dayrange.max 731 Maximum number of days one request may be answered a slot per day for
pqforce.security.passwordrule.regex - Regular expression every new password has to match
pqforce.security.passwordrule.desc.{language} - The password rule in words, for display to the user, one key per available language
pqforce.security.contentSecurity.binaries.fileExtensionWhitelist / ...Blacklist - File extensions accepted respectively rejected for uploads, a whitelist wins over the blacklist
pqforce.security.contentSecurity.binaries.mimeWhitelist / ...Blacklist - MIME types accepted respectively rejected for uploads
pqforce.security.contentSecurity.text.enabled true Whether plain text values are checked for control characters and markup
pqforce.security.contentSecurity.json.enabled true Whether JSON bodies are checked for dangerous content in strings
pqforce.website - Public web address of this instance
pqforce.login - Base URL of the PQFORCE login service
pqforce.support - Support address of this instance

An assertion that names one of these keys is bounded by its current value: pqforce.performance.bulk.max >= length > 0 on an array parameter means the array may hold at least one element and at most as many as that key says. Validating an upload against the extension, MIME type and size keys before sending it, and a new password against the password rule before submitting it, avoids rejections you would otherwise learn about only from the server.

Parallel requests

GET API/V3/PF/Limits/MaxParallelRequests returns the number of requests you may have in flight at the same time; further ones are answered with a 429 return code. The limit reported is the one that applies to you as you are calling: the limit per token when you authenticate with a bearer token, and the limit per user otherwise. The same URL therefore answers differently for a third party integration and for an interactive session. An application that reads data in parallel should keep its own concurrency below this value rather than relying on retries.

Tenant configuration values

GET API/V3/PF/Config/{configId} is a different endpoint and not a source of limits: it reads a configuration value of the current tenant, addressed by a known configuration ID instead of a dotted key, and it has PUT and DELETE counterparts for changing that value. Like ServerConfig it answers a 404 return code for an identifier it does not know.

Limits inside the JavaScript API

A JavaScript program does not need a request to learn its own runtime budget: sandbox.remainingRuntimeMs() reports the time left in the current execution. See Sandbox information for the configuration keys behind it.

Error codes

Due to security considerations, PQFORCE does not return error details, but codes refering to such details. This article describes how to find information about an error behind a certain code.

HTTP return codes

For a description of HTTP error codes refer to the huge amount of documentation in the web.

The most common HTTP return codes returned by PQFORCE API V3 endpoints

Return code Official meaning Detailed definition for PQFORCE
200 OK The request has succeeded
201 Created A new object has been created
202 Accepted The request was accepted, but processing has not yet been completed
303 See Other This code is only used in interactive requests for redirecting the browser to the client application pages
304 Not Modified The data in cache is still valid (sent by static requests when given an ETag in the If-None-Match header)
307 Temporary Redirect The URL used is a deprecated or alias URL for another endpoint and the Location header indicates the URL for the effective endpoint
308 Permanent Redirect The URL used is a deprecated or alias URL for another endpoint and the Location header indicates the URL for the effective endpoint
400 Bad Request The request does not fulfil the requirements (i.e. missing parameters or headers, unknown query parameters, or an optional parameter your license does not include)
401 Unauthorized The authentication with the bearer token was not successful
402 Payment Required Licensing restrictions apply (i.e. an endpoint your license does not include or an expired license)
403 Forbidden Trying to access an endpoint or an object without permission for user bound to token (this code may also be returned if an object requested does not exist)
404 Not Found Trying to access an endpoint which does not exist or an object that is not existing (may have been deleted), or there is no result to return
415 Unsupported Media Type No or wrong request body type
418 I'm a teapot As all backend developers like to drink coffee, this code is never sent back
422 Unprocessable Content Syntactic or semantic error in request (i.e. a value that cannot be read as its type, wrong JSON structure, unknown sort property, assertion or XSS HTML sanitizer violation)
429 Too Many Requests Trying to send too many requests in parallel, the applicable limit is reported by GET API/V3/PF/Limits/MaxParallelRequests
500 Internal Server Error Some backend internal condition (bug or misconfiguration) is preventing the backend from processing your request
503 Service Unavailable Indicates that PQFORCE backend is in maintenance mode

In case of error, HTTP requests might return additional information as JSON (or HTML, if endpoint is supposed to return text/html). In case of JSON, the structure is defined here: PF.ErrorInformation.

Note: There are more error codes and structures that may be returned by backend framework or 3rd party network components. Please refer to the widely available documentations in world wide web.

Return codes on bulk operations

For some bulk operations, PQFORCE may provide a return code per element. These codes (if provided) can be found in the header X-Detail-Codes of the response as comma separated integer values, one per element and in the order of the elements you sent. They have the same meaning as normal HTTP return codes.

The return code of the response as a whole follows the successful elements: as long as at least one element succeeded, you receive a success code together with the response body, and the elements that failed are reported only in X-Detail-Codes. If no element succeeded, the response carries the code of the first failed element (typically 404) and no body; X-Detail-Codes is present in this case as well.

The error codes returned by PQFORCE

In most cases when a request failed, PQFORCE returns an error code in a JSON structure. With only a few exceptions, these errors are logged in the error store which can be downloaded with status viewer. The code describes the first 4 characters of the error ID.

Deprecated calls

If a call to a deprecated endpoint (or with deprecated parameters) was made, a Deprecation HTTP header is added to the response.

Using the JavaScript API

PQFORCE server uses JavaScript for automation of certain processes. JavaScript is a programming language that is one of the core technologies of the World Wide Web. JavaScript programs run within the backend of PQFORCE and provides almost the whole set of functionality the REST API does. There are two main usage scenarios for JavaScript programs:

  • A special task that is run manually from time to time, doing some bulk data manipulation
  • A task that is triggered upon an event (i.e. a changed project)

Main principles of JavaScript programs in PQFORCE

PQFORCE backend provides a special environment for JavaScript programs. It is sandboxed which means that there is no direct access to PQFORCE server or hardware. All interaction with PQFORCE must be done over the API provided by PQFORCE (Pqf.*.*). However, core JavaScript functionality is also provided (i.e. string, number or array functionality and also JSON, Date and Math namespaces). Additional libraries like moment are optionally available. A special console provides logging of errors and debug output for easier functionality or bug tracking. The console is available through the three functions console.log(...), console.warn(...) and console.error(...). Every successful API V3 call returns a result. A call that delivers data (i.e. most GET requests and some POST requests) returns that data. A call that delivers no data (i.e. PUT, DELETE and some POST requests) returns the HTTP status code of the call as a number, e.g. 201 when an object was created and 200 when an existing one was updated. Only success codes are ever returned this way; a failed call throws an error as described below. If API requests fail, error handling can be done the standard JavaScript way by using try ... catch blocks. The error contains a return code in the form of a three-digit number in the form #000 which corresponds to the standardized HTTP return codes (see HTTP return codes). This code is followed by a detail message. However, this message may be hidden due to security restrictions in productive environments. In this case, a four letter hex code helps identifying the log entry which may be retrieved by our technical staff.

Accessing different generations of PQFORCE API

Each JavaScript source/ sandbox has an attribute specifying which API generation is provided on the PQFORCE API root object Pqf. However, it is always possible to explicitly access other versions by accessing the versioned roots PqfV2 and PqfV3.

Trigger information and impersonation

When triggered by an event, the variable trigger is available and contains an objectType, objectId and action member describing the kind of change. The code runs in the context of the user that triggered the change. This means, the same access permissions of that user apply also for code execution. The API request Pqf.acm.getCurrentUser() returns detail information about that user. Note: If the permissionOverride flag is set on the code, the user runs with the privileges (the same access permissions) of the user that last modified the script.

Console

You may use the console object's log(), warn() and error() functions for console output.

Storage

Each script has now its own storage. That means, that a script may store arbitrary data in an easy way without affecting other scripts and without the need of administrative privileges. A key is needed to define the object's place in the store. Therefore, any number of objects may be stored in parallel. Use store.load(key) to get data and store.save(key, data) to save data (set data to null to clear store). store.list() returns an array with all keys in use. Note: Scripts are isolated: Even by using the same key, different scripts will not see the same data. Use global store with admin privileges to share data across scripts.

Sandbox information

The sandbox object provides runtime information about the sandbox the script is running within. The following methods are available:

  • sandbox.id(): Returns a string containing the sandbox / script id.
  • sandbox.remainingRuntimeMs(): Returns the remaining runtime in milliseconds. The maximal runtime per script may be adjusted in dedicated / on-premises environments by changing the pqforce.javascript.limits.run (limit of single execution in milliseconds) and pqforce.javascript.limits.hourquota (hourly limit in milliseconds) value.
  • sandbox.engine(): Returns a String containing the JavaScript engine version. See https://mozilla.github.io/rhino/compat/engines.html for information about feature compatibility.

Html functions

The Html namespace offers easy HTML encoding and decoding by using Html.encode(...) or Html.decode(...). This may be useful for handling descriptions of objects. Furthermore, sanitizing is available by using Html.sanitize(...).

UUIDs

The Uuid namespace offers the possibility to easily generate new random UUIDs by using Uuid.generate().

Deprecated calls

If a call to a deprecated endpoint (or with deprecated parameters) was made, a deprecation warning is logged into the JavaScriptConsole.

Security considerations

Defeating XSS attacks (HTML-sanitizing)

Apart from some special members of objects (like JavaScript code), all strings containing <, > or & are considered to contain HTML. Therefore, they must be HTML-encoded (before sending, if they're not yet HTML) or HTML-decoded (after receiving, if only plain text is desired). Members that do not need to be sanitized (i.e. JavaScript code) is marked with an insecure-flag. These members are not encoded.

JavaScript and public API endpoints

The following endpoint definitions apply to the following version of PQFORCE API:

Name Value
API generation V3
API version 4.26.6
API fingerprint 1cQT-Y9Eh-AqNv-ae4s-2Qvy-UyTx-6JVs

Look for newest documentation at: YouTrack dashboard or TeamCity.

API data contracts

↑

Notification

Portal tour

A guided tour walks you through the page step by step, highlighting one control at a time and explaining what it does. You can stop at any point and pick up later.