# Go SDK

> Every namespace on the Go client, the options NewClient takes, the iterator its paginated methods return, and the typed errors errors.As matches.

Source: https://docs.levelfour.ai/sdks/go


The Go client groups the LevelFour API into one namespace per resource on the client struct: `client.Costs`, `client.Recommendations`, `client.Commitments` and the rest. Every method takes a `context.Context` first.

> **Note**
>
> Per-provider methods take a `providerID` string after the context (`"aws"`, `"gcp"`, `"azure"`, `"k8s"`). Methods with optional filters take a request struct next. Pass an empty one, such as `&levelfour.GetSummaryCostsRequest{}`, to take every default.

## Installation

```bash
go get github.com/LevelFourAI/levelfour-go@latest
```

## Client setup

```go
import "github.com/LevelFourAI/levelfour-go/levelfour"

client, err := levelfour.NewClient("l4_live_...")
if err != nil {
    log.Fatal(err)
}
```

Pass an empty string and the client reads the key from the environment instead. [Authentication](/authentication#environment-variable-recommended) names the variable.

```go
client, err := levelfour.NewClient("")
```

Confirm the client is wired before larger calls. `GetWhoami` answers only when the base URL resolves and the key is valid, which makes it a good CI smoke test.

```go
me, err := client.Auth.GetWhoami(ctx)
```

### Constructor options

```go
client, err := levelfour.NewClient("l4_live_...",
    levelfour.WithBaseURL("https://api.staging.levelfour.ai"),
    levelfour.WithMaxRetries(3),
    levelfour.WithHTTPClient(&http.Client{Timeout: 60 * time.Second}),
)
```

| Option      | Function                      | Default                    |
| ----------- | ----------------------------- | -------------------------- |
| Base URL    | `levelfour.WithBaseURL(url)`  | `https://api.levelfour.ai` |
| Max Retries | `levelfour.WithMaxRetries(n)` | `2`                        |
| No Retries  | `levelfour.WithNoRetries()`   | Retries enabled            |
| HTTP Client | `levelfour.WithHTTPClient(c)` | 30s timeout                |

## Recommendations

```go
savings, err := client.Recommendations.GetSavingsByProvider(ctx)
potential, err := client.Recommendations.GetPotentialSavings(ctx)
overview, err := client.Recommendations.GetOverview(ctx)
processing, err := client.Recommendations.ListInProgress(ctx)
detail, err := client.Recommendations.Get(ctx, "REC-1234")

page, err := client.Recommendations.List(ctx, &levelfour.ListRecommendationsRequest{
    Page:      levelfour.Int(1),
    PageSize:  levelfour.Int(50),
    SortBy:    levelfour.String("monthly_savings"),
    SortOrder: levelfour.ListRecommendationsRequestSortOrder("desc").Ptr(),
})

top, err := client.Recommendations.GetTop(ctx, "aws")

recs, err := client.Recommendations.ListByProvider(ctx, "aws",
    &levelfour.ListByProviderRecommendationsRequest{
        Page:          levelfour.Int(1),
        PageSize:      levelfour.Int(50),
        SortBy:        levelfour.String("monthly_savings"),
        SortOrder:     levelfour.ListByProviderRecommendationsRequestSortOrder("desc").Ptr(),
        Service:       []string{"EC2"},
        DisplayStatus: []string{"available", "pending"},
    },
)

providerOverview, err := client.Recommendations.GetProviderOverview(ctx, "aws")
providerFilters, err := client.Recommendations.GetProviderFilters(ctx, "aws")
providerPotential, err := client.Recommendations.GetProviderPotentialSavingsSummary(ctx, "aws")

providerPotentialPage, err := client.Recommendations.ListProviderPotentialSavings(ctx, "aws",
    &levelfour.ListProviderPotentialSavingsRecommendationsRequest{
        Page:     levelfour.Int(1),
        PageSize: levelfour.Int(50),
    },
)
```

Acting on one recommendation: narrow the resources it covers, record why it was rejected, read its activity, and ask an admin to release it.

```go
_, err := client.Recommendations.UpdateResourceSelection(ctx, "REC-1234",
    &levelfour.ResourceSelectionRequest{
        SelectedResources: []string{"vol-0abc1234def567890"},
    },
)

_, err = client.Recommendations.AddRejectionFeedback(ctx, "REC-1234",
    &levelfour.RejectionFeedbackRequest{
        Reason:      "not_applicable",
        Explanation: levelfour.String("This volume backs a disaster recovery test."),
    },
)

activity, err := client.Recommendations.GetRecommendationActivity(ctx, "REC-1234")

approval, err := client.Recommendations.RequestExecution(ctx, "REC-1234",
    &levelfour.RequestExecutionBody{
        ImplementationMethod: levelfour.String("one-click"),
    },
)

waiting, err := client.Recommendations.ListPendingApprovals(ctx)
```

[Recommendations](/resources/recommendations#acting-on-a-recommendation) has what each of these accepts and returns.

## Recommendations audit

Realized savings (audited completions). Account-wide and per-provider variants both live on `client.Recommendations.Audit`.

```go
summary, err := client.Recommendations.Audit.GetSummary(ctx)

page, err := client.Recommendations.Audit.List(ctx, &levelfour.ListAuditRequest{
    Page:      levelfour.Int(1),
    PageSize:  levelfour.Int(50),
    SortBy:    levelfour.String("monthly_savings"),
    SortOrder: levelfour.ListAuditRequestSortOrder("desc").Ptr(),
    Start:     levelfour.String("2025-01-01"),
    End:       levelfour.String("2025-03-31"),
})

providerSummary, err := client.Recommendations.Audit.GetProviderSummary(ctx, "aws")

providerPage, err := client.Recommendations.Audit.ListByProvider(ctx, "aws",
    &levelfour.ListByProviderAuditRequest{
        Page:     levelfour.Int(1),
        PageSize: levelfour.Int(50),
    },
)
```

## Realized savings detail

`client.Audit` returns realized-savings detail for one audited row. The id is the numeric `audit_id` a row in the audit list carries. The list and summary views live on `client.Recommendations.Audit` above.

```go
detail, err := client.Audit.GetRealizedAuditDetail(ctx, 4821)
```

## Costs

```go
summary, err := client.Costs.GetSummary(ctx, &levelfour.GetSummaryCostsRequest{})

breakdown, err := client.Costs.List(ctx, &levelfour.ListCostsRequest{
    Format:    levelfour.String("table"),
    Period:    levelfour.String("2025-03"),
    Page:      levelfour.Int(1),
    PageSize:  levelfour.Int(50),
    SortBy:    levelfour.String("cost"),
    SortOrder: levelfour.ListCostsRequestSortOrder("desc").Ptr(),
})

daily, err := client.Costs.GetDailyCosts(ctx, &levelfour.GetDailyCostsCostsRequest{
    Start: levelfour.String("2025-03-01T00:00:00.000Z"),
    End:   levelfour.String("2025-03-31T00:00:00.000Z"),
})

monthly, err := client.Costs.GetMonthlyCosts(ctx)

forecast, err := client.Costs.GetForecast(ctx, &levelfour.GetForecastAPIV1CostsForecastGetRequest{})

growing, err := client.Costs.GetTopGrowing(ctx, &levelfour.GetTopGrowingAPIV1CostsTopGrowingGetRequest{
    Limit: levelfour.Int(5),
})

providerSummary, err := client.Costs.GetProviderSummary(ctx, "aws", &levelfour.GetProviderSummaryCostsRequest{})

providerFilters, err := client.Costs.GetProviderFilters(ctx, "aws",
    &levelfour.GetProviderFiltersCostsRequest{},
)

providerList, err := client.Costs.ListByProvider(ctx, "aws",
    &levelfour.ListByProviderCostsRequest{
        Format:   levelfour.String("table"),
        Page:     levelfour.Int(1),
        PageSize: levelfour.Int(50),
    },
)

timeline, err := client.Costs.GetProviderTimeline(ctx, "aws",
    &levelfour.GetProviderTimelineCostsRequest{
        Start: levelfour.String("2025-01-01T00:00:00.000Z"),
        End:   levelfour.String("2025-03-31T00:00:00.000Z"),
    },
)
```

[Costs](/resources/costs) also covers usage and unit costs, cost by tag, allocation coverage and Google Cloud label grouping.

## Providers

```go
providers, err := client.Providers.List(ctx)

topSavers, err := client.Providers.GetProviderTopSavers(ctx, "aws")

invoices, err := client.Providers.GetProviderInvoices(ctx, "gcp",
    &levelfour.GetProviderInvoicesAPIV1ProvidersProviderIDCostsInvoicesGetRequest{},
)
```

`client.Providers` lists connected providers and a few provider-only reads. Most per-provider drill-downs live on the namespace that owns the resource.

| Per-provider data       | Namespace                      | Methods                                                                             |
| ----------------------- | ------------------------------ | ----------------------------------------------------------------------------------- |
| Cost                    | `client.Costs`                 | `GetProviderSummary`, `GetProviderFilters`, `ListByProvider`, `GetProviderTimeline` |
| Recommendations         | `client.Recommendations`       | `ListByProvider`, `GetProviderOverview`, `GetTop`                                   |
| Audit                   | `client.Recommendations.Audit` | `ListByProvider`, `GetProviderSummary`                                              |
| Invoices and top savers | `client.Providers`             | `GetProviderInvoices`, `GetProviderTopSavers`                                       |

## Commitments

Reserved Instances and Savings Plans. [Commitments](/resources/commitments) has what each read returns.

```go
overview, err := client.Commitments.GetOverview(ctx, &levelfour.GetOverviewAPIV1CommitmentsOverviewGetRequest{})

reservations, err := client.Commitments.GetInventory(ctx, &levelfour.GetInventoryAPIV1CommitmentsInventoryGetRequest{
    Instrument: "ri",
})

utilization, err := client.Commitments.GetUtilization(ctx, &levelfour.GetUtilizationAPIV1CommitmentsUtilizationGetRequest{
    Type:        levelfour.String("sp"),
    Granularity: levelfour.String("monthly"),
})

coverage, err := client.Commitments.GetCoverageRates(ctx, &levelfour.GetCoverageRatesAPIV1CommitmentsCoverageRatesGetRequest{})

esr, err := client.Commitments.GetEsr(ctx, &levelfour.GetEsrAPIV1CommitmentsEsrGetRequest{})

plan, err := client.Commitments.GetRenewalPlan(ctx, "arn:aws:savingsplans::123456789012:savingsplan/abcd1234")
```

## Google Cloud

Reads that only Google Cloud answers. [Google Cloud](/resources/google-cloud) has the full set.

```go
availability, err := client.GoogleCloud.GetDataAvailability(ctx)
budgets, err := client.GoogleCloud.GetBudgets(ctx)
utilization, err := client.GoogleCloud.GetUtilizationSummary(ctx)
access, err := client.GoogleCloud.GetAccess(ctx)
```

## Accounts

Connected cloud accounts, the modules your organization has, and connected work tools.

```go
accounts, err := client.Accounts.ListConnectedAccounts(ctx,
    &levelfour.ListConnectedAccountsAPIV1AccountsGetRequest{},
)

connections, err := client.Accounts.ListConnections(ctx)

modules, err := client.Accounts.ListCustomerModules(ctx)

installs, err := client.Accounts.ListGithubInstallations(ctx)
```

[Accounts](/resources/accounts) covers the rest.

## Anomalies

```go
summary, err := client.Anomalies.GetSummary(ctx, &levelfour.GetSummaryAPIV1AnomaliesSummaryGetRequest{})

anomalies, err := client.Anomalies.ListAnomalies(ctx, &levelfour.ListAnomaliesAPIV1AnomaliesGetRequest{
    Provider: levelfour.String("aws"),
    PageSize: levelfour.Int(20),
})

anomaly, err := client.Anomalies.GetAnomaly(ctx, "anomaly_id")
```

## Cost alerts

[Cost Alerts](/resources/cost-alerts) has every method, with the rule types and what each field takes. An `Enabled` of `false` pauses an alert, and `true` resumes it.

```go
alerts, err := client.CostAlerts.ListAlerts(ctx)

enabled := false
_, err = client.CostAlerts.UpdateAlert(ctx, alertID, &levelfour.UpdateAlertRequest{Enabled: &enabled})

_, err = client.CostAlerts.DeleteAlert(ctx, alertID)
```

## Cost views

A cost view is a saved set of filters and groupings you can read back and reuse.

```go
views, err := client.CostViews.ListViews(ctx)

view, err := client.CostViews.CreateView(ctx, &levelfour.CostViewBody{
    Name:      "Production compute",
    Providers: []string{"aws"},
    Filters:   map[string][]string{"service": {"Amazon Elastic Compute Cloud - Compute"}},
    GroupBy:   []string{"account_id"},
})
```

## Automated Savings grants

A grant is scoped, time-limited access that lets LevelFour apply a saving for you. Create one for a recommendation, or for a batch, then poll it until it reports connected.

```go
grant, err := client.SavingsGrants.CreateGrant(ctx, &levelfour.CreateGrantRequest{
    BatchParentID: "REC-1234",
})

status, err := client.SavingsGrants.GetGrantStatus(ctx, "grant_id")
```

[Automated Savings](/connectors/aws/automated-savings) covers the access the grant stands on.

## Repository binding

For a saving delivered as an infrastructure-as-code pull request, choose which repositories it opens against.

```go
installs, err := client.RepoBinding.ListInstallations(ctx)

targets, err := client.RepoBinding.BindRepos(ctx, "REC-1234", &levelfour.BindReposRequest{
    InstallationID: 12345678,
    Repos:          []string{"acme/infrastructure"},
})

bound, err := client.RepoBinding.ListTargets(ctx, "REC-1234")
```

## Integrations

Read a connected work tool and file a task in it. `tool` is the tool's name, such as `jira`.

```go
connection, err := client.Integrations.GetToolConnection(ctx, "jira")

task, err := client.Integrations.CreateToolTask(ctx, "jira", &levelfour.CreateTaskPayload{
    RecommendationID: "REC-1234",
    Title:            "Delete the unattached volume in us-east-1",
})
```

## Webhooks

```go
endpoints, err := client.Webhooks.List(ctx)

endpoint, err := client.Webhooks.Register(ctx, &levelfour.RegisterEndpointRequest{
    URL:        "https://example.com/webhook",
    EventTypes: []string{"recommendation.accepted", "optimization.completed"},
})

_, err = client.Webhooks.Delete(ctx, "ep_123")
```

## Auth

```go
me, err := client.Auth.GetWhoami(ctx)
```

## Pagination

Paginated methods return a `core.Page` with typed items and an iterator that fetches the next page for you.

```go
page, err := client.Recommendations.List(ctx, &levelfour.ListRecommendationsRequest{
    PageSize: levelfour.Int(50),
})
if err != nil {
    log.Fatal(err)
}

iter := page.Iterator()
for iter.Next(ctx) {
    rec := iter.Current()
    fmt.Printf("%s: $%.2f/mo\n", rec.Service, rec.MonthlySavings)
}
if err := iter.Err(); err != nil {
    log.Fatal(err)
}
```

[Pagination](/pagination) has the collect-all and manual-navigation forms, and the page size defaults.

## Error handling

Every error is a typed struct. Match one with `errors.As` and add a case per type you handle.

```go
import "errors"

detail, err := client.Recommendations.Get(ctx, "rec_nonexistent")
if err != nil {
    var notFoundErr *levelfour.NotFoundError

    switch {
    case errors.As(err, &notFoundErr):
        fmt.Printf("Not found: %v\n", notFoundErr.Body)
    default:
        fmt.Printf("Error: %v\n", err)
    }
}
```

[Error Handling](/error-handling) has the remaining types, and the failures the client retries before you ever see them.

## Webhook verification

Inside your HTTP handler, where `r` is the request and `body` its bytes:

```go
import "github.com/LevelFourAI/levelfour-go/levelfour/webhooks"

verifier, err := webhooks.NewVerifier("whsec_your_signing_secret")
if err != nil {
    log.Fatal(err)
}

payload, err := verifier.Verify(r.Header, body)
if err != nil {
    http.Error(w, "Invalid signature", http.StatusUnauthorized)
    return
}

fmt.Printf("Verified event: %v\n", payload["type"])
```

### Full HTTP handler

```go title="main.go"
package main

import (
    "fmt"
    "io"
    "log"
    "net/http"

    "github.com/LevelFourAI/levelfour-go/levelfour/webhooks"
)

func main() {
    verifier, err := webhooks.NewVerifier("whsec_your_signing_secret")
    if err != nil {
        log.Fatal(err)
    }

    http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
        body, err := io.ReadAll(r.Body)
        if err != nil {
            http.Error(w, "failed to read body", http.StatusBadRequest)
            return
        }

        payload, err := verifier.Verify(r.Header, body)
        if err != nil {
            http.Error(w, "invalid signature", http.StatusUnauthorized)
            return
        }

        fmt.Printf("Received event: %v\n", payload)
        w.WriteHeader(http.StatusOK)
    })

    log.Println("Listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}
```

See [Webhooks](/webhooks) for event types and payloads, and [Timestamp tolerance](/webhooks#timestamp-tolerance) for the replay window and how to widen it.

## Request options

The `option` package overrides client defaults for one request.

```go
import "github.com/LevelFourAI/levelfour-go/option"

summary, err := client.Recommendations.GetSavingsByProvider(ctx,
    option.WithMaxAttempts(5),
    option.WithHTTPHeader(http.Header{
        "X-Request-Id": []string{"abc123"},
    }),
)
```

| Option          | Function                        | Description             |
| --------------- | ------------------------------- | ----------------------- |
| Base URL        | `option.WithBaseURL(url)`       | Override base URL       |
| HTTP Client     | `option.WithHTTPClient(c)`      | Custom HTTP client      |
| Headers         | `option.WithHTTPHeader(h)`      | Extra headers           |
| Max Attempts    | `option.WithMaxAttempts(n)`     | Override retry attempts |
| Auth Token      | `option.WithToken(t)`           | Override Bearer token   |
| Query Params    | `option.WithQueryParameters(v)` | Extra query parameters  |
| Body Properties | `option.WithBodyProperties(m)`  | Extra body properties   |

## Next

* [Resources](/resources) has the parameters and response shapes behind these methods
* [Pagination](/pagination) has the page size defaults and iteration in the other two languages
* [Error Handling](/error-handling) has the full status-code table and what each error carries
* [Webhooks](/webhooks) has the event types, the payloads and the signature contract
* [Authentication](/authentication) covers key formats, scopes, rotation and the `whoami` check
* [SDK Overview](/sdks) compares the three clients option by option
