
Start Here: Your First PI Web API Request
This tutorial walks you through making your first successful PI Web API request using Python. You will verify connectivity, explore the API structure, find a PI point, and read live data -- all in under 10 minutes.
Before you begin
Before writing any code, confirm you have the following. If any of these are missing, the rest of this tutorial will not work.
| Requirement | Details | How to verify |
|---|---|---|
| PI Web API URL | The full base URL, e.g. https://your-server/piwebapi. Must include /piwebapi at the end. | Open it in a browser -- you should see a JSON response with server info |
| Credentials | A username and password with at least read access to PI data. Format: DOMAIN\username for Basic auth. | Ask your PI administrator |
| Python 3.8+ | With the requests library installed. | python --version and pip install requests |
| PI Web API version | 2017 SP1 or later. Older versions are missing endpoints used in this tutorial. | The root endpoint response includes ProductVersion |
Common URL mistake
The PI Web API base URL must end with /piwebapi, not just the server hostname. For example: https://myserver.corp.com/piwebapi -- not https://myserver.corp.com. If you use the wrong URL, you will get a 404 or an IIS default page instead of the API.
Step 1: Verify connectivity
The PI Web API root endpoint returns metadata about the server. Hit it first to confirm your URL, credentials, and network connectivity all work.
import requests
from requests.auth import HTTPBasicAuth
BASE_URL = "https://your-server/piwebapi"
response = requests.get(
BASE_URL,
auth=HTTPBasicAuth("DOMAIN\\username", "password"),
verify=False, # See the Authentication guide for proper cert handling
)
print(f"Status: {response.status_code}")
if response.status_code == 200:
data = response.json()
print(f"Product: {data['ProductTitle']}")
print(f"Version: {data['ProductVersion']}")
print(f"Server: {data['MachineName']}")
print(f"\nAvailable resources:")
for name, url in data["Links"].items():
if name != "Self":
print(f" {name}: {url}")
else:
print(f"Error: {response.text[:200]}")Expected output (your values will differ):
Status: 200
Product: PI Web API
Version: 2021 SP3
Server: YOURSERVER
Available resources:
AssetServers: https://your-server/piwebapi/assetservers
DataServers: https://your-server/piwebapi/dataservers
Search: https://your-server/piwebapi/search
System: https://your-server/piwebapi/system
...About verify=False
Setting verify=False disables SSL certificate verification. This is acceptable for a quick connectivity test, but you must configure proper certificate handling before moving to production. Without it, your credentials travel over an unverified connection. See the Authentication guide for proper setup.
What if it does not work?
| Symptom | Fix |
|---|---|
ConnectionError / timeout | Check the URL, firewall rules, and VPN. Try opening the URL in a browser. |
SSLError | The server uses a self-signed or internal CA certificate. Add verify=False for now, then fix it properly later. |
| Status 401 | Wrong credentials or auth method. Check username format (DOMAIN\user). Try the URL in a browser with the same credentials. |
| Status 404 or HTML response | URL is wrong. Make sure it ends with /piwebapi, not /piwebapi/ (no trailing slash). |
Step 2: Understand the API structure
PI Web API is a hypermedia API. The root response contains links to all major resource collections. You never need to guess endpoint URLs -- the API tells you where everything is.
The most important resources for getting started are:
| Resource | URL | What it does |
|---|---|---|
| Data Servers | /dataservers | Lists PI Data Archive servers (where time-series data lives) |
| Asset Servers | /assetservers | Lists PI AF Servers (where the element/attribute hierarchy lives) |
| Points | /points | Look up individual PI points by path |
| Streams | /streams | Read and write time-series values for a specific point |
| Search | /search/query | Full-text search across points, elements, and attributes |
# List all PI Data Archive servers this PI Web API can reach
response = requests.get(
f"{BASE_URL}/dataservers",
auth=HTTPBasicAuth("DOMAIN\\username", "password"),
verify=False,
)
servers = response.json()["Items"]
for server in servers:
print(f"Server: {server['Name']}")
print(f" WebId: {server['WebId']}")
print(f" Path: {server['Path']}")
print(f" IsConnected: {server['IsConnected']}")
print()What is a WebId?
Every resource in PI Web API has a WebId -- a unique, URL-safe identifier. You need a resource's WebId to read or write its values. Think of it as a primary key. Learn more in the WebID & Lookup guide.
Step 3: Find a PI point
The most reliable way to find a PI point is by its full path. This does not require the Indexed Search crawler to be running (which is a common configuration issue with the search endpoint).
# Look up a PI point by its full path
# Replace YOUR-SERVER with your PI Data Archive server name
response = requests.get(
f"{BASE_URL}/points",
params={"path": "\\\\YOUR-SERVER\\sinusoid"},
auth=HTTPBasicAuth("DOMAIN\\username", "password"),
verify=False,
)
if response.status_code == 200:
point = response.json()
print(f"Name: {point['Name']}")
print(f"WebId: {point['WebId']}")
print(f"PointType: {point['PointType']}")
print(f"Descriptor: {point.get('Descriptor', 'N/A')}")
print(f"EngUnits: {point.get('EngineeringUnits', 'N/A')}")
# Save the WebId for the next step
POINT_WEB_ID = point["WebId"]
elif response.status_code == 404:
print("Point not found. Check the server name and point name.")
print("Tip: Server name is case-sensitive on some configurations.")
else:
print(f"Error {response.status_code}: {response.text[:200]}")The sinusoid point
Most PI Data Archive installations include a default point called sinusoid that generates a continuous sine wave. It is the standard test point for learning. If your server does not have it, ask your PI administrator for the name of any active numeric point you can read.
Alternatively, you can search by name. This requires the PI Web API Indexed Search crawler to be configured and running.
# Search for points by name (requires Indexed Search to be enabled)
response = requests.get(
f"{BASE_URL}/search/query",
params={"q": "name:sinusoid", "count": 5},
auth=HTTPBasicAuth("DOMAIN\\username", "password"),
verify=False,
)
if response.status_code == 200:
results = response.json()
items = results.get("Items", [])
if items:
for item in items:
print(f"{item['Name']} ({item['ItemType']}) - WebId: {item['WebId']}")
else:
print("No results. The Indexed Search crawler may not be configured.")
print("Use the path-based lookup above instead.")
elif response.status_code == 502:
print("Search service unavailable. Indexed Search may not be enabled.")
print("Use the path-based lookup above instead.")Search requires configuration
The /search/query endpoint requires the PI Web API Indexed Search crawler to be enabled and running. Many PI Web API installations do not have this configured, which causes search to return empty results or a 502 error. If search does not work, use the path-based lookup method above -- it always works.
Step 4: Read the current value
With a WebId, you can read the current (snapshot) value of any PI point. This is the most recent value in the PI Data Archive.
# Read the current snapshot value
response = requests.get(
f"{BASE_URL}/streams/{POINT_WEB_ID}/value",
auth=HTTPBasicAuth("DOMAIN\\username", "password"),
verify=False,
)
data = response.json()
print(f"Value: {data['Value']}")
print(f"Timestamp: {data['Timestamp']}")
print(f"Good: {data['Good']}")
print(f"Units: {data.get('UnitsAbbreviation', 'N/A')}")
# The full response contains more fields:
# - Substituted: whether the value was manually overridden
# - Annotated: whether the value has a note attached
# - Good: whether the data quality is acceptableExpected response structure:
{
"Timestamp": "2026-03-15T14:30:00Z",
"Value": 87.345,
"UnitsAbbreviation": "",
"Good": true,
"Questionable": false,
"Substituted": false,
"Annotated": false
}Understanding the Good field
The Good field indicates data quality. When Good is false, the value may be stale, from a disconnected sensor, or otherwise unreliable. Always check this field before using the value in calculations or displays. In production code, you should handle bad-quality values explicitly.
Step 5: Read historical values
The real power of PI is historical data. Let's read the last hour of recorded values.
# Read recorded values from the last hour
response = requests.get(
f"{BASE_URL}/streams/{POINT_WEB_ID}/recorded",
params={
"startTime": "*-1h", # 1 hour ago
"endTime": "*", # Now
"maxCount": 100, # Limit results
},
auth=HTTPBasicAuth("DOMAIN\\username", "password"),
verify=False,
)
data = response.json()
items = data.get("Items", [])
print(f"Retrieved {len(items)} recorded values:\n")
for item in items[:5]: # Show first 5
quality = "Good" if item["Good"] else "BAD"
print(f" {item['Timestamp']}: {item['Value']:.3f} [{quality}]")
if len(items) > 5:
print(f" ... and {len(items) - 5} more")
# Important: if len(items) == maxCount, there may be more data
# that was truncated. See the Pagination guide for how to handle this.
if len(items) == 100:
print("\nWarning: results may be truncated. Increase maxCount or paginate.")PI time syntax
PI Web API accepts relative time expressions: * (now), *-1h (1 hour ago), *-7d (7 days ago), t (today at midnight), y (yesterday at midnight). You can also use ISO 8601 format: 2026-03-15T00:00:00Z. These expressions work on all time-based endpoints.
Recorded values are compressed
PI Data Archive uses exception-based compression, so "recorded" values are not raw sensor readings at fixed intervals. They are the values that passed the compression filter -- typically when the value changed significantly. This is why you may get 20 values for one hour of a slowly-changing point but 500 for a volatile one. If you need evenly-spaced values, use the /streams/{webId}/interpolated endpoint instead. See the Reading Values guide for details.

Complete working example
Here is the entire tutorial as a single, self-contained script you can copy and run. Replace the placeholder values at the top with your actual server details.
"""PI Web API Quick Start -- your first requests in Python.
Replace the configuration values below with your actual server details.
Run: python pi_web_api_quickstart.py
"""
import requests
from requests.auth import HTTPBasicAuth
import urllib3
# Suppress SSL warnings for this quick test
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# ── Configuration (replace these) ──────────────────────────────
BASE_URL = "https://your-server/piwebapi"
USERNAME = "DOMAIN\\username"
PASSWORD = "password"
PI_SERVER = "YOUR-PI-SERVER" # PI Data Archive server name
POINT_NAME = "sinusoid" # PI point to read
# ───────────────────────────────────────────────────────────────
session = requests.Session()
session.auth = HTTPBasicAuth(USERNAME, PASSWORD)
session.verify = False
session.headers.update({"Accept": "application/json"})
# Step 1: Verify connectivity
print("1. Connecting to PI Web API...")
resp = session.get(BASE_URL)
if resp.status_code != 200:
print(f" FAILED: {resp.status_code} - {resp.text[:200]}")
exit(1)
info = resp.json()
print(f" Connected to {info['ProductTitle']} {info['ProductVersion']}")
print(f" Server: {info['MachineName']}")
# Step 2: Find the PI point
print(f"\n2. Looking up point '{POINT_NAME}' on {PI_SERVER}...")
resp = session.get(
f"{BASE_URL}/points",
params={"path": f"\\\\{PI_SERVER}\\{POINT_NAME}"},
)
if resp.status_code != 200:
print(f" FAILED: {resp.status_code} - {resp.text[:200]}")
exit(1)
point = resp.json()
web_id = point["WebId"]
print(f" Found: {point['Name']} (Type: {point['PointType']})")
print(f" WebId: {web_id}")
# Step 3: Read the current value
print("\n3. Reading current value...")
resp = session.get(f"{BASE_URL}/streams/{web_id}/value")
data = resp.json()
quality = "Good" if data["Good"] else "BAD"
print(f" Value: {data['Value']} [{quality}]")
print(f" Timestamp: {data['Timestamp']}")
# Step 4: Read the last hour of history
print("\n4. Reading last hour of recorded values...")
resp = session.get(
f"{BASE_URL}/streams/{web_id}/recorded",
params={"startTime": "*-1h", "endTime": "*", "maxCount": 100},
)
items = resp.json().get("Items", [])
print(f" Retrieved {len(items)} recorded values")
if items:
values = [item["Value"] for item in items if item["Good"]]
if values:
print(f" Min: {min(values):.3f}")
print(f" Max: {max(values):.3f}")
print(f" Avg: {sum(values)/len(values):.3f}")
print("\nDone! You have successfully connected to PI Web API.")What you learned
- How to connect to PI Web API and verify your credentials
- The API structure: root endpoint, data servers, points, and streams
- How to find a PI point by path (more reliable than search)
- How to read the current snapshot value and historical recorded values
- The importance of quality flags and data compression in PI
Next steps
Now that you can connect and read data, here is the recommended path forward:
- 1
Set up proper authentication
Learn about Kerberos, NTLM, and certificate handling for production use. Stop using verify=False.
- 2
Understand WebID and lookup patterns
Learn how to find any PI resource by path, name, or search query. Understand WebID encoding.
- 3
Read all value types
Master recorded, interpolated, and summary values. Learn about compression, quality flags, and digital states.
- 4
Write values back to PI
Send single or bulk value updates to PI points. Understand buffering, update options, and error handling.