
Common Errors & Troubleshooting
A practical guide to diagnosing and fixing the most frequent PI Web API errors. Each section covers what the error looks like, why it happens, and exactly how to fix it.
HTTP status code quick reference
PI Web API uses standard HTTP status codes. Here is what each one means in the context of PI System operations.
| Status | Meaning | Common PI Web API cause |
|---|---|---|
| 200 | OK | Request succeeded. Data returned in body. |
| 202 | Accepted | Write accepted but not yet committed (buffered write). |
| 204 | No Content | Write succeeded. No response body. |
| 400 | Bad Request | Malformed JSON, invalid parameter name, or wrong value type. |
| 401 | Unauthorized | Authentication failed. Credentials rejected or missing. |
| 403 | Forbidden | Authenticated but no permission on the PI resource. |
| 404 | Not Found | Invalid WebID, deleted point, or wrong URL path. |
| 408 | Request Timeout | Query took too long. Reduce time range or maxCount. |
| 409 | Conflict | Value already exists at that timestamp. Use updateOption=Replace. |
| 500 | Internal Server Error | PI Data Archive or AF Server issue, or malformed request body. |
| 502 | Bad Gateway | IIS cannot reach the PI Web API application pool. |
| 503 | Service Unavailable | Server overloaded or application pool recycling. |
SSL certificate errors (the #1 pain point)
SSL errors are the single most common issue when first connecting to PI Web API. Nearly every PI Web API deployment uses a self-signed or internal CA certificate that your client machine does not trust by default.
Error: SSLCertVerificationError / CERTIFICATE_VERIFY_FAILED
requests.exceptions.SSLError: HTTPSConnectionPool(host='your-server', port=443):
Max retries exceeded with url: /piwebapi/ (Caused by SSLError(
SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED]
certificate verify failed: unable to get local issuer certificate')))Fix 1: Extract and provide the CA certificate (recommended)
Use openssl to extract the server certificate directly from the command line. This is more reliable than browser export.
# Extract the full certificate chain from the PI Web API server
openssl s_client -connect your-server:443 -showcerts < /dev/null 2>/dev/null \
| openssl x509 -outform PEM > pi-web-api-cert.pem
# Verify the extracted certificate
openssl x509 -in pi-web-api-cert.pem -noout -subject -issuer -dates
# If there's an intermediate CA, save the full chain:
openssl s_client -connect your-server:443 -showcerts < /dev/null 2>/dev/null \
| awk '/BEGIN CERT/,/END CERT/{print}' > pi-web-api-chain.pem# Point your session to the extracted certificate
session.verify = "/path/to/pi-web-api-cert.pem"
# Or set via environment variable (applies to all requests in the process)
# export REQUESTS_CA_BUNDLE=/path/to/pi-web-api-cert.pemFix 2: Use the Windows certificate store (Windows only)
# This makes Python's requests library trust certificates
# from the Windows certificate store automatically
pip install python-certifi-win32
# After installation, if your org's CA cert is in the Windows
# trust store, requests will automatically trust PI Web API's cert.
# No code changes needed.Fix 3: Add to system trust store (Linux)
# Debian/Ubuntu
sudo cp pi-web-api-cert.pem /usr/local/share/ca-certificates/pi-web-api.crt
sudo update-ca-certificates
# RHEL/CentOS/Fedora
sudo cp pi-web-api-cert.pem /etc/pki/ca-trust/source/anchors/
sudo update-ca-trustFix 4: Disable verification (testing only)
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
session.verify = FalseNever disable SSL verification in production
Disabling verification makes your connection vulnerable to man-in-the-middle attacks. Credentials travel in plain text over the network. Always resolve the CA certificate properly for production use.
Debugging SSL issues step by step
"""Diagnose SSL certificate issues with PI Web API."""
import ssl
import socket
def debug_ssl(hostname, port=443):
"""Show certificate details for a PI Web API server."""
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
with socket.create_connection((hostname, port)) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cert = ssock.getpeercert(binary_form=False)
der_cert = ssock.getpeercert(binary_form=True)
if cert:
print(f"Subject: {cert.get('subject')}")
print(f"Issuer: {cert.get('issuer')}")
print(f"Not Before: {cert.get('notBefore')}")
print(f"Not After: {cert.get('notAfter')}")
print(f"SANs: {cert.get('subjectAltName')}")
else:
# Self-signed cert with no parsed fields
pem = ssl.DER_cert_to_PEM_cert(der_cert)
print("Certificate (PEM):")
print(pem[:200] + "...")
print(f"Protocol: {ssock.version()}")
print(f"Cipher: {ssock.cipher()}")
debug_ssl("your-pi-web-api-server")401 Unauthorized
You can reach the server, but authentication fails. The WWW-Authenticate header in the 401 response tells you which authentication methods the server accepts.
| Cause | How to identify | Fix |
|---|---|---|
| Wrong credentials | Works in browser with same creds but not in code | Use DOMAIN\\username format. Check for special characters that need escaping. |
| Basic auth disabled | WWW-Authenticate header shows only Negotiate | Use Kerberos (requests-kerberos) or NTLM (requests-ntlm). |
| Kerberos ticket expired | Run klist -- shows expired or no tickets | Run kinit to get a fresh ticket. |
| SPN mismatch | Kerberos auth fails but NTLM works | Check SPN with setspn -L servername$. Must match HTTP/hostname. |
| NTLM blocked by policy | NTLM fails, Kerberos fails, Basic works | Group Policy may block NTLMv1. Use Kerberos or Basic over HTTPS. |
| Account locked out | Was working, suddenly stopped. Other services also fail. | Check AD account status. Automated retries with wrong passwords cause lockouts. |
"""Diagnose authentication issues with PI Web API."""
def debug_auth(base_url):
"""Check what authentication methods the server accepts."""
import requests
# Make an unauthenticated request to see what the server offers
resp = requests.get(f"{base_url}/", verify=False)
print(f"Status: {resp.status_code}")
www_auth = resp.headers.get("WWW-Authenticate", "Not present")
print(f"WWW-Authenticate: {www_auth}")
# Parse supported methods
methods = []
if "Basic" in www_auth:
methods.append("Basic")
if "Negotiate" in www_auth:
methods.append("Kerberos (Negotiate)")
if "NTLM" in www_auth:
methods.append("NTLM")
if "Bearer" in www_auth:
methods.append("Bearer / OpenID Connect")
print(f"Supported auth methods: {', '.join(methods) or 'None detected'}")
# If Kerberos is available, check the ticket cache
if "Negotiate" in www_auth:
import subprocess
try:
result = subprocess.run(["klist"], capture_output=True, text=True)
print(f"\nKerberos ticket cache:\n{result.stdout}")
if "expired" in result.stdout.lower():
print("WARNING: Kerberos ticket is expired. Run 'kinit' to refresh.")
except FileNotFoundError:
print("\nklist not found. Kerberos tools may not be installed.")
debug_auth("https://your-server/piwebapi")Kerberos-specific debugging
# Check your current Kerberos tickets
klist
# Get a new ticket (will prompt for password)
kinit username@DOMAIN.COM
# Verify the SPN for PI Web API (run on domain-joined machine)
setspn -L PI-WEB-API-SERVER$
# Check if the correct SPN exists
# Expected: HTTP/pi-web-api-server.domain.com
setspn -Q HTTP/pi-web-api-server.domain.com
# View Kerberos events on Windows (run as admin)
# Event Viewer > Windows Logs > Security
# Filter for Event ID 4768 (TGT request) and 4769 (Service ticket request)
# Enable Kerberos debug logging on Linux
# Add to /etc/krb5.conf under [logging]:
# default = FILE:/var/log/krb5.logNTLM-specific debugging
from requests_ntlm import HttpNtlmAuth
# NTLM requires DOMAIN\username format
session = requests.Session()
session.auth = HttpNtlmAuth("DOMAIN\\username", "password")
# Test the connection
resp = session.get(f"{BASE_URL}/")
print(f"Status: {resp.status_code}")
# Common NTLM issues:
# 1. NTLMv1 disabled by policy: upgrade to NTLMv2 or use Kerberos
# 2. Wrong domain: check with 'whoami /fqdn' on a domain machine
# 3. Account lockout: NTLM retries can lock accounts faster than KerberosNTLM is being deprecated
Microsoft is deprecating NTLM in favor of Kerberos. If your organization is still using NTLM, plan to migrate to Kerberos or Bearer/OpenID Connect authentication for PI Web API.
403 Forbidden
Authentication succeeds, but you lack permissions for the requested resource. This is a PI System permissions issue, not a network or credentials issue.
How PI Web API permissions work
PI Web API maps your Windows identity to a PI Identity through PI Identity mappings configured on the PI Data Archive. Your effective permissions are determined by which PI Identity your Windows account maps to, and what security settings are applied to each PI point or AF element.
| Scenario | Typical cause | Fix |
|---|---|---|
| Can read some points but not others | Point-level security differs | Ask PI admin to check Data Access on the specific points in PI SMT. |
| Can read but not write | Write permissions are separate from read | PI admin must grant Write access to your PI Identity on those points. |
| Can read PI points but not AF elements | AF security is separate from Data Archive security | Check AF Database security in PI System Explorer. |
| No PI Identity mapping found | Your Windows account has no PI Identity mapping | PI admin must create a mapping in PI SMT > Security > Mappings & Trusts. |
| Works from one machine but not another | Different Windows identity on each machine (service account vs. user) | Check which identity PI Web API sees with the diagnostic below. |
"""Check which identity PI Web API sees for your connection."""
def check_my_identity(session, base_url):
"""Show the identity PI Web API resolves for your credentials."""
# The system/userinfo endpoint shows your resolved identity
resp = session.get(f"{base_url}/system/userinfo")
if resp.ok:
info = resp.json()
print(f"Windows Identity: {info.get('Name', 'Unknown')}")
print(f"Is Administrator: {info.get('IsAdministrator', False)}")
print(f"SID: {info.get('SID', 'Unknown')}")
else:
print(f"Cannot check identity: {resp.status_code}")
# Try to list data servers (tests basic PI Data Archive access)
resp = session.get(f"{base_url}/dataservers")
if resp.ok:
servers = resp.json().get("Items", [])
print(f"\nCan access {len(servers)} data server(s):")
for s in servers:
print(f" - {s['Name']} ({s.get('IsConnected', 'Unknown')})")
else:
print(f"\nCannot list data servers: {resp.status_code}")
if resp.status_code == 403:
print(" Your PI Identity does not have Read access to any Data Archive.")
check_my_identity(session, BASE_URL)404 Not Found
The URL or resource does not exist. In PI Web API, 404 usually means a WebID problem, not a server misconfiguration.
| Cause | How to identify | Fix |
|---|---|---|
| Wrong or stale WebID | WebID was hardcoded or cached from another server | Re-look up the point using path or search. Never hardcode WebIDs. |
| PI point was deleted or renamed | Used to work, now returns 404 | Search for the current name. Check PI SMT for deleted points. |
| Typo in endpoint URL | URL path does not match PI Web API docs | Check the URL. Common mistake: /piwebapi/stream/ vs /piwebapi/streams/ (note the s). |
| WebID from a different PI system | WebID encodes the server name | WebIDs are not portable between PI systems. Look up dynamically. |
Missing /piwebapi prefix | Using https://server/streams/... | URL must include /piwebapi: https://server/piwebapi/streams/... |
Always look up WebIDs dynamically
A WebID from your development PI server will not work on production. Always look up WebIDs by name or path at the start of your script, then cache them for the duration of the session. Never hardcode WebIDs in configuration files.
408 Request Timeout / 504 Gateway Timeout
Your request is taking too long. This usually happens with large recorded-value queries or when PI Data Archive is under heavy load.
| Strategy | Details |
|---|---|
| Reduce the time range | Querying a year of 1-second data returns millions of rows. Query smaller windows (hours or days) and paginate. |
| Use interpolated values | If you do not need every raw event, /streams/{webId}/interpolated returns evenly-spaced samples and is much faster. |
| Use summary values | For dashboards and reports, /streams/{webId}/summary returns min/max/avg without transferring all raw data. |
| Lower maxCount | Default is 1000. Using maxCount=150000 forces the server to assemble a huge response. Use smaller values and paginate. |
| Use selectedFields | Requesting selectedFields=Items.Value;Items.Timestamp reduces response size significantly. |
| Increase client timeout | If the query legitimately needs time: session.timeout = (10, 120) (10s connect, 120s read). |
# Configure separate connect and read timeouts
session.timeout = (10, 120) # 10s to connect, 120s to read
# Or per-request:
resp = session.get(
f"{BASE_URL}/streams/{WEB_ID}/recorded",
params={"startTime": "*-30d", "endTime": "*", "maxCount": 50000},
timeout=(10, 300), # 5 minute read timeout for large queries
)409 Conflict
Happens when writing a value to a timestamp that already has data. PI Data Archive default behavior rejects duplicate timestamps.
# Use updateOption to control write behavior
response = session.post(
f"{BASE_URL}/streams/{WEB_ID}/value",
json={"Value": 42.0, "Timestamp": "2026-03-15T10:00:00Z"},
params={"updateOption": "Replace"}, # Overwrite existing value
)
# updateOption values:
# "Replace" - Overwrite existing value at this timestamp
# "Insert" - Only write if no value exists at this timestamp
# "NoReplace" - Same as Insert (legacy name)
# "Remove" - Delete the value at this timestamp
# "InsertNoCompression" - Insert without applying compression500 Internal Server Error
A 500 error is a server-side failure. The response body usually contains an error message that helps identify the root cause.
| Error message pattern | Root cause | Fix |
|---|---|---|
| PI Data Archive [...] is not available | Data Archive server is down or unreachable | Check PI Data Archive service status and network connectivity from PI Web API server. |
| Cannot connect to AF Server | PI AF Server is down (for element/attribute queries) | Check PI AF Server service and SQL Server connectivity. |
| System.Runtime.InteropServices.COMException | AF SDK connection failure to Data Archive | PI Web API's internal AF SDK connection is broken. Restart the PI Web API application pool in IIS. |
| Invalid JSON | Malformed request body | Validate your JSON payload. Common issue: sending a string where an object is expected. |
| Object reference not set | Server bug or unexpected null in request | Check PI Web API logs at %ProgramData%\OSIsoft\PI Web API\Logs. |
"""Extract useful details from 500 error responses."""
resp = session.get(f"{BASE_URL}/streams/{WEB_ID}/recorded", params=params)
if resp.status_code == 500:
try:
error_body = resp.json()
# PI Web API usually returns errors in this structure
errors = error_body.get("Errors", [])
for err in errors:
print(f"Server error: {err}")
except Exception:
# Sometimes the response is not valid JSON
print(f"Raw error: {resp.text[:500]}")PI Web API server log locations
%ProgramData%\OSIsoft\PI Web API\Logs\-- PI Web API application logs%SystemRoot%\System32\LogFiles\W3SVC1\-- IIS request logsEvent Viewer > Application-- .NET runtime errors and PI Web API service eventsPI Data Archive message logs-- check via PI SMT for backend data errors
ConnectionError / MaxRetryError
You cannot reach the PI Web API server at all. The connection is refused, times out, or DNS resolution fails.
"""Step-by-step connection debugging for PI Web API."""
import socket
import requests
def debug_connection(hostname, port=443):
"""Test each layer of connectivity to PI Web API."""
# Step 1: DNS resolution
print(f"1. DNS resolution for {hostname}...")
try:
ip = socket.gethostbyname(hostname)
print(f" Resolved to: {ip}")
except socket.gaierror as e:
print(f" FAILED: {e}")
print(" Fix: Check hostname spelling. Try IP address directly.")
return
# Step 2: TCP connection
print(f"2. TCP connection to {ip}:{port}...")
try:
sock = socket.create_connection((ip, port), timeout=10)
sock.close()
print(" Connected successfully")
except (socket.timeout, ConnectionRefusedError) as e:
print(f" FAILED: {e}")
print(" Fix: Check firewall rules, VPN, or if PI Web API service is running.")
return
# Step 3: HTTPS handshake
print(f"3. HTTPS handshake with {hostname}...")
try:
resp = requests.get(
f"https://{hostname}:{port}/piwebapi/",
timeout=10,
verify=False,
auth=None, # Skip auth for connectivity test
)
print(f" HTTP {resp.status_code}")
if resp.status_code == 401:
print(" Server is reachable and requires authentication (this is good!)")
elif resp.status_code == 200:
info = resp.json()
print(f" Product: {info.get('ProductTitle', 'Unknown')}")
print(f" Version: {info.get('ProductVersion', 'Unknown')}")
except requests.exceptions.SSLError:
print(" SSL error (but server is reachable). See SSL section above.")
except Exception as e:
print(f" FAILED: {e}")
debug_connection("your-pi-web-api-server")Common causes
- VPN not connected: PI Web API is usually on a corporate network. Ensure VPN is active.
- Firewall blocking port 443: Check with your network team. Some orgs use non-standard ports.
- PI Web API service stopped: Check IIS on the server. The application pool may have crashed.
- Wrong hostname: The server might be
piwebapi.domain.comnotpiserver.domain.com. - Proxy interference: Corporate proxies may intercept HTTPS. Set
NO_PROXY=pi-server-hostname.
Comprehensive diagnostic script
Run this script to diagnose the most common PI Web API issues in one pass. It checks connectivity, authentication, search, data access, and write permissions.
"""Comprehensive PI Web API diagnostic script.
Usage:
python pi_diagnostic.py
Set these environment variables before running:
PI_WEB_API_URL = https://your-server/piwebapi
PI_USERNAME = DOMAIN\username
PI_PASSWORD = your-password
PI_CA_BUNDLE = /path/to/cert.pem (optional)
"""
import os
import time
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def run_diagnostics():
base_url = os.environ.get("PI_WEB_API_URL", "https://your-server/piwebapi")
username = os.environ.get("PI_USERNAME", "")
password = os.environ.get("PI_PASSWORD", "")
ca_bundle = os.environ.get("PI_CA_BUNDLE", False)
session = requests.Session()
if username:
session.auth = (username, password)
session.verify = ca_bundle if ca_bundle else False
session.headers["X-Requested-With"] = "PiSharp-Diagnostic"
retry = Retry(total=2, backoff_factor=1, status_forcelist=[502, 503])
session.mount("https://", HTTPAdapter(max_retries=retry))
results = {"passed": 0, "failed": 0, "warnings": 0}
def check(name, func):
print(f"\n{'='*60}")
print(f"CHECK: {name}")
print(f"{'='*60}")
try:
func()
results["passed"] += 1
except AssertionError as e:
print(f" FAILED: {e}")
results["failed"] += 1
except Exception as e:
print(f" ERROR: {type(e).__name__}: {e}")
results["failed"] += 1
def check_connectivity():
start = time.perf_counter()
resp = session.get(f"{base_url}/")
elapsed = time.perf_counter() - start
assert resp.status_code == 200, f"Expected 200, got {resp.status_code}"
info = resp.json()
print(f" Server: {info.get('ProductTitle', 'Unknown')}")
print(f" Version: {info.get('ProductVersion', 'Unknown')}")
print(f" Response time: {elapsed:.3f}s")
def check_data_servers():
resp = session.get(f"{base_url}/dataservers")
assert resp.status_code == 200, f"Expected 200, got {resp.status_code}"
servers = resp.json().get("Items", [])
assert len(servers) > 0, "No data servers found"
for s in servers:
connected = s.get("IsConnected", "Unknown")
print(f" {s['Name']}: Connected={connected}")
def check_af_servers():
resp = session.get(f"{base_url}/assetservers")
assert resp.status_code == 200, f"Expected 200, got {resp.status_code}"
servers = resp.json().get("Items", [])
print(f" Found {len(servers)} AF server(s)")
for s in servers:
print(f" {s['Name']}")
def check_search():
resp = session.get(
f"{base_url}/search/query",
params={"q": "name:sinu*", "count": 5},
)
if resp.status_code == 200:
hits = resp.json().get("Items", [])
print(f" Search returned {len(hits)} results")
elif resp.status_code == 410:
print(" WARNING: Indexed Search is not enabled (410 Gone)")
print(" This is OK -- use path-based lookups instead of search.")
results["warnings"] += 1
else:
assert False, f"Search failed with status {resp.status_code}"
def check_point_read():
# Try to find and read the sinusoid point
resp = session.get(f"{base_url}/dataservers")
if not resp.ok:
assert False, f"Cannot list data servers: {resp.status_code}"
servers = resp.json().get("Items", [])
if not servers:
print(" SKIP: No data servers available")
return
ds_web_id = servers[0]["WebId"]
resp = session.get(
f"{base_url}/dataservers/{ds_web_id}/points",
params={"nameFilter": "sinusoid", "maxCount": 1},
)
if resp.ok and resp.json().get("Items"):
point = resp.json()["Items"][0]
web_id = point["WebId"]
print(f" Found point: {point['Name']}")
# Read current value
resp = session.get(
f"{base_url}/streams/{web_id}/value",
params={"selectedFields": "Timestamp;Value;Good"},
)
assert resp.status_code == 200, f"Read failed: {resp.status_code}"
val = resp.json()
print(f" Current value: {val.get('Value')} at {val.get('Timestamp')}")
print(f" Quality good: {val.get('Good', 'Unknown')}")
else:
print(" WARNING: sinusoid point not found (may not exist on this server)")
results["warnings"] += 1
check("Connectivity & Authentication", check_connectivity)
check("PI Data Archive Servers", check_data_servers)
check("PI AF Servers", check_af_servers)
check("Indexed Search", check_search)
check("Point Lookup & Data Read", check_point_read)
print(f"\n{'='*60}")
print(f"RESULTS: {results['passed']} passed, {results['failed']} failed, {results['warnings']} warnings")
print(f"{'='*60}")
if __name__ == "__main__":
run_diagnostics()