Go

The official client for Go.

The vojo-go module is the official Go client. It needs Go 1.23 or newer, for range-over-func iterators, and uses only the standard library.

Install

go get github.com/VoJo-Media/vojo-go

The module isn't published yet. Until it is, ask your Vojo contact for a copy.

Make your first call

package main

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

	"github.com/VoJo-Media/vojo-go"
)

func main() {
	client, err := vojo.NewClient(os.Getenv("VOJO_KEY"))
	if err != nil {
		log.Fatal(err)
	}

	organisation, err := client.Organisation.Retrieve(context.Background())
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(organisation.Name)
}

Pass "" and the client reads VOJO_SECRET_KEY.

Call the API

Methods follow the API's paths, and every call takes a context.Context first. Each reference page shows its method. Optional fields are pointers, so use vojo.Ptr to set one. A field the API allows to be null takes vojo.Value or vojo.Null. Most methods return the resource itself, and statistics and issues keep Data and Meta.

site, err := client.Sites.Create(ctx, &vojo.CreateSiteRequest{Name: "Leeds Trinity"})

screen, err := client.Screens.Create(ctx, &vojo.CreateScreenRequest{
	Name:       "Lobby North",
	SiteID:     vojo.Ptr(site.ID),
	SizeInches: vojo.Ptr(55.0),
})

same, err := client.Screens.Retrieve(ctx, screen.ID)

// A nil field is unchanged. vojo.Null sends null.
updated, err := client.Screens.Update(ctx, screen.ID, &vojo.UpdateScreenRequest{
	Name:  vojo.Ptr("Lobby South"),
	Notes: vojo.Null[string](),
})

Page through a list

All walks every item across every page, fetching more as it goes.

for screen, err := range client.Screens.List(ctx, nil).All() {
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(screen.Name)
}

FirstPage gives you one page, if you want to page yourself.

page, err := client.Screens.List(ctx, &vojo.ScreensListQuery{Limit: 100}).FirstPage()
fmt.Println(len(page.Data), page.HasMore())

Handle errors

Errors work with errors.Is and errors.As.

screen, err := client.Screens.Retrieve(ctx, "scr_missing")

var apiErr *vojo.APIError
switch {
case errors.Is(err, vojo.ErrNotFound):
	// Handle a missing screen.
case errors.Is(err, vojo.ErrRateLimit):
	errors.As(err, &apiErr)
	wait, _ := apiErr.RetryAfter()
	fmt.Println("wait", wait)
case errors.As(err, &apiErr):
	fmt.Println(apiErr.Status, apiErr.Code, apiErr.RequestID)
}
Sentinel When
ErrAuthentication 401
ErrPermission 402 or 403
ErrNotFound 404
ErrValidation 400, 413 or 422
ErrConflict 409
ErrRateLimit 429: see RetryAfter()
ErrServer 5xx
ErrPagination The API's paging data couldn't be followed safely

A request with no response gives a *vojo.ConnectionError, with Timeout set when it timed out. Always handle a code you haven't seen, because new ones can be added.

Verify webhooks

Verifying needs only the endpoint's signing secret, not an API key. Pass the raw request body, exactly as you received it.

func handler(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "unreadable", http.StatusBadRequest)
		return
	}
	event, err := webhook.ConstructEvent(body, r.Header.Get(webhook.SignatureHeader), signingSecret)
	if err != nil {
		http.Error(w, "bad signature", http.StatusBadRequest)
		return
	}

	if event.Type == webhook.TypeVerificationScore {
		var score webhook.VerificationScoreEvent
		if err := event.Decode(&score); err != nil {
			http.Error(w, "bad payload", http.StatusBadRequest)
			return
		}
	}
	w.WriteHeader(http.StatusNoContent)
}

The package is github.com/VoJo-Media/vojo-go/webhook. Deliveries can repeat, so deduplicate on event.ID, which stays the same across retries. While you switch to a new secret after a roll, hold both: webhook.ConstructEvent(body, header, newSecret, webhook.WithAdditionalSecret(oldSecret)).

errors.As with a *webhook.VerificationError gives the reason in Kind. To test your own handler, sign a payload with webhook.GenerateTestHeader(body, secret, time.Now()).

Retries and idempotency

Failed requests are retried twice when that's safe: connection failures, 429, and 5xx on operations that can't apply twice.

Every create is sent with an Idempotency-Key, reused across its retries. To retry across your own restarts, supply your own.

site, err := client.Sites.Create(ctx, &vojo.CreateSiteRequest{Name: "Leeds Trinity"},
	vojo.WithIdempotencyKey("site-"+yourRecordID))

Configure the client

client, err := vojo.NewClient(key,
	vojo.WithOrganisation("org_123"),          // OAuth access tokens only.
	vojo.WithVersion("2026-10-07"),            // Default: the version this release was built against.
	vojo.WithTimeout(30*time.Second),          // Per attempt. Default 60s. Use the context for an overall limit.
	vojo.WithMaxRetries(5),                    // Default 2.
	vojo.WithBaseURL("http://127.0.0.1:4020"), // For example, the mock server.
)

// The same options work on one call.
screen, err := client.Screens.Retrieve(ctx, screenID, vojo.WithTimeout(5*time.Second))

vojo.WithTokenSource reads the token on each request, for OAuth tokens that refresh.

Results carry the HTTP details on LastResponse, with Status, Header, RequestID, Meta and IdempotentReplayed. Check wrapper.LastResponse.Meta after you wrap a creative, and quote RequestID when you contact Vojo.