Go SDK


Official Go SDK for the GrowPanel REST API. Distributed through Go modules — no registry, just go get. Source: growpanel/growpanel-sdk-go.


Installation

go get github.com/growpanel/growpanel-sdk-go

Requires Go 1.22+. The generated client uses only the standard library plus a thin runtime helper from github.com/oapi-codegen/runtime.


Authentication

Get an API key from app.growpanel.io → Account → API keys. See Authentication for key scoping and rotation.

import (
"context"
"os"
growpanel "github.com/growpanel/growpanel-sdk-go"
)

gp, err := growpanel.New(os.Getenv("GROWPANEL_API_KEY"))
if err != nil {
log.Fatal(err)
}

Override the base URL or HTTP client:

gp, err := growpanel.New(
os.Getenv("GROWPANEL_API_KEY"),
growpanel.WithBaseURL("https://api-dev.growpanel.io"),
growpanel.WithHTTPClient(&http.Client{Timeout: 30 * time.Second}),
)

Surfaces

v0.1 ships a thin wrapper around the generated low-level client. Every endpoint in the OpenAPI spec is reachable as a typed method. Two flavours are exposed:

FieldUse when
gp.WithResponses.*You want typed *<Op>Response with parsed JSON200/JSON400 fields. Recommended.
gp.Client.*You want the raw (*http.Response, error) return — closest to the wire.

Both expose one method per endpoint, named after the operation: GetReportsMrr, PostDataCustomers, PutDataCustomersId, DeleteIntegrationsWebhooksId, etc. The WithResponses variant appends WithResponse to each: GetReportsMrrWithResponse, etc.

Future versions will add ergonomic group fields (gp.Reports.GetMrr(...), gp.Data.Customers.Create(...)) so callers don't need to spell out the full operation ID. The flat methods will keep working.


Quick start

package main

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

growpanel "github.com/growpanel/growpanel-sdk-go"
gen "github.com/growpanel/growpanel-sdk-go/generated"
)

func ptr[T any](v T) *T { return &v }

func main() {
gp, err := growpanel.New(os.Getenv("GROWPANEL_API_KEY"))
if err != nil { log.Fatal(err) }
ctx := context.Background()

// Top-level KPIs
summary, err := gp.WithResponses.GetReportsSummaryWithResponse(ctx, nil)
if err != nil { log.Fatal(err) }
fmt.Println("MRR (øre):", *summary.JSON200.Summary.MrrCurrent)

// MRR time series with typed query params
mrr, err := gp.WithResponses.GetReportsMrrWithResponse(ctx, &gen.GetReportsMrrParams{
Date: ptr("20260101-20260531"),
Interval: ptr("month"),
})
if err != nil { log.Fatal(err) }
for _, period := range mrr.JSON200.Result {
fmt.Println(period.Date, period.TotalMrr)
}
}

The generated types use pointers for optional fields, so a tiny helper makes literals cleaner:

func ptr[T any](v T) *T { return &v }

Usage by area

Analytics — reports:

gp.WithResponses.GetReportsSummaryWithResponse(ctx, nil)
gp.WithResponses.GetReportsMrrWithResponse(ctx, &gen.GetReportsMrrParams{...})
gp.WithResponses.GetReportsMrrGrowthWithResponse(ctx, &gen.GetReportsMrrGrowthParams{...})
gp.WithResponses.GetReportsCohortWithResponse(ctx, &gen.GetReportsCohortParams{...})
gp.WithResponses.GetReportsRetentionWithResponse(ctx, &gen.GetReportsRetentionParams{...})
gp.WithResponses.GetReportsCashflowFailedPaymentsWithResponse(ctx, &gen.GetReportsCashflowFailedPaymentsParams{...})
// ... 22 report endpoints in total

Customers (analytics view):

gp.WithResponses.GetCustomersWithResponse(ctx, &gen.GetCustomersParams{Limit: ptr("50")})
gp.WithResponses.GetCustomersIdWithResponse(ctx, "cus_xxx", nil)

Plans (analytics view):

gp.WithResponses.GetPlansWithResponse(ctx)

Account & integrations:

gp.WithResponses.GetProfileWithResponse(ctx)
gp.WithResponses.PutProfileWithResponse(ctx, gen.PutProfileJSONBody{FirstName: ptr("Lasse")})

gp.WithResponses.GetSettingsNotificationsWithResponse(ctx)
gp.WithResponses.GetIntegrationsWebhooksWithResponse(ctx)
gp.WithResponses.PostIntegrationsWebhooksWithResponse(ctx, gen.PostIntegrationsWebhooksJSONBody{
Url: "https://hooks.zapier.com/...",
EventType: "movement.churn",
})
gp.WithResponses.DeleteIntegrationsWebhooksIdWithResponse(ctx, "wh_xxx")

Data ingestion (every /data/* endpoint):

gp.WithResponses.GetDataCustomersWithResponse(ctx, &gen.GetDataCustomersParams{Source: "ds_xxx"})
gp.WithResponses.PostDataCustomersWithResponse(ctx, gen.PostDataCustomersJSONBody{...})
gp.WithResponses.PutDataCustomersIdWithResponse(ctx, "cus_xxx", gen.PutDataCustomersIdJSONBody{...})
gp.WithResponses.DeleteDataCustomersIdWithResponse(ctx, "cus_xxx")

gp.WithResponses.GetDataPlansWithResponse(ctx, &gen.GetDataPlansParams{Source: ptr("ds_xxx")})
gp.WithResponses.PostDataPlansWithResponse(ctx, gen.PostDataPlansJSONBody{...})

gp.WithResponses.GetDataInvoicesWithResponse(ctx, &gen.GetDataInvoicesParams{Source: "ds_xxx"})
gp.WithResponses.PostDataInvoicesWithResponse(ctx, gen.PostDataInvoicesJSONBody{...})

gp.WithResponses.GetDataPlanGroupsWithResponse(ctx, &gen.GetDataPlanGroupsParams{})
gp.WithResponses.PostDataPlanGroupsWithResponse(ctx, gen.PostDataPlanGroupsJSONBody{
Name: "Pro tier",
Plans: []string{"price_a", "price_b"},
})

gp.WithResponses.GetDataSegmentsWithResponse(ctx)
gp.WithResponses.GetDataDataSourcesWithResponse(ctx, &gen.GetDataDataSourcesParams{})
gp.WithResponses.PostDataDataSourcesIdFullImportWithResponse(ctx, "ds_xxx")
gp.WithResponses.GetDataDataSourcesIdProgressWithResponse(ctx, "ds_xxx")

Errors

The generated client returns (*Response, error). Network/decoding errors land in err; API-level errors (4xx/5xx) come back on the response with status accessible via resp.StatusCode().

resp, err := gp.WithResponses.GetCustomersIdWithResponse(ctx, "cus_doesnotexist", nil)
if err != nil {
return fmt.Errorf("network: %w", err)
}
switch resp.StatusCode() {
case 200:
// resp.JSON200 has the typed payload
case 404:
// customer not found
case 429:
// rate-limited
}

See error codes for what each status means.


Updating

go get -u github.com/growpanel/growpanel-sdk-go

The SDK is regenerated on every API surface change. Changelog on GitHub Releases.