Every non-2xx API response is raised as a typed exception. Import from the package root:
from spekoai import SpekoApiError, SpekoAuthError, SpekoRateLimitError
| Exception | When | Attributes |
|---|
SpekoAuthError | HTTP 401 | message, status=401, code="AUTH_ERROR" |
SpekoRateLimitError | HTTP 429 | message, status=429, code="RATE_LIMITED", retry_after: int | None |
SpekoApiError | any other non-2xx | message, status, code |
message and code are parsed from the JSON error body when present ({"error": "...", "code": "..."}); otherwise they fall back to response.text and "UNKNOWN".
Example — targeted handling
import time
from spekoai import Speko, SpekoApiError, SpekoAuthError, SpekoRateLimitError
speko = Speko(api_key="sk_live_...")
try:
speko.complete(
messages=[{"role": "user", "content": "Hi"}],
intent={"language": "en", "vertical": "general"},
)
except SpekoAuthError:
raise
except SpekoRateLimitError as err:
time.sleep(err.retry_after or 1)
except SpekoApiError as err:
log.exception("speko call failed: %s (%s)", err.code, err.status)
SpekoAuthError and SpekoRateLimitError both inherit from SpekoApiError, so a bare except SpekoApiError catches all three. Always branch from most specific to most general.
Realtime errors
connect_realtime raises the same three exceptions from the initial POST /v1/sessions call. Once the WebSocket is open, transport failures surface as frames with type == "error":
async for frame in session:
if frame["type"] == "error":
log.error("realtime: %s — %s", frame["code"], frame["message"])
break