Easy Labs
SDKsPython

Error handling

Error classes, retry semantics, and idempotency.

Every non-2xx response is mapped to a typed exception that subclasses easylabs.EasyError. Catch the specific subclass for targeted handling or EasyError to catch everything the SDK throws.

from easylabs import (
    EasyError,            # base class for every SDK error
    AuthenticationError,  # 401
    PermissionError,      # 403  — note: shadows the Python builtin
    NotFoundError,        # 404
    ConflictError,        # 409
    InvalidRequestError,  # 400 / 422
    RateLimitError,       # 429
    ServerError,          # 5xx
)

easylabs.PermissionError shadows the Python built-in PermissionError. Always import explicitly from easylabs (or reference as easylabs.PermissionError) to keep things unambiguous.

Every EasyError carries the same fields:

AttributeTypeNotes
messagestrHuman-readable summary.
statusintHTTP status code.
codestr | NoneServer-supplied error code (e.g. "INVALID_CARD").
detailsAnyField-level details from the API.
retry_after_secondsint | NoneParsed from the Retry-After header on 429s.
rawAnyThe raw decoded JSON body (or None).

Retryable vs. non-retryable

HTTP statusExceptionRetry?
400InvalidRequestErrorNo — fix the request.
401AuthenticationErrorNo — fix the key.
403PermissionErrorNo — caller lacks the required scope.
404NotFoundErrorNo — the ID doesn't exist (or never did).
409ConflictErrorNo — re-read state, then retry deliberately.
422InvalidRequestErrorNo — validation failed.
429RateLimitErrorYes — wait retry_after_seconds.
5xxServerErrorYes — exponential backoff with jitter.

A minimal retry loop:

import time
from easylabs import RateLimitError, ServerError

def with_retries(fn, *, attempts: int = 5):
    for i in range(attempts):
        try:
            return fn()
        except RateLimitError as e:
            time.sleep(e.retry_after_seconds or 1)
        except ServerError:
            time.sleep(min(2 ** i, 30))
    return fn()  # last attempt; let the exception escape if it fails

Idempotency keys

Every mutating method on every resource accepts an idempotency_key= keyword argument. The SDK sends it as the Idempotency-Key header. Replays of the same key within the server's retention window return the original response — safe to retry without creating duplicate charges, customers, or refunds.

import uuid

key = f"refund-{order_id}-{uuid.uuid4()}"

client.transfers.create_refund(
    transfer_id,
    refund_amount=2500,
    idempotency_key=key,
)

A few rules of thumb:

  • Generate the key once per logical operation (e.g. one per checkout attempt), then reuse it across retries.
  • Use UUIDs or any unique string up to 255 characters.
  • Read endpoints (list, retrieve) accept the header but ignore it — no harm in passing it through unconditionally if your transport middleware does.

The SDK does not retry automatically. Pair idempotency_key= with the retry loop above to make the combination safe.

Network errors

Network-level failures are not wrapped — httpx raises them directly. You'll see things like:

  • httpx.ConnectError — DNS / TCP failure.
  • httpx.ReadTimeout / httpx.WriteTimeout — request exceeded the default 30s timeout.
  • httpx.RemoteProtocolError — malformed server response.

Handle them alongside EasyError if you need a single retry layer:

import httpx
from easylabs import EasyError

try:
    customer = client.customers.retrieve("cus_123")
except (httpx.HTTPError, EasyError) as e:
    log.warning("Easy API call failed: %s", e)
    raise

On this page