Sign in Get started

Error handling

The exception hierarchy, when each error fires, and how to retry safely.

View as Markdown

Every SDK error subclasses PictographError. Catch the specific subclass to handle a known failure mode; catch the base class to log and rethrow.

Hierarchy

PictographError
├── ConfigurationError    — missing API key, invalid base URL
├── AuthError             — 401 (bad / missing / revoked key)
├── ForbiddenError        — 403 (role lacks permission)
├── NotFoundError         — 404 (resource missing)
├── ConflictError         — 409 (duplicate name, optimistic-lock fail)
├── ValidationError       — 422 (payload shape rejected)
├── PaymentRequiredError  — 402 (out of credits)
├── RateLimitError        — 429 (per-key rate cap hit)
├── ServerError           — 5xx (transient backend failure)
├── NetworkError          — connection / DNS / TLS failure
├── RequestTimeoutError   — request exceeded the SDK's timeout budget
├── PollTimeoutError      — long-running job (training, batch SAM3) didn't finish
└── ApiError              — catch-all for unmatched status codes

Import from the top-level package:

from pictograph.exceptions import (
    PictographError, AuthError, ForbiddenError, NotFoundError,
    ConflictError, ValidationError, PaymentRequiredError,
    RateLimitError, ServerError, NetworkError, RequestTimeoutError,
    PollTimeoutError, ApiError,
)

When each fires

ExceptionCommon causeWhat to do
ConfigurationErrorPICTOGRAPH_API_KEY not set, no api_key= argSet the env var or pass api_key
AuthError (401)Key revoked / typoRe-issue the key
ForbiddenError (403)viewer key calling a write opUse a member+ key
NotFoundError (404)Dataset name typo (case-sensitive!)Verify with datasets list
ConflictError (409)Same image filename in same folderPass skip_existing=True to the upload workflow, or use a new name
ValidationError (422)class instead of name, flat polygon arrayFix the payload (see Annotation format)
PaymentRequiredError (402)Out of credits mid-operationShow e.upgrade_url to the user
RateLimitError (429)Per-key burst limitSDK auto-retries when Retry-After < 120s; otherwise raise
ServerError (5xx)Backend incidentSDK retries with exponential backoff; persistent failure surfaces
NetworkErrorConnection droppedRetry idempotent ops; investigate non-idempotent
PollTimeoutErrorTraining run exceeded timeoutRe-poll with client.training.get(run_id)

Retry behavior

The SDK already retries on transient failures with exponential backoff:

  • 5xx responses — up to 3 retries, backoff 1s → 2s → 4s.
  • 429 with Retry-After ≤ 120s — auto-waits then retries.
  • Network errors (connection reset, DNS blip) — same 3-retry policy.
  • Idempotency — retried requests inherit the original Idempotency-Key header, so the backend dedupes.

Override on the Client:

client = Client(timeout=30.0, max_retries=5)

PaymentRequiredError details

from pictograph.exceptions import PaymentRequiredError

try:
    client.training.create(dataset_name, export_name, pipeline_type="yolox")
except PaymentRequiredError as e:
    print(f"Need {e.required} credits, you have {e.remaining}")
    print(f"Top up at: {e.upgrade_url}")

required, remaining, and upgrade_url are populated from the backend’s detail block — fall back to plain str(e) if you only need a user-facing message.

ValidationError details

The backend returns a structured body listing every offending field:

from pictograph.exceptions import ValidationError

try:
    client.annotations.save(image_id, [{"class": "person", "type": "bbox"}])
except ValidationError as e:
    print(e)            # human-readable summary
    print(e.errors)     # list of {"loc": [...], "msg": "...", "type": "..."}

The most common cause is the class vs name field mistake — the backend rejects any annotation that uses class.

PollTimeoutError + recovery

Long-running jobs (training_pipeline, batch auto-annotate, large dataset imports) accept a timeout arg and raise PollTimeoutError when it elapses. The job is not cancelled — it keeps running on the backend.

from pictograph.exceptions import PollTimeoutError

try:
    run, model = train_pipeline(client, "ds", pipeline="yolox", timeout=60.0)
except PollTimeoutError as e:
    # Pick up later
    run_id = e.run_id  # most poll errors carry the resource ID
    run = client.training.get(run_id)
    if run.status == "completed":
        model = client.models.get(run.model_id)

Idempotency

For mutating ops the SDK auto-generates an Idempotency-Key header, so retries are safe. Override per-call:

client.images.upload(
    dataset_id=ds.id,
    file_path="x.jpg",
    idempotency_key="upload-x-jpg-2026-04-19",
)

Backend dedupes within 24h. Reusing the same key with a different body returns 409 ConflictError (error_code: idempotency_conflict).

See Rate limits for the per-tier limits and burst behaviour.

Copied to clipboard