Skip to main content
Libraries/API Clients

Go API Client

Glean's Go API client provides idiomatic Go interfaces for integrating enterprise search and AI capabilities into your Go applications.

api-client-go

Official Go client for Glean's Client API

Installation

go get github.com/gleanwork/api-client-go

Quick Start

package main

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

apiclientgo "github.com/gleanwork/api-client-go"
"github.com/gleanwork/api-client-go/models/components"
)

func main() {
ctx := context.Background()

s := apiclientgo.New(
apiclientgo.WithSecurity(os.Getenv("GLEAN_API_TOKEN")),
apiclientgo.WithInstance(os.Getenv("GLEAN_INSTANCE")),
)

res, err := s.Client.Chat.Create(ctx, components.ChatRequest{
Messages: []components.ChatMessage{
{
Fragments: []components.ChatMessageFragment{
{Text: apiclientgo.Pointer("What are our company values?")},
},
},
},
}, nil, nil)
if err != nil {
log.Fatal(err)
}

if res.ChatResponse != nil {
for _, msg := range res.ChatResponse.Messages {
for _, frag := range msg.Fragments {
if frag.Text != nil {
fmt.Print(*frag.Text)
}
}
}
fmt.Println()
}
}

The unified SDK exposes both the Client and Indexing APIs from a single *apiclientgo.Glean value. Client-facing operations (search, chat, retrieval) live under s.Client; indexing operations live under s.Indexing. Configure the backend with either apiclientgo.WithInstance("instance-name") or apiclientgo.WithServerURL("https://instance-name-be.glean.com").

Core Features

Chat API

Chat responses are returned as a list of messages, each made up of one or more fragments. Concatenate the text fragments to assemble the assistant's reply. The chatAnswer helper below is reused by later examples on this page.

package main

import (
"context"
"fmt"
"strings"

apiclientgo "github.com/gleanwork/api-client-go"
"github.com/gleanwork/api-client-go/models/components"
)

// chatAnswer concatenates the text fragments of a chat response.
func chatAnswer(resp *components.ChatResponse) string {
var sb strings.Builder
for _, msg := range resp.Messages {
for _, frag := range msg.Fragments {
if frag.Text != nil {
sb.WriteString(*frag.Text)
}
}
}
return sb.String()
}

func chatExample(s *apiclientgo.Glean) error {
ctx := context.Background()

res, err := s.Client.Chat.Create(ctx, components.ChatRequest{
Messages: []components.ChatMessage{
{
Fragments: []components.ChatMessageFragment{
{Text: apiclientgo.Pointer("Explain our Q4 strategy")},
},
},
},
}, nil, nil)
if err != nil {
return err
}

if res.ChatResponse != nil {
fmt.Println(chatAnswer(res.ChatResponse))
}
return nil
}

To stream a response instead of waiting for the full answer, use s.Client.Chat.CreateStream, which returns an event stream on res.ChatRequestStream.

Search API

package main

import (
"context"
"fmt"

apiclientgo "github.com/gleanwork/api-client-go"
"github.com/gleanwork/api-client-go/models/components"
)

func searchExample(s *apiclientgo.Glean) error {
ctx := context.Background()

res, err := s.Client.Search.Query(ctx, components.SearchRequest{
Query: "quarterly business review",
PageSize: apiclientgo.Pointer[int64](10),
}, nil)
if err != nil {
return err
}

if res.SearchResponse == nil {
return nil
}

for _, result := range res.SearchResponse.Results {
title := ""
if result.Title != nil {
title = *result.Title
}
fmt.Printf("Title: %s\n", title)
fmt.Printf("URL: %s\n", result.URL)
}

return nil
}

SearchResult.Title is an optional *string, so guard against nil before dereferencing it. SearchResult.URL is a required string.

Framework Integrations

Gin Web Framework

package main

import (
"net/http"

"github.com/gin-gonic/gin"
apiclientgo "github.com/gleanwork/api-client-go"
"github.com/gleanwork/api-client-go/models/components"
)

type ChatRequest struct {
Message string `json:"message"`
}

func setupRoutes(s *apiclientgo.Glean) *gin.Engine {
r := gin.Default()

r.POST("/chat", func(c *gin.Context) {
var req ChatRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

res, err := s.Client.Chat.Create(c.Request.Context(), components.ChatRequest{
Messages: []components.ChatMessage{
{
Fragments: []components.ChatMessageFragment{
{Text: apiclientgo.Pointer(req.Message)},
},
},
},
}, nil, nil)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}

answer := ""
if res.ChatResponse != nil {
answer = chatAnswer(res.ChatResponse) // helper defined in the Chat API section
}
c.JSON(http.StatusOK, gin.H{"response": answer})
})

return r
}

Echo Framework

import (
"net/http"

"github.com/labstack/echo/v4"
apiclientgo "github.com/gleanwork/api-client-go"
"github.com/gleanwork/api-client-go/models/components"
)

func chatHandler(s *apiclientgo.Glean) echo.HandlerFunc {
return func(c echo.Context) error {
var req ChatRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}

res, err := s.Client.Chat.Create(c.Request().Context(), components.ChatRequest{
Messages: []components.ChatMessage{
{
Fragments: []components.ChatMessageFragment{
{Text: apiclientgo.Pointer(req.Message)},
},
},
},
}, nil, nil)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}

answer := ""
if res.ChatResponse != nil {
answer = chatAnswer(res.ChatResponse) // helper defined in the Chat API section
}
return c.JSON(http.StatusOK, map[string]string{
"response": answer,
})
}
}

Concurrency Patterns

Batch Processing with Goroutines

func batchSearch(s *apiclientgo.Glean, queries []string) ([]*components.SearchResponse, error) {
ctx := context.Background()
results := make([]*components.SearchResponse, len(queries))
errs := make([]error, len(queries))

var wg sync.WaitGroup

for i, query := range queries {
wg.Add(1)
go func(index int, q string) {
defer wg.Done()

res, err := s.Client.Search.Query(ctx, components.SearchRequest{
Query: q,
}, nil)
if err != nil {
errs[index] = err
return
}

results[index] = res.SearchResponse
}(i, query)
}

wg.Wait()

// Check for errors
for _, err := range errs {
if err != nil {
return nil, err
}
}

return results, nil
}

Authentication

User-Scoped Tokens

The API token is supplied through apiclientgo.WithSecurity:

s := apiclientgo.New(
apiclientgo.WithSecurity("your-user-token"),
apiclientgo.WithServerURL("https://instance-name-be.glean.com"),
)

Global Tokens with ActAs

Per-request headers are passed with operations.WithSetHeaders, supplied as trailing options after the request's optional locale and timezoneOffset parameters:

import (
apiclientgo "github.com/gleanwork/api-client-go"
"github.com/gleanwork/api-client-go/models/components"
"github.com/gleanwork/api-client-go/models/operations"
)

res, err := s.Client.Chat.Create(ctx, components.ChatRequest{
Messages: []components.ChatMessage{
{
Fragments: []components.ChatMessageFragment{
{Text: apiclientgo.Pointer("Hello")},
},
},
},
}, nil, nil, operations.WithSetHeaders(map[string]string{
"X-Glean-ActAs": "user@company.com",
}))

OAuth Access Tokens

An OAuth access token is a bearer credential, so it goes in the same WithSecurity option:

s := apiclientgo.New(
apiclientgo.WithSecurity(oauthAccessToken),
apiclientgo.WithServerURL("https://instance-name-be.glean.com"),
)

Tokens issued by the Glean OAuth Authorization Server (including tokens obtained via Dynamic Client Registration) are detected automatically. Tokens issued by an external identity provider (Google, Okta, Azure, etc.) additionally require the X-Glean-Auth-Type: OAUTH header on each request:

import (
apiclientgo "github.com/gleanwork/api-client-go"
"github.com/gleanwork/api-client-go/models/components"
"github.com/gleanwork/api-client-go/models/operations"
)

res, err := s.Client.Search.Query(ctx, components.SearchRequest{
Query: "quarterly reports",
}, nil, operations.WithSetHeaders(map[string]string{
"X-Glean-Auth-Type": "OAUTH",
}))

See the OAuth authentication guide for identity-provider setup.

Complete Example: Authorization Code with PKCE

This example uses golang.org/x/oauth2. It runs the Authorization Code flow with PKCE (S256ChallengeOption / VerifierOption) and AccessTypeOffline for a refresh token, then passes the access token to the Glean client. Read AuthURL/TokenURL from the issuer's metadata document.

package main

import (
"encoding/json"
"net/http"
"os"

"golang.org/x/oauth2"
apiclientgo "github.com/gleanwork/api-client-go"
"github.com/gleanwork/api-client-go/models/components"
"github.com/gleanwork/api-client-go/models/operations"
)

var (
conf = &oauth2.Config{
ClientID: os.Getenv("OAUTH_CLIENT_ID"),
ClientSecret: os.Getenv("OAUTH_CLIENT_SECRET"), // omit for a public client
RedirectURL: "http://localhost:8080/callback",
Scopes: []string{"openid", "offline_access", "SEARCH"}, // SEARCH lets the token call /search; offline_access → refresh token
Endpoint: oauth2.Endpoint{
AuthURL: os.Getenv("OAUTH_AUTH_URL"),
TokenURL: os.Getenv("OAUTH_TOKEN_URL"),
},
}
// Demo only: generate and store the verifier per request (e.g. in session) in production.
verifier = oauth2.GenerateVerifier()
)

func main() {
http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline,
oauth2.S256ChallengeOption(verifier))
http.Redirect(w, r, url, http.StatusFound)
})

http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
tok, err := conf.Exchange(ctx, r.URL.Query().Get("code"), oauth2.VerifierOption(verifier))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

s := apiclientgo.New(
apiclientgo.WithSecurity(tok.AccessToken),
apiclientgo.WithServerURL(os.Getenv("GLEAN_SERVER_URL")),
)

res, err := s.Client.Search.Query(ctx, components.SearchRequest{
Query: "quarterly reports",
}, nil, // Omit this option when the token is from the Glean Authorization Server.
operations.WithSetHeaders(map[string]string{"X-Glean-Auth-Type": "OAUTH"}))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(res.SearchResponse)
})

http.ListenAndServe(":8080", nil)
}
tip

Access tokens expire. Wrap the token in conf.TokenSource(ctx, tok) to refresh automatically when you requested offline_access.

Error Handling

Every operation returns either a response or an error, never both. API errors are returned as apierrors.APIError, which carries the HTTP status code and response body. Some operations return more specific typed errors (for example, Search.Query may return apierrors.GleanDataError for 403/422); use errors.As to detect them.

package main

import (
"context"
"errors"
"fmt"
"time"

apiclientgo "github.com/gleanwork/api-client-go"
"github.com/gleanwork/api-client-go/models/apierrors"
"github.com/gleanwork/api-client-go/models/components"
)

func safeChat(s *apiclientgo.Glean, message string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

res, err := s.Client.Chat.Create(ctx, components.ChatRequest{
Messages: []components.ChatMessage{
{
Fragments: []components.ChatMessageFragment{
{Text: apiclientgo.Pointer(message)},
},
},
},
}, nil, nil)
if err != nil {
var apiErr *apierrors.APIError
if errors.As(err, &apiErr) {
return "", fmt.Errorf("chat failed with status %d: %w", apiErr.StatusCode, apiErr)
}
return "", fmt.Errorf("chat error: %w", err)
}

if res.ChatResponse == nil {
return "", nil
}
return chatAnswer(res.ChatResponse), nil // helper defined in the Chat API section
}

Testing

The SDK talks to a standard net/http client, so the simplest way to test code that calls Glean is to point the client at an httptest server via WithServerURL and return canned responses:

package main

import (
"context"
"net/http"
"net/http/httptest"
"testing"

apiclientgo "github.com/gleanwork/api-client-go"
"github.com/gleanwork/api-client-go/models/components"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestSearch(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"results":[{"title":"Q4 Report","url":"https://example.com/q4"}]}`))
}))
defer srv.Close()

s := apiclientgo.New(
apiclientgo.WithSecurity("test-token"),
apiclientgo.WithServerURL(srv.URL),
)

res, err := s.Client.Search.Query(context.Background(), components.SearchRequest{
Query: "quarterly business review",
}, nil)
require.NoError(t, err)
require.NotNil(t, res.SearchResponse)
require.Len(t, res.SearchResponse.Results, 1)
assert.Equal(t, "https://example.com/q4", res.SearchResponse.Results[0].URL)
}

For lower-level control (custom timeouts, transports, or request assertions) you can also supply your own *http.Client with apiclientgo.WithClient.

Additional Resources