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

V2 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 / IndicatorVersionMeaning
HTTP 401v1/v2/v3Authentication failed — check credentials
HTTP 403v1/v2/v3No access to the form or resource
HTTP 200 + "state": "FAILED"v1Import or sync operation failed
HTTP 200 + "__is_rpc_exception__": truev2Operation failed — check type and value for detail
HTTP 404 + errorCode: "itemNotFound"v3Resource not found
HTTP 422 + errorCode: "requiredFieldMissing"v3Missing a required field
HTTP 422 + errorCode: "valueNotParsable"v3Malformed request body
HTTP 500v1Payload too large or missing required fields — see below

Troubleshooting

HTTP 500 on /api/v1/synchronise

This usually means the request payload is too large. Common causes:

  • lastSynchronisationTimeOnServer: 0 on 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
            raise

Silent 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"])