Reference

Error Handling

Which HTTP status raises which class in each SDK, what an error carries, and which failures the SDKs retry before you see them.

Every LevelFour SDK maps an HTTP status code to its own typed error class, and every one of those classes inherits from a base class you can catch when the specific type does not matter. Some failures never reach your handler at all, because the SDK retries them first.

Error types

HTTP StatusError CodePythonTypeScriptGo
400BAD_REQUESTBadRequestErrorBadRequestError*BadRequestError
401AUTHENTICATION_ERRORAuthenticationErrorAuthenticationError*UnauthorizedError
403AUTHORIZATION_ERRORAuthorizationErrorAuthorizationError*ForbiddenError
404NOT_FOUNDNotFoundErrorNotFoundError*NotFoundError
409CONFLICTConflictErrorConflictError*ConflictError
422VALIDATION_ERRORValidationErrorValidationError*UnprocessableEntityError
429RATE_LIMIT_EXCEEDEDRateLimitErrorRateLimitError*TooManyRequestsError
500INTERNAL_SERVER_ERRORInternalServerErrorInternalServerError*InternalServerError

Each SDK's base class:

  • Python: LevelFourError
  • TypeScript: LevelFourError
  • Go: *core.APIError (use errors.As to match specific types)

Python and TypeScript add two more, for failures that carry no HTTP status:

  • Python: LevelFourConnectionError, LevelFourTimeoutError
  • TypeScript: LevelFourConnectionError, LevelFourTimeoutError
In Python status_code is 0 on both, so a branch keyed on the status code will not catch a connection failure or a timeout. Catch the class by name.

Catching errors

from levelfour import (
    LevelFour,
    LevelFourError,
    AuthenticationError,
    NotFoundError,
    RateLimitError,
    ValidationError,
)

client = LevelFour()

try:
    detail = client.recommendations.get("rec_123")
except NotFoundError as e:
    print(f"Not found: {e.message}")
    print(f"Status: {e.status_code}")
    print(f"Code: {e.code}")
except RateLimitError as e:
    print(f"Rate limited. Retry after: {e.retry_after}")
except ValidationError as e:
    print(f"Validation errors: {e.details}")
except AuthenticationError as e:
    print(f"Auth failed: {e.message}")
except LevelFourError as e:
    print(f"API error {e.status_code}: {e.message}")
Order the branches specific first, base last. In Python and TypeScript a branch on LevelFourError matches every class in the table above, so a specific branch below it never runs and you never read the fields that class adds.

Error properties

All exceptions inherit from LevelFourError:

PropertyTypeDescription
status_codeintHTTP status code (0 for connection/timeout errors)
codestrError code string
messagestrHuman-readable error message
detailsdict | list | NoneAdditional error context

RateLimitError adds:

PropertyTypeDescription
retry_afterstr | NoneSeconds until retry is allowed

All error classes extend LevelFourError:

PropertyTypeDescription
statusCodenumber | undefinedHTTP status code
bodyunknownParsed error response body
rawResponseRawResponse | undefinedRaw HTTP response
messagestringError message

All error types embed *core.APIError:

PropertyTypeDescription
StatusCodeintHTTP status code
BodytypedParsed error response (type varies per error)

The Body field is typed per error: BadRequest, AuthenticationError, AuthorizationError, NotFound, Conflict, *HTTPValidationError, RateLimitError, InternalServer.

Retry behavior

Each SDK retries a failed request on its own, with exponential backoff. The default is two retries.

Retryable errors

ConditionRetried
Network errors / timeoutsYes
408 Request TimeoutYes
409 ConflictYes
429 Too Many RequestsYes
5xx Server ErrorsYes
400 Bad RequestNo
401 UnauthorizedNo
403 ForbiddenNo
404 Not FoundNo
422 Validation ErrorNo

Configuring retries

# five retries
client = LevelFour(max_retries=5)

# no retries at all
client = LevelFour(max_retries=0)
Turn retries off while you are debugging. A failure comes back at once instead of after two backoffs.

Per-request override

summary = client.recommendations.get_savings_by_provider(
    request_options={"max_retries": 5},
)

Retries are one of several per-request options. Each SDK page has the full set: Python, TypeScript, Go.

Rate limits

Each API key can make 600 requests a minute. Past that, the API answers 429 with the code RATE_LIMIT_EXCEEDED, and the SDKs back off and retry it as the retryable errors table shows. A batch job that walks many pages stays under the limit by paging sequentially rather than in parallel.

Idempotent writes

Send an Idempotency-Key header on a POST, PATCH or PUT to make retrying it safe.

  • The key must be a version 4 UUID. Anything else is a 422.
  • A successful response is kept for 24 hours. The same key from the same API key inside that window gets the stored response back, with an Idempotent-Replayed: true header, and the write does not run again.
  • A failed request, 4xx or 5xx, is not kept, so retrying it with the same key runs it.
  • A request whose key is still in flight gets 409. Wait for the first one to finish.
  • Keys belong to the caller that sent them. Two API keys using the same value do not collide.
curl -s -X POST "https://api.levelfour.ai/api/v1/webhooks/endpoints" \
  -H "Authorization: Bearer $LEVELFOUR_API_KEY" \
  -H "Idempotency-Key: $(uuidgen | tr 'A-Z' 'a-z')" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/webhook", "event_types": ["optimization.completed"]}'

Generate a new key for each distinct write, and reuse it only when retrying that same write.

Next

  • SDKs compares the three clients and their constructor options, including the retry default
  • API Reference is the JSON envelope these error codes arrive in over REST
  • Pagination is the other cross-SDK behavior, with auto-paginating iterators

On this page

Ask the FinOps Agent about your cloud spend