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-goRequires 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:
| Field | Use 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 totalCustomers (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-goThe SDK is regenerated on every API surface change. Changelog on GitHub Releases.