genko.meDocs
Frameworks and languagesGo

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.

go
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.

genko/client.go
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.

genko/post.go
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

main.go
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.

go
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.

go
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

StatusLikely cause
400Invalid query format, value, or operator
401Missing, invalid, or expired API key
403Key and workspace do not match
404Workspace, API, or entry does not exist, or is outside the key's scope
429More 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

On this page