Go quickstart

A Go program that refreshes a token, lists your screens and puts a playlist on a screen. The blocks on this page form one file.

1 Get a token

Open the CMS at Settings, Connected apps and click Generate token. A member without admin rights does not see this tab.

Then choose the organization, which decides the screens the program sees, and click Allow. The next page shows the client ID and the refresh token, once. Copy both.

In your terminal, set the client ID and the path of the token file, then write the refresh token to that file. read -rs waits without showing anything: paste the refresh token and press Enter, so it stays out of the shell history:

export GIBEON_CLIENT_ID=CLIENT_ID
export GIBEON_REFRESH_TOKEN_FILE=./gibeon-refresh-token
touch "$GIBEON_REFRESH_TOKEN_FILE"
chmod 600 "$GIBEON_REFRESH_TOKEN_FILE"
read -rs GIBEON_REFRESH_TOKEN && printf '%s' "$GIBEON_REFRESH_TOKEN" > "$GIBEON_REFRESH_TOKEN_FILE"

The program needs Go 1.18 or newer. It uses the standard library only, so there is nothing to install. Put the blocks below in order in main.go and run go run main.go.

2 Refresh the token

An access token lasts 60 minutes. Every refresh also returns a new refresh token, so a value in the environment is stale after one run.

The program writes the new token to a temp file in the same directory, sets chmod 600 and renames it over the file in GIBEON_REFRESH_TOKEN_FILE. Only then does it use the access token. A process that dies while using it keeps a working refresh token.

This block also holds the imports and the helpers. On any status outside 2xx, send stops the program with exit code 1.

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"time"
)

var client = &http.Client{Timeout: 30 * time.Second}

type player struct {
	ID     string `json:"id"`
	Name   string `json:"name"`
	Status string `json:"status"`
}

// exitOn stops the program with a non-zero exit code.
func exitOn(err error) {
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

// send runs the request and stops the program on any non-2xx status.
func send(req *http.Request) []byte {
	response, err := client.Do(req)
	exitOn(err)
	defer response.Body.Close()

	body, err := io.ReadAll(response.Body)
	exitOn(err)
	if response.StatusCode < 200 || response.StatusCode > 299 {
		exitOn(fmt.Errorf("%s %s: %s", req.Method, req.URL, response.Status))
	}
	return body
}

func refresh() string {
	tokenFile := os.Getenv("GIBEON_REFRESH_TOKEN_FILE")
	refreshToken, err := os.ReadFile(tokenFile)
	exitOn(err)

	form := url.Values{
		"grant_type":    {"refresh_token"},
		"client_id":     {os.Getenv("GIBEON_CLIENT_ID")},
		"refresh_token": {strings.TrimSpace(string(refreshToken))},
	}
	req, err := http.NewRequest("POST", "https://id.gibeon.io/auth/v1/oauth/token", strings.NewReader(form.Encode()))
	exitOn(err)
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	var tokens struct {
		AccessToken  string `json:"access_token"`
		RefreshToken string `json:"refresh_token"`
	}
	exitOn(json.Unmarshal(send(req), &tokens))
	if tokens.RefreshToken == "" {
		exitOn(fmt.Errorf("the response holds no refresh token"))
	}

	// Store the new refresh token before anything uses the access token.
	tmp, err := os.CreateTemp(filepath.Dir(tokenFile), ".gibeon-refresh-token-*")
	exitOn(err)
	_, err = tmp.WriteString(tokens.RefreshToken)
	exitOn(err)
	exitOn(tmp.Close())
	exitOn(os.Chmod(tmp.Name(), 0o600))
	exitOn(os.Rename(tmp.Name(), tokenFile))

	return tokens.AccessToken
}

3 List your screens

This returns every screen in your tenant. The program prints the id, name and status of each screen, and never a token.

func listScreens(accessToken string) {
	req, err := http.NewRequest("GET", "https://www.gibeon.io/v1/players", nil)
	exitOn(err)
	req.Header.Set("Authorization", "Bearer "+accessToken)

	var result struct {
		Data []player `json:"data"`
	}
	exitOn(json.Unmarshal(send(req), &result))

	for _, p := range result.Data {
		fmt.Println(p.ID, p.Name, p.Status)
	}
}

4 Assign a playlist and publish

This step runs only when GIBEON_PLAYER_ID and GIBEON_PLAYLIST_ID are set. The screen id is in the list from the previous step. The playlist id is in the CMS address bar when you open a playlist: /playlists/PLAYLIST_ID/content. Export both and run the program again.

A screen with zones plays those; otherwise its sequence; otherwise its planning; otherwise its own playlist. The body clears the sequence and the planning, so the playlist plays unless the screen has zones.

The PATCH alone changes nothing on the screen: until you publish, it keeps showing the old assignment. Publishing builds a new snapshot and tells the screen right away.

The last function, main, calls the three in order.

func assignAndPublish(accessToken string) {
	playerID := os.Getenv("GIBEON_PLAYER_ID")
	playlistID := os.Getenv("GIBEON_PLAYLIST_ID")
	if playerID == "" || playlistID == "" {
		return
	}

	body, err := json.Marshal(map[string]any{
		"playlist_id": playlistID,
		"sequence_id": nil,
		"planning_id": nil,
	})
	exitOn(err)
	req, err := http.NewRequest("PATCH", fmt.Sprintf("https://www.gibeon.io/v1/players/%s", playerID), bytes.NewReader(body))
	exitOn(err)
	req.Header.Set("Authorization", "Bearer "+accessToken)
	req.Header.Set("Content-Type", "application/json")

	var result struct {
		Data player `json:"data"`
	}
	exitOn(json.Unmarshal(send(req), &result))

	req, err = http.NewRequest("POST", fmt.Sprintf("https://www.gibeon.io/v1/players/%s/publish", playerID), nil)
	exitOn(err)
	req.Header.Set("Authorization", "Bearer "+accessToken)
	send(req)

	fmt.Println(result.Data.ID, result.Data.Name, result.Data.Status)
}

func main() {
	accessToken := refresh()
	listScreens(accessToken)
	assignAndPublish(accessToken)
}

What’s next