Get started
Use Go's net/http to fetch a list and a single entry from genko.me and decode them into structs.
This guide connects Go to the genko.me blog API and fetches a list and a single entry using only the standard library.
Prerequisites
- A genko.me account and workspace. You need the Owner role to create an API and issue an API key.
- Go 1.21 or later. The code uses generics (available since 1.18) and
http.NewRequestWithContext. - One list API and one published entry. If needed, create them in steps 2 and 3 of the quickstart.
Replace your-workspace with your workspace ID and blog with your endpoint throughout this guide.
Install
Nothing to install. You do not need go get. Add the following code to your project.
Configure an API key and workspace ID
If your dashboard URL is https://genko.me/w/your-workspace, your workspace ID is your-workspace.
Create an API key in Settings and plan → API keys (/w/your-workspace/settings/api-keys). Scope it to blog under Selected APIs only to limit exposure if the key leaks. You cannot view its value again after creation.
Read the key from an environment variable and check it at startup.
apiKey := os.Getenv("GENKO_API_KEY")if apiKey == "" { log.Fatal("GENKO_API_KEY is not set.")}os.Getenv returns an empty string when unset. Without this check, you would only notice after a 401. Do not put the key in source code.
Write a client
Put delivery API GET requests in one place.
package genko
import ( "context" "encoding/json" "fmt" "net/http" "net/url")
const baseURL = "https://api.genko.me"
// Client holds a connection to the delivery API for one workspace.type Client struct { Workspace string APIKey string HTTP *http.Client}
// APIError represents an error response from the delivery API.type APIError struct { Status int Message string `json:"message"`}
func (e *APIError) Error() string { return fmt.Sprintf("genko: delivery API returned %d: %s", e.Status, e.Message)}
// List is the response from a list API.type List[T any] struct { Contents []T `json:"contents"` TotalCount int `json:"totalCount"` Offset int `json:"offset"` Limit int `json:"limit"`}
func (c *Client) get(ctx context.Context, path string, query url.Values, dest any) error { endpoint := baseURL + "/v1/" + c.Workspace + path if len(query) > 0 { endpoint += "?" + query.Encode() }
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { return err } req.Header.Set("X-API-KEY", c.APIKey)
res, err := c.HTTP.Do(req) if err != nil { return err } defer res.Body.Close()
if res.StatusCode >= 400 { apiErr := &APIError{Status: res.StatusCode} // Return the status even if message cannot be decoded. _ = json.NewDecoder(res.Body).Decode(apiErr) return apiErr }
return json.NewDecoder(res.Body).Decode(dest)}
// GetList fetches a list. Go methods cannot have type parameters, so this is a function.func GetList[T any](ctx context.Context, c *Client, endpoint string, query url.Values) (*List[T], error) { var list List[T] if err := c.get(ctx, "/"+endpoint, query, &list); err != nil { return nil, err } return &list, nil}
// GetEntry fetches one entry from a list API.func GetEntry[T any](ctx context.Context, c *Client, endpoint, id string, query url.Values) (*T, error) { var entry T if err := c.get(ctx, "/"+endpoint+"/"+url.PathEscape(id), query, &entry); err != nil { return nil, err } return &entry, nil}url.Values.Encode() URL-encodes brackets in filters and non-ASCII values. Do not concatenate a query string yourself.
Define structs for your schema
Use dashboard field IDs as json tags.
package genko
import "time"
// Post holds the blog API fields. Use dashboard field IDs as json tags.type Post struct { ID string `json:"id"` Title string `json:"title"` // Rich text is an HTML string. Body string `json:"body"` Category string `json:"category"` Tags []string `json:"tags"` // The delivery value for an image has only url and alt; no dimensions. Thumbnail *Image `json:"thumbnail"` // If fields excludes it, this is empty. Check for the zero value. PublishedAt time.Time `json:"publishedAt"` UpdatedAt time.Time `json:"updatedAt"`}
type Image struct { URL string `json:"url"` Alt string `json:"alt"`}Dates are ISO 8601 strings, so they decode directly into time.Time. They arrive in UTC; convert to time.Local or Asia/Tokyo before display.
Optional fields and fields added later may be absent from older entries. Missing values become zero values. The same applies when you narrow the response with fields. Use a pointer such as *Image for a single reference so you can handle nil.
Show a list
package main
import ( "context" "fmt" "log" "net/http" "net/url" "os" "time"
"example.com/yourapp/genko")
func main() { apiKey := os.Getenv("GENKO_API_KEY") if apiKey == "" { log.Fatal("GENKO_API_KEY is not set.") }
client := &genko.Client{ Workspace: "your-workspace", APIKey: apiKey, HTTP: &http.Client{Timeout: 10 * time.Second}, }
list, err := genko.GetList[genko.Post](context.Background(), client, "blog", url.Values{ "limit": {"10"}, "orders": {"-publishedAt"}, "fields": {"title,publishedAt"}, }) if err != nil { log.Fatal(err) }
fmt.Printf("%d entries total\n", list.TotalCount) for _, post := range list.Contents { fmt.Printf("%s %s %s\n", post.ID, post.Title, post.PublishedAt.Format(time.DateOnly)) }}The response has Contents, TotalCount, Offset, and Limit. When no entries match, Contents is an empty slice rather than an error.
html/template escapes text fields automatically. To render rich text (Body) as HTML, convert it to template.HTML(post.Body). This disables escaping. It displays content entered in the dashboard, so give editing access only to trusted authors.
Show a single entry
Append an ID from the list to the path.
post, err := genko.GetEntry[genko.Post](ctx, client, "blog", id, nil)
var apiErr *genko.APIErrorif errors.As(err, &apiErr) && apiErr.Status == http.StatusNotFound { http.NotFound(w, r) return}if err != nil { http.Error(w, "Could not fetch entry", http.StatusInternalServerError) log.Printf("genko: %v", err) return}Unlike a list response, a detail response is the entry object itself, without a contents wrapper. Unpublished, deleted, and nonexistent IDs all return 404.
Use errors.As to extract *APIError and branch on Status. A network failure that never receives an HTTP response is not an *APIError.
Do not return the raw error to the browser. It may contain a URL or key. Log it and show a general message.
Filter entries
filters combines a field ID and an operator in a string.
tech, err := genko.GetList[genko.Post](ctx, client, "blog", url.Values{ "filters": {"category[equals]tech"}, "orders": {"-publishedAt"}, "limit": {"10"},})
since := time.Now().AddDate(0, 0, -30).UTC().Format("2006-01-02T15:04:05.000Z")recent, err := genko.GetList[genko.Post](ctx, client, "blog", url.Values{ "filters": {"publishedAt[greater_than]" + since},})See the query reference for the operators and how to combine conditions with [and] and [or].
Handle errors
| Status | Likely cause |
|---|---|
400 | Invalid query format, value, or operator |
401 | Missing, invalid, or expired API key |
403 | Key and workspace do not match |
404 | Workspace, API, or entry does not exist, or is outside the key's scope |
429 | More than 300 requests per minute with the same API key |
See Errors and limits for details.
Next steps
Continue with the tutorial for pagination, previewing saved drafts, and webhook signature verification.
Last updated: October 6, 2026