Skip to main content
SDK

Go SDK

Install and use the Qoder Cloud Agents Go SDK, including authentication, request types, streaming responses, pagination, retries, and timeouts.

The Qoder Cloud Agents Go SDK provides typed Forward and Managed clients. It supports request control through context, reading streaming responses event by event, and configuring authentication, retries, and HTTP transport.

Installation

Requires Go 1.23 or later. Install it in an existing Go Modules project:
go get github.com/QoderAI/qoder-cloud-agents-sdk-go
Keep go.mod and go.sum in version control to pin the SDK version used by your application.

Basic usage

Save your personal access token (PAT) in an environment variable:
export QODER_PAT="replace-with-your-PAT"
Use the Forward client to create a template and identity, start a session, and send a message:
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/QoderAI/qoder-cloud-agents-sdk-go/forward"
)

func main() {
	ctx := context.Background()
	client := forward.NewClient()

	env, err := client.Environments.New(ctx, forward.EnvironmentNewParams{
		Name:   "demo",
		Config: map[string]any{"type": "cloud"},
	})
	if err != nil {
		log.Fatal(err)
	}

	identity, err := client.Identities.New(ctx, forward.IdentityNewParams{
		ExternalID: "demo-user",
		Name:       forward.String("Demo User"),
	})
	if err != nil {
		log.Fatal(err)
	}

	template, err := client.Templates.New(ctx, forward.TemplateNewParams{
		Name:          "assistant",
		Model:         forward.ModelConfigUnionParam{OfString: forward.String("auto")},
		EnvironmentID: env.ID,
	})
	if err != nil {
		log.Fatal(err)
	}

	session, err := client.Sessions.New(ctx, forward.SessionNewParams{
		IdentityID: identity.ID,
		TemplateID: template.ID,
	})
	if err != nil {
		log.Fatal(err)
	}

	message := forward.SessionEventParam{
		Type: "user.message",
		Content: forward.EventContentUnionParam{
			OfBlocks: []forward.ContentBlockParam{
				{Type: "text", Text: forward.String("Hello")},
			},
		},
	}
	_, err = client.Sessions.Events.Send(ctx, session.ID, forward.SessionEventSendParams{
		Events: []forward.SessionEventParam{message},
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(session.ID)
}
Use the Managed client to create an Agent and environment, start a session, and send a message. Save the following code as main.go and run go run main.go in a project with the SDK installed:
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/QoderAI/qoder-cloud-agents-sdk-go/managed"
)

func main() {
	ctx := context.Background()
	client := managed.NewClient()

	agent, err := client.Agents.New(ctx, managed.AgentNewParams{
		Name:  "assistant",
		Model: managed.ManagedAgentsModelConfigParams{ID: "auto"},
	})
	if err != nil {
		log.Fatal(err)
	}

	env, err := client.Environments.New(ctx, managed.EnvironmentNewParams{
		Name: "demo",
		Config: managed.EnvironmentNewParamsConfigUnion{
			OfCloud: &managed.CloudConfigParams{Type: "cloud"},
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	session, err := client.Sessions.New(ctx, managed.SessionNewParams{
		Agent:         managed.SessionNewParamsAgentUnion{OfString: managed.String(agent.ID)},
		EnvironmentID: env.ID,
	})
	if err != nil {
		log.Fatal(err)
	}

	message := managed.ManagedAgentsEventParamsUnion{
		OfUserMessage: &managed.ManagedAgentsUserMessageEventParams{
			Type: "user.message",
			Content: []managed.ManagedAgentsUserMessageEventParamsContentUnion{
				{OfText: &managed.ManagedAgentsTextBlockParam{Type: "text", Text: "Hello"}},
			},
		},
	}
	_, err = client.Sessions.Events.Send(ctx, session.ID, managed.SessionEventSendParams{
		Events: []managed.ManagedAgentsEventParamsUnion{message},
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(session.ID)
}
Subsequent Go snippets omit package and import. In addition to the standard library, convention, option, and param refer to the module packages convention, convention/option, and convention/param, respectively. In the examples, client is a configured Forward client; replace resource IDs with real IDs accessible to the current credentials. See the Managed quickstart and Forward API reference for more integration instructions.

Client configuration

Pass configuration to constructors or individual calls through the option package. Per-call settings usually override client defaults for the same option; cumulative options such as appended headers and middleware are combined according to their own rules.
ConfigurationForwardManaged
Constructorforward.NewClientmanaged.NewClient
Base URL environment variableQODER_FORWARD_BASE_URLQODER_BASE_URL
Global default URLhttps://api.qoder.com/api/v1/forwardhttps://api.qoder.com/api/v1/cloud
CN URLhttps://api.qoder.com.cn/api/v1/forwardhttps://api.qoder.com.cn/api/v1/cloud
Explicit configuration takes precedence over environment variables. Default PAT mode reads QODER_ACCESS_TOKEN first, then QODER_PAT. Clients do not automatically load .env files. The Base URL must include the full API prefix and match the region of the account, credentials, and resources. Complete configuration before reusing a client across goroutines; do not modify its Options concurrently after sharing it.

Authentication

PAT

Use option.WithPAT(token) to explicitly configure a personal access token. Store the token in your application server environment.

Service Account

After setting QODER_SA_KEY, explicitly create a credential component. It exchanges and caches SATs (service account tokens), refreshing the token when subsequent requests require it:
func serviceAccountClients() (forward.Client, managed.Client, error) {
	credential, err := convention.ServiceAccountCredentialFromEnv(
		convention.ServiceAccountCredentialConfig{
			Region: "global",
			Scopes: []string{"qca.access", "forward.access"},
		},
	)
	if err != nil {
		return forward.Client{}, managed.Client{}, err
	}
	return forward.NewClient(
		option.WithCredential(credential),
		option.WithBaseURL("https://api.qoder.com/api/v1/forward"),
	), managed.NewClient(
		option.WithCredential(credential),
		option.WithBaseURL("https://api.qoder.com/api/v1/cloud"),
	), nil
}
Both clients share the same credential instance and token cache. For the China site, use both Region: "cn" and a CN Base URL. For Managed-only access, you can request the qca.access scope; permissions and Managed workspace bindings must be configured on the server. Ordinary clients do not automatically enable credential exchange merely because QODER_SA_KEY exists in the environment. An SA Key is only used to obtain SATs and cannot serve directly as a business API access token. SA authentication failures do not fall back to PAT. If the application manages SATs itself, use option.WithAuthMode("sa") and option.WithAuthToken(token). The application must refresh static SATs. Choose one explicit credential source per client; avoid combining native SA credentials with static tokens. See the authentication documentation.

Request and response types

Optional fields

Required fields generally use Go values directly; optional scalars use param.Opt[T]. An unset field differs from an explicitly sent zero value:
ExpressionJSON behavior
Unset param.Opt[T]Omit the field
forward.String("")Send an empty string
forward.Bool(false)Send false
forward.Int(0)Send 0
param.Null[string]()Send null
Nil map or slice with omitzeroOmit the field
Non-nil empty map or sliceSend {} or []
The following function demonstrates parameter encoding only; it does not make a request:
func encodeParameters() ([]byte, error) {
	params := forward.SessionUpdateParams{
		Title: param.Null[string](),
	}
	return json.Marshal(params)
}
Whether a field accepts null and how the server handles it are defined by the corresponding API. Managed provides equivalent helpers such as managed.String, Bool, and Int.

Union types and response fields

For request union types, select one representation through an Of... field and set only one branch. For response union types, use the provided As... or AsAny() methods to read a specific branch; see the API Reference for exact methods. The JSON metadata in response structures distinguishes missing fields, explicit null, and normal values. Do not use Go zero values alone to determine whether a field exists. Use the response model RawJSON() for raw JSON. Unmodeled fields are retained in the response extra-field information.

Streaming responses

Use StreamEvents to establish a single SSE connection, read events in order, and check Err() afterward:
func readEvents(ctx context.Context, client forward.Client, sessionID string) error {
	stream := client.Sessions.Events.StreamEvents(
		ctx, sessionID, forward.SessionEventStreamParams{},
	)
	defer stream.Close()

	for stream.Next() {
		event := stream.Current()
		fmt.Println(event.Type)
	}
	return stream.Err()
}
A single stream ends at EOF. To keep subscribing after a recoverable disconnection, explicitly use NewResumableStream:
func readResumableEvents(
	ctx context.Context,
	client forward.Client,
	sessionID string,
	cursor string,
) error {
	params := forward.SessionEventStreamParams{}
	if cursor != "" {
		params.LastEventID = forward.String(cursor)
	}
	stream := client.Sessions.Events.NewResumableStream(ctx, sessionID, params)
	defer stream.Close()

	for stream.Next() {
		event := stream.Current()
		fmt.Println(event.Type)
		// Persist this resume position after the application successfully processes the event.
		fmt.Println("cursor:", stream.LastEventID())
	}
	return stream.Err()
}
A resumable stream reconnects with the SSE cursor of the most recently delivered event, without resending business requests. It may run continuously; use context to set a subscription deadline or cancel explicitly. HTTP retry counts apply to individual connection requests, not the reconnection limit of the entire subscription. Without a cursor, it starts at the current event stream tail. To read history, first obtain a valid historical resume position. Automatic reconnection does not guarantee exactly-once processing; multiple deltas for one event may share an ID, so do not discard all repeated frames solely by ID. See the corresponding API definitions for event meanings and task completion conditions. Closing the stream or cancelling the local context stops only the current subscription; it does not automatically stop execution already started in the cloud.

Error handling and Request ID

HTTP errors use *convention.Error. Handle network, encoding, decoding, and context errors separately:
func retrieveWithErrors(ctx context.Context, client forward.Client, sessionID string) error {
	_, err := client.Sessions.Get(ctx, sessionID)
	if err == nil {
		return nil
	}
	var apiErr *convention.Error
	switch {
	case errors.Is(err, context.Canceled):
		fmt.Println("Request canceled")
	case errors.Is(err, context.DeadlineExceeded):
		fmt.Println("Request deadline exceeded")
	case errors.As(err, &apiErr):
		fmt.Printf("status=%d code=%s request_id=%s\n",
			apiErr.StatusCode, apiErr.Code, apiErr.RequestID)
	default:
		fmt.Printf("Request failed: %v\n", err)
	}
	return err
}
Errors also provide Message, Type(), Request, Response, and RawJSON(). Keep status codes, error codes, and Request IDs for troubleshooting; avoid logging complete requests containing access tokens.

Retries and idempotency

By default, requests are retried up to 2 times, for a maximum of 3 attempts. Retries require a replayable request body and a caller context that is still valid.
RequestRetried by default for
GET, HEAD, or requests with an idempotency keyConnection errors, 408, 429, 5xx
Other requests without an idempotency key429
409 responseNo automatic retries
Within these safety rules, the SDK also handles server retry hints and delays; some APIs impose stricter retry limits. Use option.WithMaxRetries(0) to disable retries, or override the count for an individual call. The application generates and stores idempotency keys. Pass one through the request parameter IdempotencyKey or option.WithHeader("Idempotency-Key", key). Reuse the same key for all retries of one logical operation; use a new key for a new operation.

Timeouts and cancellation

The default Transport timeout for waiting for response headers is 10 minutes. This limit does not cover response body reads and is not a total deadline for the entire call. Use context to control the entire call and WithRequestTimeout to control each attempt:
func retrieveWithTimeout(parent context.Context, client forward.Client, sessionID string) error {
	ctx, cancel := context.WithTimeout(parent, time.Minute)
	defer cancel()
	_, err := client.Sessions.Get(ctx, sessionID,
		option.WithRequestTimeout(15*time.Second),
		option.WithMaxRetries(1),
	)
	return err
}
The example waits at most 1 minute overall and 15 seconds per attempt; whichever deadline arrives first takes effect. WithRequestTimeout also covers response body reads, so configure a suitable subscription deadline for long-lived SSE.

Pagination

Automatic pagination fetches subsequent pages on demand:
func listAll(ctx context.Context, client forward.Client) error {
	pager := client.Sessions.ListAutoPaging(ctx, forward.SessionListParams{
		Limit: forward.Int(20),
	})
	for pager.Next() {
		fmt.Println(pager.Current().ID)
	}
	return pager.Err()
}
For page-by-page processing, use List and GetNextPage(). When there is no next page, it returns nil:
func listPages(ctx context.Context, client forward.Client) error {
	page, err := client.Sessions.List(ctx, forward.SessionListParams{Limit: forward.Int(20)})
	for err == nil && page != nil {
		fmt.Println(len(page.Data))
		page, err = page.GetNextPage()
	}
	return err
}
Paginators retain query conditions and request configuration. Cursor formats can differ between APIs; prefer the pagination methods provided by the returned page.

Advanced configuration

Raw responses

Use WithResponseInto to read the status code and response headers:
func inspectResponse(ctx context.Context, client forward.Client, sessionID string) error {
	var response *http.Response
	_, err := client.Sessions.Get(ctx, sessionID, option.WithResponseInto(&response))
	if response != nil {
		fmt.Println(response.StatusCode, response.Header.Get("X-Request-ID"))
	}
	return err
}
The response body may already have been read by the SDK. Use RawJSON() on the parsed result for response JSON, or WithResponseBodyInto to specify your own decoding destination.

HTTP clients and request extensions

Use WithHTTPClient to configure connection pools, proxies, and a custom Transport. When you supply a custom client, the application is responsible for its timeouts and connection pool configuration.
func customHTTPClient(token string) forward.Client {
	transport := http.DefaultTransport.(*http.Transport).Clone()
	transport.ResponseHeaderTimeout = time.Minute
	transport.MaxIdleConnsPerHost = 20
	return forward.NewClient(
		option.WithPAT(token),
		option.WithHTTPClient(&http.Client{Transport: transport}),
	)
}
Use WithMiddleware to observe each request attempt. Use WithHeader, WithQuery, and WithJSONSet for custom headers, query parameters, and JSON fields. The parameter structure SetExtraFields accepts fields already supported by the server but not yet modeled by the SDK. Override only fields your application explicitly needs to adjust, avoiding accidental replacement of authentication or routing configuration.