Error Handling
Each API version has a different convention for communicating errors. Understanding these differences is straightforward once you know what to look for.
Response Patterns by API Version
v1 — Check the response body for success or failure indicators
V1 endpoints return HTTP 200 for successful data operations. The response body indicates whether the operation succeeded, so always read the body rather than relying on the status code alone.
Authentication failures and requests for inaccessible forms return HTTP 401 and HTTP 403 respectively — these do not return JSON bodies.
import requests
from base64 import b64encode
AMS_SERVER = "your-server.smartabase.com"
AMS_APP = "your-site"
USERNAME = "your.username"
PASSWORD = "your.password"
BASE_URL = f"https://{AMS_SERVER}/{AMS_APP}"
AUTH = b64encode(f"USERNAME:{PASSWORD}".encode()).decode()
HEADERS = {"Authorization": f"Basic {AUTH}", "Content-Type": "application/json"}
def safe_v1_post(url, payload):
resp = requests.post(url, json=payload, headers=HEADERS)
resp.raise_for_status() # Catches 401, 403, 5xx errors
body = resp.json() if resp.text.strip().startswith("{") else resp.text
# Some v1 endpoints return a 'state' field
if isinstance(body, dict) and body.get("state") in ("FAILED", "ERROR"):
raise ValueError(f"v1 error: {body}")
return body
# Example: synchronise event data
try:
result = safe_v1_post(
f"{BASE_URL}/api/v1/synchronise?informat=json&format=json",
{"formName": "My Form", "lastSynchronisationTimeOnServer": 0, "userIds": [1009]},
)
print(f"Success: {len(result.get('dataList', []))} records")
except requests.HTTPError as e:
print(f"HTTP error: {e.response.status_code}")
except ValueError as e:
print(f"AMS error: {e}")v2 — Check for __is_rpc_exception__ in the response body
__is_rpc_exception__ in the response bodyV2 endpoints return HTTP 200 for both successful and failed requests. Failed operations return a JSON body containing "__is_rpc_exception__": true with a type and value describing the error. Successful operations return the expected data shape.
def check_v2_response(body):
if isinstance(body, dict) and body.get("__is_rpc_exception__"):
error_type = body.get("type", "UnknownError")
detail = (body.get("value") or {}).get("detailMessage") or error_type
raise ValueError(f"v2 error [{error_type}]: {detail}")
return body
# Example: authenticate user (v2)
resp = requests.post(
f"{BASE_URL}/api/v2/user/loginUser",
json={
"username": USERNAME,
"password": PASSWORD,
"loginProperties": {"appName": AMS_APP, "clientTime": "2026-05-01T09:00"},
},
headers={"session-header": "...", "Cookie": "JSESSIONID=...", "Content-Type": "application/json"},
)
resp.raise_for_status()
body = check_v2_response(resp.json())v3 — Use standard HTTP status codes
V3 returns meaningful HTTP status codes and a structured error body for every failure. Write your error handling the same way you would for any REST API.
SESSION_HEADERS = {
"session-header": "your-session-token",
"Cookie": "JSESSIONID=your-session-token",
"Content-Type": "application/json",
}
resp = requests.get(
f"{BASE_URL}/api/v3/group-athletes/9999",
headers=SESSION_HEADERS,
)
if resp.status_code == 404:
error = resp.json()["errors"][0]
print(f"Not found [{error['errorCode']}]: {error['message']}")
elif resp.status_code == 422:
errors = resp.json()["errors"]
for e in errors:
print(f"Validation error [{e['errorCode']}]: {e['message']}")
elif resp.status_code == 200:
print("Success:", resp.json())
else:
resp.raise_for_status()The v3 error body always follows this shape:
{
"status": 404,
"href": "/api/v3/group-athletes/9999",
"errors": [
{
"errorCode": "itemNotFound",
"message": "Could not find group 9999.",
"metaData": null
}
]
}Quick Reference
| Status / Indicator | Version | Meaning |
|---|---|---|
| HTTP 401 | v1/v2/v3 | Authentication failed — check credentials |
| HTTP 403 | v1/v2/v3 | No access to the form or resource |
HTTP 200 + "state": "FAILED" | v1 | Import or sync operation failed |
HTTP 200 + "__is_rpc_exception__": true | v2 | Operation failed — check type and value for detail |
HTTP 404 + errorCode: "itemNotFound" | v3 | Resource not found |
HTTP 422 + errorCode: "requiredFieldMissing" | v3 | Missing a required field |
HTTP 422 + errorCode: "valueNotParsable" | v3 | Malformed request body |
| HTTP 500 | v1 | Payload too large or missing required fields — see below |
Troubleshooting
HTTP 500 on /api/v1/synchronise
/api/v1/synchroniseThis usually means the request payload is too large. Common causes:
lastSynchronisationTimeOnServer: 0on a form with a large history (forces a full scan)- Requesting too many users without pagination
Fix: Pass a non-zero lastSynchronisationTimeOnServer on subsequent calls to fetch only changes since your last sync. On first sync, paginate in batches of 100.
HTTP 403
Your API account does not have access to the form or resource. Check:
- The account has Coach access to the groups containing the users whose data you need
- The account has Read/Write access to the form in the AMS Admin portal
- You are using the correct form name (exact match, case-sensitive)
Session expiry (v2/v3)
Sessions expire after a period of inactivity. If you receive unexpected failures on v2/v3 calls after a long gap, re-authenticate using the full two-step flow and retry.
def with_session_retry(fn, get_session_fn, max_retries=1):
"""Call fn(). If it raises a session error, re-authenticate and retry."""
for attempt in range(max_retries + 1):
try:
return fn()
except requests.HTTPError as e:
if e.response.status_code in (401, 403) and attempt < max_retries:
get_session_fn()
continue
raiseSilent failures on bulk import
POST /api/v1/eventsimport returns HTTP 200 even if all records failed. Always check result.state and eventImportResultForForm[*].eventImportResults.state in the response body.
result = resp.json()
overall_state = result.get("result", {}).get("state", "")
if overall_state != "SUCCESSFULLY_IMPORTED":
print(f"Import failed: {overall_state}")
for form_result in result.get("eventImportResultForForm", []):
print(form_result["formName"], form_result["eventImportResults"]["message"])Updated 7 days ago
