# Error handling

A failed API request returns an HTTP error. A successfully queued upload can separately enter the asset `error` state if processing fails. Handle both paths.

## Request errors

HTTP errors normally return:

```json
{
  "code": "asset/not-found",
  "message": "Asset not found",
  "traceId": "..."
}
```

The SDKs return structured API errors. Handle errors by their `code`; log the message and details when debugging.

**python**

```python
import os
from calmlens import CalmLensClient

client = CalmLensClient(
    api_key=os.environ["CALMLENS_API_KEY"],
    project_id=os.environ["CALMLENS_PROJECT_ID"],
)

from calmlens import CalmLensError

try:
    asset = client.assets.retrieve({"assetId": "your-asset-id"})
except CalmLensError as error:
    if error.code == "asset/not-found":
        asset = None
    else:
        raise
```

**go**

```go
package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "errors"
    calmlens "github.com/Censkh/CalmLens/packages/go-sdk"
)

func main() {
    client, err := calmlens.NewClient(calmlens.Options{APIKey: os.Getenv("CALMLENS_API_KEY"), ProjectID: os.Getenv("CALMLENS_PROJECT_ID")})
    if err != nil { log.Fatal(err) }
    ctx := context.Background()
    asset, err := client.Assets.Retrieve(ctx, calmlens.AssetIDOptions{AssetID: "your-asset-id"})
    if err != nil {
        var apiError *calmlens.Error
        if errors.As(err, &apiError) && apiError.Code == calmlens.CodeAssetNotFound {
            fmt.Println("Asset not found")
            return
        }
        log.Fatal(err)
    }
    fmt.Println(asset.ID)
}

```

**JavaScript**

```javascript
import { CalmLensError } from "@calmlens/sdk";

try {
  await client.assets.retrieve({ assetId });
} catch (error) {
  if (error instanceof CalmLensError && error.code === "asset/not-found") {
    return undefined;
  }
  throw error;
}
```

**ruby**

```ruby
require 'calmlens'

client = CalmLens::Client.new(api_key: ENV.fetch('CALMLENS_API_KEY'), project_id: ENV.fetch('CALMLENS_PROJECT_ID'))
result = client.assets.retrieve(asset_id: "your-asset-id")
puts result
client.close
```

**rust**

```rust
use calmlens::{models::*, Client};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new(std::env::var("CALMLENS_API_KEY")?, std::env::var("CALMLENS_PROJECT_ID")?)?;
    let result = client.assets().retrieve(AssetId { asset_id: "your-asset-id".into(), ..Default::default() }).await?;
    println!("{result:?}");
    Ok(())
}
```

**java**

```java
import com.calmlens.ApiException;
import com.calmlens.CalmLensClient;
import com.calmlens.ErrorCodes;
import com.calmlens.Models.*;

public class Example {
  public static void main(String[] args) {
    try (var client = CalmLensClient.builder(System.getenv("CALMLENS_API_KEY"), System.getenv("CALMLENS_PROJECT_ID")).build()) {
      Asset asset = client.assets.retrieve(new AssetId("your-asset-id"));
      System.out.println(asset.id());
    } catch (ApiException error) {
      if (error.getCode().equals(ErrorCodes.ASSET_NOT_FOUND)) System.out.println("Asset not found");
      else System.err.println(error.getCode() + " (request " + error.getRequestId() + ")");
    }
  }
}

```

Every response, successful or not, carries an `X-Request-Id` header that identifies the request in CalmLens logs. Log it with failed requests and include it when you contact support.

Retry network failures and server errors with capped exponential backoff. Do not automatically retry validation errors. For a multipart `409`, check the expected part number or restart the upload. Retrying the same stale part will fail again.

## Retrying requests safely

A request that timed out may have succeeded. Retrying a create or moderate request could then create a second asset that counts toward your usage. To prevent this, send an `Idempotency-Key` header on [`POST /projects/:projectId/assets`](/docs/api/assets#create-asset), [`POST /projects/:projectId/assets/upload`](/docs/api/assets#upload-asset-file), [`POST /projects/:projectId/assets/multipart-upload`](/docs/api/assets#start-multipart-file-upload) and [`POST /assets/:assetId/part`](/docs/api/assets#upload-asset-part), and send the same key with every retry of that request:

```text
Idempotency-Key: 4f9a1c2e-8b7d-4e61-9a35-0c2b7f1d6e80
```

- **Use a new key for each request.** A UUID works well. Keys can be up to 255 printable ASCII characters; anything else returns [`400 idempotency/invalid-key`](/docs/api/assets#post-asset-error-idempotency-invalid-key).
- **A retry returns the saved response.** When the first request succeeded, a retry with the same key gets its response for 24 hours, with the header `Idempotent-Replayed: true`, and nothing runs again.
- **The key belongs to one request.** Reusing a key for a different request (another path, other query parameters or another body) returns [`409 idempotency/key-reused`](/docs/api/assets#post-asset-error-idempotency-key-reused). Raw uploads are compared by their query parameters, `Content-Type` and `Content-Length` rather than by their bytes.
- **Concurrent retries wait their turn.** While the first request is still running, a retry returns [`409 idempotency/request-in-progress`](/docs/api/assets#post-asset-error-idempotency-request-in-progress) with a `Retry-After` header. Wait that many seconds and retry with the same key to receive the result.
- **Failures are not saved.** If the first request failed, the key is freed and a retry runs the request again, so you can fix the problem and retry with the same key.
- **Keys are scoped to your API key.** A key sent with one API key never matches a request sent with another.

An `externalId` is unique within a project, so it also protects against duplicates: a second asset with the same `externalId` is refused with [`409 asset/duplicate-external-id`](/docs/api/assets#post-asset-error-asset-duplicate-external-id). Unlike an idempotency key, it doesn't return the first response, so retrieve the existing asset instead.

## Rate limits

CalmLens does not currently publish a general requests-per-second limit. If a request is throttled, it returns `429` with a `Retry-After` header giving the number of seconds to wait before retrying. Account upload allowances are enforced separately: exhausting the free upload allowance returns [`403 upload/limit-exceeded`](/docs/api/assets#post-asset-error-upload-limit-exceeded). File-size and multipart constraints return their documented `upload/*` errors.

## Choose a recovery action

| Failure | Recovery |
| --- | --- |
| Missing or invalid credentials, or insufficient access | Fix credentials and permissions before retrying. |
| Validation or unsupported input | Correct the request or file. |
| Network timeout or server failure | Retry with capped backoff and the same `Idempotency-Key`; without one, check whether a write succeeded before repeating it. |
| Throttled (`429`) or request still in progress | Wait for the `Retry-After` delay, then retry. |
| Idempotency key reused for a different request | Send the new request with a new key. |
| Upload allowance exhausted | Resolve account usage or billing before uploading again. |
| Multipart conflict | Check the expected part number or restart the upload. |

## Processing failures

A queued request can succeed while classification later fails. Watch for asset status `error` through retrieval or events; do not publish that content. Inspect its failure details, correct the input when needed, then retry an eligible asset. See [Asset lifecycle](/docs/guides/asset-lifecycle).
