Authentication
Overview
The Avista API uses authentication based on OAuth 2.0 with X.509 certificates (mTLS). This layered security model ensures that only authorized clients with valid certificates can access the API.
Why mTLS?
mTLS (mutual TLS) offers superior security compared to simple tokens:
- Mutual authentication: Both the client and the server authenticate each other
- Non-repudiation: Certificates linked to the account ensure traceability
- Protection against credential theft: Even with leaked clientId/clientSecret, the attacker would need the certificate
Prerequisites
Before you begin, you will need:
Obtain your client certificate through the Avista portal. The certificate must be in PEM format and will be linked to your account.
Request your credentials (clientId and clientSecret) in the admin panel.
Configure your environment to send the certificate in the X-SSL-Client-Cert header.
The X.509 certificate must be linked to your account before being used. Unlinked certificates will be rejected even if technically valid.
Authentication Endpoint
POST /api/auth/token
Generates a JWT access token valid for 30 minutes (1800 seconds).
The X.509 certificate must be sent URL-encoded in the X-SSL-Client-Cert header. The system validates the SHA256 fingerprint of the certificate against the records linked to the account.
Request
curl -X POST https://api.avista.global/api/auth/token \
-H "Content-Type: application/json" \
-H "X-SSL-Client-Cert: -----BEGIN%20CERTIFICATE-----%0AMIIB..." \
-d '{
"clientId": "account-93-550e8400",
"clientSecret": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
}'Response (201 Created)
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 1800
}Practical Example: Node.js
Installation
npm install axiosComplete Code
const axios = require('axios');
const fs = require('fs');
// Load X.509 certificate
const certificate = fs.readFileSync('./client-cert.pem', 'utf8');
const encodedCert = encodeURIComponent(certificate);
// Request configuration
const config = {
method: 'post',
url: 'https://api.avista.global/api/auth/token',
headers: {
'Content-Type': 'application/json',
'X-SSL-Client-Cert': encodedCert
},
data: {
clientId: process.env.AVISTA_CLIENT_ID,
clientSecret: process.env.AVISTA_CLIENT_SECRET
}
};
// Make request
async function getToken() {
try {
const response = await axios(config);
console.log('Token obtained successfully!');
console.log('Expires in:', response.data.expires_in, 'seconds');
return response.data.access_token;
} catch (error) {
console.error('Error obtaining token:', error.response?.data || error.message);
throw error;
}
}
getToken();Practical Example: Python
Installation
pip install requestsComplete Code
import os
import requests
import urllib.parse
# Load and encode certificate
with open('client-cert.pem', 'r') as f:
certificate = f.read()
encoded_cert = urllib.parse.quote(certificate)
# Request configuration
url = 'https://api.avista.global/api/auth/token'
headers = {
'Content-Type': 'application/json',
'X-SSL-Client-Cert': encoded_cert
}
payload = {
'clientId': os.environ.get('AVISTA_CLIENT_ID'),
'clientSecret': os.environ.get('AVISTA_CLIENT_SECRET')
}
# Make request
try:
response = requests.post(url, json=payload, headers=headers)
response.raise_for_status()
data = response.json()
print('Token obtained successfully!')
print(f"Expires in: {data['expires_in']} seconds")
except requests.exceptions.RequestException as e:
print(f'Error obtaining token: {e}')
if hasattr(e.response, 'text'):
print(f'Response: {e.response.text}')Using the Token
After obtaining the token, include it in the Authorization header of all requests:
curl -X GET https://api.avista.global/api/balance \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."Token Renewal
Tokens expire after 30 minutes. Implement automatic renewal logic in your application to avoid interruptions.
Recommended Strategy
class TokenManager {
constructor(clientId, clientSecret, certificatePath) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.certificatePath = certificatePath;
this.token = null;
this.expiresAt = null;
}
async getValidToken() {
// Check if the token is still valid (with a 30-second margin)
if (this.token && this.expiresAt && Date.now() < this.expiresAt - 30000) {
return this.token;
}
// Renew the token
return await this.refreshToken();
}
async refreshToken() {
const response = await this.requestNewToken();
this.token = response.access_token;
this.expiresAt = Date.now() + (response.expires_in * 1000);
return this.token;
}
async requestNewToken() {
const fs = require('fs');
const axios = require('axios');
const certificate = fs.readFileSync(this.certificatePath, 'utf8');
const encodedCert = encodeURIComponent(certificate);
const response = await axios.post('https://api.avista.global/api/auth/token', {
clientId: this.clientId,
clientSecret: this.clientSecret
}, {
headers: {
'Content-Type': 'application/json',
'X-SSL-Client-Cert': encodedCert
}
});
return response.data;
}
}
// Usage
const tokenManager = new TokenManager(
process.env.AVISTA_CLIENT_ID,
process.env.AVISTA_CLIENT_SECRET,
'./client-cert.pem'
);
// In any request
const token = await tokenManager.getValidToken();Certificate Validation
The system performs the following validations on the certificate:
- Valid PEM format: The certificate must be in PEM format and URL-encoded
- Account linkage: The SHA256 fingerprint of the certificate must be registered and linked to your account
- Credential matching: The certificate must belong to the same account as the OAuth credentials
Certificates that are unlinked or linked to another account will be rejected, even if technically valid.
Error responses
All errors returned by POST /api/auth/token follow the same shape:
{
"statusCode": 400,
"timestamp": "2026-04-24T16:59:25.577Z",
"path": "/api/auth/token",
"method": "POST",
"code": "PUB_CERT_MALFORMED_PEM",
"message": "Certificate could not be parsed",
"userMessage": "The provided certificate is malformed.",
"details": {
"hint": "The PEM could not be parsed. Common cause: '+' characters in the base64 body must be URL-encoded as '%2B', not '%20'."
},
"errorId": "81421e7a5afbd9cf25212f51dac08da1"
}code: stable machine-readable identifier — branch on this.message: short technical description (English, log-facing).userMessage: end-user-facing description (English).details.hint: actionable guidance for developers integrating the API.errorId: unique request identifier — quote it when contacting support.
Error codes reference
PUB_CERT_HEADER_MISSING (400)
The X-SSL-Client-Cert header was not present on the request.
How to fix: include the header with a URL-encoded PEM certificate. If you are behind a TLS-terminating gateway (NGINX/ALB), confirm the gateway is configured to forward $ssl_client_escaped_cert as X-SSL-Client-Cert.
PUB_CERT_MALFORMED_PEM (400)
The certificate could not be parsed at the gateway or the upstream service.
How to fix: the most common cause is incorrect URL encoding — the + characters that occur naturally in the base64 body must be encoded as %2B, not %20. Some encoders (e.g., Go's url.QueryEscape followed by a naive + → %20 replace) corrupt the base64 in this way. Use url.PathEscape in Go, or verify your encoder produces %2B for literal +.
PUB_REQUEST_BODY_INVALID (400)
The JSON body did not satisfy the schema (missing field, wrong type, or clientId not a UUID v4). Check details.violations for the offending field(s).
PUB_CERT_NOT_YET_VALID (401)
Certificate's notBefore date is in the future.
How to fix: sync your system clock via NTP. If the date is genuinely in the future, wait or request a new certificate.
PUB_CERT_EXPIRED (401)
Certificate's notAfter date has passed.
How to fix: request a new certificate from support and rotate it in your integration.
PUB_CERT_NOT_REGISTERED (401)
The SHA-256 fingerprint of the certificate is not in the registry — either it was never registered or it has been revoked.
How to fix: compute your local fingerprint with openssl x509 -noout -fingerprint -sha256 -in cert.pem and compare with the fingerprint you received at onboarding. If they match, contact support — the certificate may have been revoked. If they differ, contact support to register the new certificate.
Note: revoked certificates currently surface as
PUB_CERT_NOT_REGISTERED. A dedicatedPUB_CERT_REVOKEDcode may be introduced in a future release; until then, this code covers both cases and the support team disambiguates.
PUB_CERT_NOT_AUTHORIZED_FOR_ACCOUNT (403)
The certificate is valid and registered, but is not linked to the account that owns the clientId you provided.
How to fix: verify you are using the correct certificate/clientId pair for this environment.
PUB_INVALID_CREDENTIALS (401)
clientId is not recognized, or the clientSecret does not match. The platform deliberately does not differentiate between these cases (enumeration resistance).
How to fix: verify both clientId and clientSecret. If the problem persists, contact support with the errorId from the response.
PUB_AUTH_UPSTREAM_UNAVAILABLE (503)
The upstream authentication service is temporarily unreachable.
How to fix: retry with exponential backoff (e.g., 1s, 2s, 4s). If the issue persists for over a minute, contact support.
PUB_AUTH_UPSTREAM_ERROR (502)
The upstream returned an unrecognized error.
How to fix: contact support with the errorId — this code indicates a server-side issue we will investigate.
Migration notes (for existing integrations)
If your integration hard-codes checks on the old PUB_INVALID_CERTIFICATE or PUB_TOKEN_GENERATION_FAILED codes, they have been renamed:
| Old code | New code(s) |
|---|---|
PUB_INVALID_CERTIFICATE | PUB_CERT_MALFORMED_PEM (PEM parse failures only — other certificate problems now have dedicated codes) |
PUB_TOKEN_GENERATION_FAILED | PUB_AUTH_UPSTREAM_UNAVAILABLE (503) for transient unavailability, PUB_AUTH_UPSTREAM_ERROR (502) for unrecognized upstream errors |
PUB_INVALID_CREDENTIALS is retained but the meaning is narrower — it now fires only when the clientId or clientSecret is wrong, never as a catch-all for any 401 from the auth service.
Troubleshooting
"My certificate works with openssl but the API returns PUB_CERT_MALFORMED_PEM"
The PEM file on disk is fine — the issue is in transport. Most commonly this is the + → %20 URL-encoding bug. Print the exact value of the header your client sends, decode it, and compare byte-by-byte with the original PEM. The base64 should preserve every literal + as %2B.
"I occasionally get PUB_CERT_NOT_YET_VALID"
Your system clock has drifted past your certificate's notBefore. Configure NTP and ensure the host's clock is within seconds of UTC. Containers can inherit clock drift from the host — verify with date -u inside the container.
"I get PUB_CERT_NOT_REGISTERED after rotating certificates"
Compute the new certificate's fingerprint and confirm it was registered:
openssl x509 -noout -fingerprint -sha256 -in new-cert.pem. If the fingerprint differs from what we have on file, request registration with support before cutting over.