# Go SDK

The Go SDK supports Go 1.22+ and uses only the standard library. It includes typed responses, context cancellation, streaming uploads and downloads, safe automatic retries, pagination iterators, webhook verification and an in-memory testing package.

## Choose a moderation flow

Use the SDK's moderate method to wait for a decision in the request, or its create method to queue moderation and let your app continue. Text and images support both flows. Audio and video always run asynchronously, even when using moderate.

For queued content, save the returned asset ID and [set up a webhook](/docs/guides/webhooks) for [`asset.classified`](/docs/api/events#event-asset-classified) (decision ready) and [`asset.failed`](/docs/api/events#event-asset-failed) (processing failed). Keep content unpublished until approved. You can retrieve the asset periodically instead of receiving events. The [quick start](/docs) demonstrates both flows.

## Install

> The Go SDK is not yet available from the Go module proxy.

## Moderate text

Keep API credentials on your server. Each network method accepts a `context.Context` and returns an error.

```go
package main

import (
    "context"
    "fmt"
    "log"
    "os"

    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)
}
    asset, err := client.Assets.Moderate(context.Background(), calmlens.SubmitAssetOptions{
        Name: "text",
        Content: &calmlens.Content{
            Type: "text/plain",
            PlainText: calmlens.Ptr("Great photo!"),
        },
        ExternalID: calmlens.Ptr("comment-42"),
    })
    if err != nil {
    log.Fatal(err)
}
    if asset.Status == "approved" {
        fmt.Println("Approved", asset.Report)
    }
}
```

Only publish after an explicit approval. An approved, flagged result can still need review. A category with no score is unknown, rather than safely zero. See [Report format](/docs/guides/moderation-reports).

## Upload and deliver images

Inside your application, using the client above:

```go
ctx := context.Background()
file, err := os.Open("photo.jpg")
if err != nil {
    log.Fatal(err)
}
defer file.Close()
queued, err := client.Assets.Create(ctx, calmlens.SubmitAssetOptions{
    Name: "photo.jpg",
    Content: &calmlens.Content{Type: "image/jpeg", Data: file},
    Visibility: "private",
    ProcessingOptions: calmlens.JSON{"keepContentAfterProcessing": true},
})
if err != nil {
    log.Fatal(err)
}

// Retrieve later, or respond to an asset.classified webhook.
current, err := client.Assets.Retrieve(ctx, calmlens.AssetIDOptions{AssetID: queued.ID})
if err != nil {
    log.Fatal(err)
}
if current.Status == "approved" {
    signed, err := client.Assets.CreateSignedURL(ctx, calmlens.SignedURLOptions{
        AssetID: current.ID, Resource: "content", ExpiresInSeconds: 60,
    })
    if err != nil {
    log.Fatal(err)
}
    fmt.Println(signed.URL)
}
```

`Assets.Create` queues processing. `Assets.Moderate` waits for classification for supported content; audio and video remain asynchronous. `Data` readers are streamed without buffering, and timeouts never cut off an upload or a `CDN.Serve` download: bound those with a context deadline. Multipart readers need an exact `SizeInBytes`; send parts sequentially. CalmLens saves the queue mode chosen when a multipart upload starts, so any process can finish it with the asset ID; `UploadAssetPartOptions.Queue` overrides it, and `SubmitAssetOptions.TotalSizeInBytes` declares the whole file size up front. Remote downloads are limited to 10 MB for images and 20 MB for other files. See [Assets API](/docs/api/assets) for upload limits.

## Operations

| Method | Options type |
| --- | --- |
| `Assets.Create/Moderate(ctx, options)` | `SubmitAssetOptions` |
| `Assets.Retrieve/Retry/DeleteContent/Delete(ctx, options)` | `AssetIDOptions` |
| `Assets.Update(ctx, options)` | `UpdateAssetOptions` |
| `Assets.List(ctx, options)` | `ListAssetsOptions` |
| `Assets.ListChildren(ctx, options)` | `ListAssetChildrenOptions` |
| `Assets.UploadPart(ctx, options)` | `UploadAssetPartOptions` |
| `Assets.Move(ctx, options)` | `MoveAssetOptions` |
| `Assets.CreateSignedURL(ctx, options)` | `SignedURLOptions` |
| `Folders.List(ctx)` | No options. |
| `Folders.Create/Retrieve/Update/Move/Delete(ctx, options)` | Corresponding folder options. |
| `Events.List(ctx, options)` | `ListEventsOptions` |
| `Assets.ListAll` / `Assets.ListChildrenAll` / `Events.ListAll(options)` | Iterators over every page; see below. |
| `EnsureLens/EnsureLenses(ctx, options)` | `EnsureLensOptions` / `EnsureLensesOptions` |
| `CDN.GetAssetURL(options)` / `CDN.Serve(ctx, options)` | `CDNAssetURLOptions` / `CDNServeOptions` |

Use `calmlens.Ptr(value)` for optional scalars. `FolderID: nil` moves an asset to the root. Updates use `UpdateAssetOptions.Fields`, a JSON patch map; omitted keys stay unchanged and `nil` clears a nullable field.

## Pagination

`Assets.ListAll`, `Assets.ListChildrenAll` and `Events.ListAll` return iterators that fetch pages as you go:

```go
it := client.Assets.ListAll(calmlens.ListAssetsOptions{PageSize: 100})
for it.Next(ctx) {
    fmt.Println(it.Asset().ID)
}
if err := it.Err(); err != nil {
    log.Fatal(err)
}
```

On Go 1.23 or later, `for asset, err := range it.All(ctx)` works too. For events, start from a saved cursor and store `it.Cursor()` afterwards.

## Retries

Reads (`GET`) and uploads (`Create`, `Moderate`, `Text.Moderate`, `UploadPart`) are retried up to `Options.MaxRetries` times (default 2) with jittered backoff after a connection failure or timeout, `429`, `502`, `503`, `504` or `idempotency/request-in-progress`, honouring `Retry-After`. Uploads always send an `Idempotency-Key`, generated per call and reused for the SDK's retries, so a retry never creates a second asset. Set `IdempotencyKey` yourself to retry safely across restarts. Other writes are never retried automatically. Set `MaxRetries: calmlens.Ptr(0)` to turn retries off. See [Retrying requests safely](/docs/guides/error-handling#retrying-requests-safely).

## Errors

Every failure is a `*calmlens.Error`, including invalid arguments and encoding failures. It carries `Code`, `Status`, `RequestID` (the `X-Request-Id` header), `Header`, `RetryAfter`, `Details` and `Cause`, and `Error()` includes the code, status and request ID. Compare codes with the generated constants, such as `calmlens.CodeAssetNotFound`, or use `calmlens.IsNotFound(err)`, `calmlens.IsRetryable(err)` and `calmlens.ErrorCode(err)`. Connection, TLS, protocol, decompression and truncated-body failures have code `sdk/transport-error`, timeouts `sdk/timeout` and cancellation `sdk/cancelled`; the original error stays in `Cause`, so `errors.Is` and `errors.As` still work. Arguments rejected before a request use `sdk/invalid-argument`, and invalid client options `sdk/invalid-config`.

`Options.Timeout` (default 120 seconds) bounds each attempt of a JSON API call. The default transport also limits connecting (`ConnectTimeout`, 10 seconds) and waiting for response headers (`ResponseHeaderTimeout`, 120 seconds). Requests send `User-Agent: calmlens-go/<version>`.

## Webhooks

Verify the exact raw webhook bytes and the signature and timestamp headers before parsing JSON. Timestamps more than 5 minutes from now are rejected. With `net/http`, chi or any router that passes the `*http.Request`, `VerifyWebhookRequest` reads the body (up to 1 MiB), verifies it and returns the event:

```go
func handleWebhook(w http.ResponseWriter, r *http.Request) {
	event, err := calmlens.VerifyWebhookRequest(r, os.Getenv("CALMLENS_WEBHOOK_SECRET"))
	if err != nil {
		http.Error(w, "Invalid webhook", http.StatusUnauthorized)
		return
	}
	// Deduplicate by event.ID, queue the work, then respond quickly.
	w.WriteHeader(http.StatusNoContent)
}
```

Call it before any middleware reads or decodes the body. With gin, pass `c.Request` before binding. If you already hold the raw bytes, `calmlens.ParseWebhookEvent(rawBody, signature, timestamp, secret)` verifies and decodes them, and `calmlens.VerifyWebhookSignature` returns a boolean. `calmlens.WithTolerance`, `calmlens.WithNow` and `calmlens.WithMaxBodyBytes` change the defaults.

`calmlens.WebhookDeliveryHeader` (`x-calmlens-delivery`) identifies each delivery for deduplication. `calmlens.SignWebhookPayload(rawBody, timestamp, secret)` takes the same argument order and signs test deliveries.

Install the standalone [CalmLens CLI](/docs/guides/local-development) to forward events locally:

```bash
calmlens listen --forward-to http://localhost:3000/webhook --env-file .env.local
calmlens trigger clean
```

See [Webhooks & Connectors](/docs/guides/webhooks) for event handling and signatures. When using `CDN.Serve`, check the response status and close `response.Body`; the body is streamed. Use short-lived signed URLs for private content.

## Test without API calls

See [Go testing](/docs/sdk/go/testing) for offline mock setup, request inspection and simulated failures.
