P21 API Authentication
Disclaimer: This is unofficial, community-created documentation for Epicor Prophet 21 APIs. It is not affiliated with, endorsed by, or supported by Epicor Software Corporation. All product names, trademarks, and registered trademarks are property of their respective owners. Use at your own risk.
Overview
All P21 APIs require authentication via Bearer tokens. This guide covers the two authentication methods:
- User Credentials - Username and password authentication
- Consumer Key - Pre-authenticated application key
Token Endpoints
V2 Endpoint (Recommended)
POST https://{hostname}/api/security/token/v2
The V2 endpoint accepts credentials in the request body.
V1 Endpoint (Deprecated)
POST https://{hostname}/api/security/token
The V1 endpoint accepts credentials as headers. While still functional, Epicor recommends migrating to V2.
Method 1: User Credentials
Use when you have a P21 username and password. The user must be: - A valid, non-deleted Prophet 21 user - Associated with a Windows user, AAD user, or SQL user
V2 Request (Recommended)
Request:
POST /api/security/token/v2 HTTP/1.1
Host: play.p21server.com
Content-Type: application/json
Accept: application/json
{
"username": "api_user",
"password": "your_password"
}
Response:
{
"AccessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"RefreshToken": "dGhpcyBpcyBhIHNhbXBsZSByZWZyZXNoIHRva2Vu...",
"ExpiresInSeconds": 86400,
"TokenType": "Bearer"
}
V1 Request (Legacy)
Request:
POST /api/security/token HTTP/1.1
Host: play.p21server.com
username: api_user
password: your_password
Content-Type: application/json
Accept: application/json
Response: Same as V2.
Python Example
import httpx
def get_token_v2(base_url: str, username: str, password: str) -> dict:
"""Get token using V2 endpoint (recommended)."""
response = httpx.post(
f"{base_url}/api/security/token/v2",
json={"username": username, "password": password},
headers={"Accept": "application/json"}
)
response.raise_for_status()
return response.json()
def get_token_v1(base_url: str, username: str, password: str) -> dict:
"""Get token using V1 endpoint (legacy)."""
response = httpx.post(
f"{base_url}/api/security/token",
headers={
"username": username,
"password": password,
"Content-Type": "application/json",
"Accept": "application/json"
},
content=""
)
response.raise_for_status()
return response.json()
Method 2: Consumer Key
Use for service accounts and automated integrations. Consumer keys are created in the SOA Admin console.
Creating a Consumer Key
- Log in to SOA Admin Page (
https://{hostname}/api/admin) - Open the API Console tab
- Click Register Consumer Key
- Configure:
- Consumer: Descriptive name
- SDK Access: Enable for SDK access
- Token Expire: Key validity duration
- API Scope: Restrict access (see Scopes section)
Request
POST /api/security/token/v2 HTTP/1.1
Host: play.p21server.com
Content-Type: application/json
Accept: application/json
{
"ClientSecret": "62ccc18a-25e2-440c-bf6d-749c117fa9db",
"GrantType": "client_credentials"
}
With optional username (required for Interactive API):
{
"ClientSecret": "62ccc18a-25e2-440c-bf6d-749c117fa9db",
"GrantType": "client_credentials",
"username": "api_user"
}
API-Specific Behavior
| API | Without Username | With Username |
|---|---|---|
| OData | Works - uses consumer key scope | Username ignored |
| Transaction | Works - uses P21 install user | Uses specified user |
| Interactive | Does not work - username required | Works |
| Entity | Works - uses admin by default | Uses specified user |
API Scopes
Consumer keys can restrict access to specific endpoints and data.
URL Scopes
| Scope | Access |
|---|---|
/api |
All API endpoints |
/uiserver0 |
Interactive and Transaction APIs |
/odata |
OData endpoints (must specify tables) |
/api;/uiserver0 |
Multiple scopes (semicolon delimiter) |
OData Table Scopes
For OData access, specify allowed tables/views:
/odata:price_page,supplier,product_group
This restricts the token to only those tables.
P21 Permissions (User Credential Auth)
When authenticating with User Credentials (Method 1), generating a valid token is not sufficient for API access. The P21 user account must also have specific permissions enabled in the P21 Desktop Client. Without these, you'll receive a generic "You are not authorized to access API" error even with a valid token.
Note: Consumer Key authentication (Method 2) bypasses these requirements entirely. Access is controlled by the consumer key's API scope instead.
Step 1: Application Security
Each user must be explicitly granted API access in User Maintenance.
- Open User Maintenance in the P21 Desktop Client
- Pull up the user's details
- Go to the Application Security tab
- Find "Allow OData API Service" and set it to Yes (default is No)

Step 2: Role-Level Dataservice Permissions
After enabling Application Security, the user's role must grant access to specific tables and views.
- Open Role Maintenance in the P21 Desktop Client
- Pull up the role assigned to the user
- Go to the Dataservice Permission tab
- Set each required table/view to Allow

Key details about the Dataservice Permission screen:
- Schema Type dropdown filters between Tables, Views, or Both
- Allow All / Allow All Views / Allow All Tables checkboxes grant blanket access
- Each table/view can be individually set to Allow or Deny
- The list includes all 6000+ tables and views in the P21 database
Permission Requirements by Auth Method
| Auth Method | Application Security | Dataservice Permission | Notes |
|---|---|---|---|
| User Credentials | Required | Required | Both must be configured |
| Consumer Key (no username) | Not needed | Not needed | Access controlled by API scope |
| Consumer Key (with username) | Not needed | Not needed | User credentials are ignored for OData |
Troubleshooting
If you receive a 401 or 403 "not authorized" error with a valid token:
- Verify Allow OData API Service = Yes in User Maintenance → Application Security
- Verify the target table/view is set to Allow in Role Maintenance → Dataservice Permission
- Ensure you're checking the correct role (the one actually assigned to the user)
Using the Token
Include the token in the Authorization header for all API requests:
GET /odataservice/odata/table/supplier HTTP/1.1
Host: play.p21server.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Accept: application/json
Python Example
def get_auth_headers(token: str) -> dict:
"""Build authorization headers for API requests."""
return {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json"
}
# Usage
token_data = get_token_v2(base_url, username, password)
headers = get_auth_headers(token_data["AccessToken"])
response = httpx.get(
f"{base_url}/odataservice/odata/table/supplier",
headers=headers
)
Token Expiration
| Property | Value |
|---|---|
| Default lifetime | 24 hours (86400 seconds) |
| Returned in | ExpiresInSeconds field |
| Refresh token | Provided for token renewal |
Handling Expiration
import time
class TokenManager:
def __init__(self, base_url, username, password):
self.base_url = base_url
self.username = username
self.password = password
self.token_data = None
self.token_time = 0
def get_token(self) -> str:
"""Get valid token, refreshing if needed."""
now = time.time()
expires = self.token_data.get("ExpiresInSeconds", 0) if self.token_data else 0
# Refresh if expired or expiring in 5 minutes
if not self.token_data or (now - self.token_time) > (expires - 300):
self.token_data = get_token_v2(
self.base_url, self.username, self.password
)
self.token_time = now
return self.token_data["AccessToken"]
UI Server URL
The Interactive and Transaction APIs require the UI server URL, which is obtained after authentication:
GET /api/ui/router/v1?urlType=external HTTP/1.1
Host: play.p21server.com
Authorization: Bearer {token}
Accept: application/json
Response:
{
"Url": "https://play.p21server.com/uiserver0"
}
Python Example
def get_ui_server_url(base_url: str, token: str) -> str:
"""Get UI server URL for Interactive/Transaction APIs."""
response = httpx.get(
f"{base_url}/api/ui/router/v1?urlType=external",
headers=get_auth_headers(token)
)
response.raise_for_status()
return response.json()["Url"].rstrip("/")
XML Token Responses
Some P21 middleware instances return XML instead of JSON for token endpoints, even when Accept: application/json is set. This typically occurs with certain middleware versions or configurations.
Example XML Response
<?xml version="1.0" encoding="utf-8"?>
<TokenResponse>
<AccessToken>eyJhbGciOiJSUzI1NiIs...</AccessToken>
<TokenType>Bearer</TokenType>
<ExpiresIn>86400</ExpiresIn>
<RefreshToken>dGhpcyBpcyBhIHNhbXBsZQ...</RefreshToken>
</TokenResponse>
Note: The XML response uses
ExpiresInwhile the JSON response usesExpiresInSeconds. Both represent the token lifetime in seconds.
Handling Both Formats
import re
def parse_token_response(response: httpx.Response) -> dict:
"""Parse token response, handling both JSON and XML formats."""
# Try JSON first
try:
data = response.json()
if isinstance(data, dict) and "AccessToken" in data:
return data
except (ValueError, KeyError):
pass
# Fall back to XML regex parsing
text = response.text
result = {}
for field in ("AccessToken", "TokenType", "ExpiresIn",
"ExpiresInSeconds", "RefreshToken"):
match = re.search(rf"<{field}>([^<]*)</{field}>", text)
if match and match.group(1):
result[field] = match.group(1)
if "AccessToken" not in result:
raise ValueError(f"Could not parse token from response: {text[:500]}")
return result
Common Errors
| HTTP Code | Cause | Solution |
|---|---|---|
| 401 | Invalid credentials | Check username/password |
| 401 | Expired token | Request new token |
| 401 | Invalid consumer key | Verify key in SOA Admin |
| 403 | Scope restriction | Check consumer key scope |
| 404 | Wrong endpoint | Use /api/security/token or /api/security/token/v2 |
| 200 (XML body) | Middleware returning XML instead of JSON | Use dual-format parser (see XML Token Responses) |
Best Practices
- Use V2 endpoint for new integrations
- Store credentials securely - use environment variables, not code
- Handle token expiration - refresh 5 minutes before expiry to avoid failed requests
- Use consumer keys for service accounts
- Restrict scopes to minimum required access
- Disable SSL verification only in development (
verify=False) - Handle both JSON and XML token responses for maximum middleware compatibility
Related
- API Selection Guide
- Error Handling
- scripts/common/auth.py - Authentication module
- scripts/common/client.py - Reusable P21 API client with auto token refresh