Skip to content

Server SDKs/Go

Go

github.com/judeotine/velo/sdks/go is the server half of Velo: one *Client over the REST API, a context.Context on every call, typed errors and nothing outside the standard library. It is built to hold a project API key, which is why it belongs on a server you control.

The package

The module is github.com/judeotine/velo/sdks/go and the package it declares is velo. Its go.mod names go 1.26 and lists no requirements at all: the transport is net/http, the codec is encoding/json, and nothing else is pulled in. Inside this repository the module is a member of the root go.work workspace, so it builds and tests alongside the services with no extra setup.

  • 53 methods on one *velo.Client, one per project endpoint.
  • Every method takes a context.Context first. Cancelling it cancels the request in flight.
  • Responses decode into structs with time.Time timestamps. Opaque JSON, such as Room.Metadata, Destination.Config and WebhookDelivery.Payload, stays as json.RawMessage.

This package is the opposite of the mobile and browser packages: it is meant to hold a project API key, because it runs on your server. A key starts with vk_ and grants full control of the project. It ends rooms, removes participants, mints a token for any identity, starts recordings and reads your usage. Never compile it into a client application, and never hand a project key to a browser or an app binary, because a shipped binary can be disassembled. A client gets in with a room token from POST /v1/tokens, which this package calls as CreateToken, or by exchanging a room code it was given.

Install

One module, no transitive dependencies to audit.

bash
go get github.com/judeotine/velo/sdks/go

The import path ends in go but the package is named velo, so name it at the import site and every call reads as velo.Something.

go
import velo "github.com/judeotine/velo/sdks/go"

Constructing a client

velo.New returns (*Client, error). Read the key from the environment, never from source, and let the error path handle a misconfigured deployment rather than discovering it on the first request.

go
client, err := velo.New(os.Getenv("VELO_API_KEY"))
if err != nil {
	log.Fatal(err)
}
  • A blank key is ErrMissingAPIKey.
  • A key that does not start with vk_ is ErrInvalidAPIKey. Room tokens and console session tokens are rejected here on purpose: they are not project keys.
  • Surrounding whitespace is trimmed before either check, so a key pasted with a trailing newline still works.

The rest is functional options. Each one is applied after the defaults, so passing none is the same as passing the defaults.

go
client, err := velo.New(
	os.Getenv("VELO_API_KEY"),
	velo.WithBaseURL("https://api.usevelo.xyz"),
	velo.WithHTTPClient(&http.Client{Transport: myTransport}),
	velo.WithTimeout(15*time.Second),
)
WithBaseURL
Overrides velo.DefaultBaseURL, which is https://api.usevelo.xyz. Whitespace and trailing slashes are trimmed; if that leaves nothing, New returns ErrMissingBaseURL.
WithHTTPClient
Your own *http.Client, for a proxy, a custom transport or a retry wrapper. A plain &http.Client{} is built when you do not pass one.
WithTimeout
Bounds every request, as a context.WithTimeout layered over the context you pass. velo.DefaultTimeout is 30 seconds. Pass 0 to leave the deadline entirely to your context and your HTTP client.

Every request carries Authorization: Bearer with your key and Accept: application/json. A request with a body adds Content-Type: application/json, and an idempotent call adds Idempotency-Key. Client.BaseURL() reads back the origin that was resolved, which is useful in a health check.

Quickstart

Three steps, and only the third one produces something a client may hold: create the room, mint a room token for one identity, and return the token and its url. The key stays in the process.

go
package main

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

	velo "github.com/judeotine/velo/sdks/go"
)

func main() {
	client, err := velo.New(os.Getenv("VELO_API_KEY"))
	if err != nil {
		log.Fatal(err)
	}

	room, err := client.CreateRoom(context.Background(), "consultation-42", &velo.CreateRoomOptions{
		MaxParticipants: 8,
		IdempotencyKey:  "consultation-42-create",
	})
	if err != nil {
		log.Fatal(err)
	}

	http.HandleFunc("/api/velo-token", func(w http.ResponseWriter, r *http.Request) {
		token, err := client.CreateToken(r.Context(), velo.CreateTokenRequest{
			Room:       room.Name,
			Identity:   "patient-1187",
			Name:       "Amina",
			TTLSeconds: 3600,
			Role:       "patient",
		})
		if err != nil {
			http.Error(w, "could not mint a token", http.StatusBadGateway)
			return
		}
		_ = json.NewEncoder(w).Encode(map[string]string{"token": token.Token, "url": token.URL})
	})

	log.Fatal(http.ListenAndServe(":8080", nil))
}

The same three steps in both server SDKs. Only token and url leave the server.

CreateToken returns a *velo.Token carrying Token, URL, ExpiresAt, and, when the room has a template, Role and PublishProfile. Send the first two to the browser or the phone and nothing else.

velo.CreateTokenRequest
FieldTypeRequiredDescription
RoomstringYesThe room name, unnamespaced, as you created it.
IdentitystringYesWho the token admits. It is the identity every other call refers to.
NamestringNoThe display name other participants see.
MetadatastringNoAn opaque string carried on the participant. It is not read by Velo.
TTLSecondsintNoHow long the token stays valid. Omitted when zero, which takes the server default.
RolestringNoA role from the template the room uses. It fixes publish sources, the subscribe graph, permissions, priority and capacity.
Permissions*TokenPermissionsNoCanPublish, CanSubscribe and CanPublishData, each a *bool so that unset and false are different things.
IdempotencyKeystringNoSent as the Idempotency-Key header when it is not empty.

What it covers

The same 53 calls the Python SDK exposes, in Go naming. This is a map, not a reference: every method maps to one endpoint, and the full request and response shapes live in the REST API reference.

  • Rooms and tokens: CreateRoom, ListRooms, GetRoom, DeleteRoom, CreateToken.
  • Participants and moderation: ListParticipants, GetParticipant, RemoveParticipant, MuteTrack, SendData, ChangeParticipantRole, ListRoleChanges.
  • Sessions: ListSessions, GetSession, ListRoomSessions.
  • Recordings and broadcasts: StartRecording, ListRecordings, ListRoomRecordings, GetRecording, StopRecording, StartBroadcast, ListBroadcasts, GetBroadcast, StopBroadcast.
  • Ingress: CreateIngress, ListIngresses, GetIngress, DeleteIngress. Ingress.StreamKey is populated on the create response only.
  • Templates, roles and destinations: CreateTemplate, ListTemplates, GetTemplate, UpdateTemplate, DeleteTemplate, GetTemplateSettings, UpdateTemplateSettings, PutRole, GetRole, DeleteRole, CreateDestination, ListDestinations, GetDestination, UpdateDestination, DeleteDestination.
  • Room codes: CreateRoomCode, ListRoomCodes, DeleteRoomCode. This is how a client joins without ever seeing a key.
  • Webhooks: CreateWebhookEndpoint, ListWebhookEndpoints, DeleteWebhookEndpoint, ListWebhookDeliveries, RetryWebhookDelivery.
  • Billing: GetUsage and GetPlan. UsageFilters.From and UsageFilters.To are YYYY-MM-DD day strings, while timestamp filters such as After and Before are *time.Time and are sent as RFC 3339 in UTC.

SendData takes raw []byte and base64 encodes it for the wire, so pass your payload as it is. Destination offers RecordingConfig() and RTMPConfig() to decode its config for the matching Kind; stored secrets come back redacted as velo.RedactedSecret.

Pagination

List methods return *velo.Page[T], one generic type across every resource. It carries Items, which is never nil, plus Limit and Offset as the server applied them and HasMore. Paging is velo.PageOptions, which filter structs embed, so a filtered list takes its window on the same struct as its filters.

go
live := true
filters := &velo.SessionFilters{
	PageOptions: velo.PageOptions{Limit: 100},
	Room:        "consultation-42",
	Active:      &live,
}

for {
	page, err := client.ListSessions(ctx, filters)
	if err != nil {
		return err
	}
	for _, session := range page.Items {
		fmt.Println(session.ParticipantIdentity, session.DurationSeconds)
	}
	if !page.HasMore {
		break
	}
	filters.Offset = page.Offset + page.Limit
}
  • A zero Limit or Offset is left out of the query string entirely, so the server default applies rather than a literal zero.
  • ListDestinations is the one exception to the shape: the endpoint returns a plain array, so it returns []Destination.
  • A simple list such as ListRooms takes *PageOptions directly, and nil means the defaults.

Idempotency

Calls that create or trigger something accept an idempotency key, sent as the Idempotency-Key header. Replaying the same key with the same body replays the original response instead of acting twice; replaying it with a different body is rejected with idempotency_key_reuse. An empty key means the header is not sent at all.

go
room, err := client.CreateRoom(ctx, "consultation-42", &velo.CreateRoomOptions{
	IdempotencyKey: "consultation-42-create",
})

rec, err := client.StopRecording(ctx, egressID, &velo.IdempotencyOptions{
	IdempotencyKey: "stop-" + egressID,
})
  • The key lives on the options struct as IdempotencyKey for methods that have other options: CreateRoom, CreateToken, SendData, ChangeParticipantRole, StartRecording, StartBroadcast, CreateTemplate, CreateDestination and CreateRoomCode.
  • It arrives as *velo.IdempotencyOptions where it is the only option: MuteTrack, StopRecording, StopBroadcast, CreateWebhookEndpoint and RetryWebhookDelivery. Passing nil omits it.
  • Nothing else takes one. CreateIngress, the update calls and PutRole have no idempotency parameter, because they are already defined by the state they set.

Errors

Every API failure arrives in one envelope, and the package turns it into an error you can branch on rather than handing back a response to inspect.

json402 Payment Required
{
  "error": {
    "code": "quota_exceeded",
    "message": "monthly participant minutes exhausted"
  }
}
go
import (
	"errors"

	velo "github.com/judeotine/velo/sdks/go"
)

recording, err := client.StartRecording(ctx, "consultation-42", velo.StartRecordingOptions{
	Type: velo.RecordingTypeRoomComposite,
})

switch {
case err == nil:
	log.Printf("recording %s started", recording.EgressID)
case errors.Is(err, velo.ErrQuotaExceeded):
	showUpgradePrompt()
case errors.Is(err, velo.ErrRateLimited):
	retryAfterBackoff()
case errors.Is(err, velo.ErrNotFound):
	log.Print("that room is gone")
default:
	var apiErr *velo.APIError
	if errors.As(err, &apiErr) {
		log.Printf("velo rejected %s: %s (%s)", apiErr.Field, apiErr.Message, apiErr.Code)
	}
	var connErr *velo.ConnectionError
	if errors.As(err, &connErr) {
		log.Printf("%s %s never reached velo: %v", connErr.Method, connErr.Path, connErr.Err)
	}
}
  • *velo.APIError is any non-2xx response. It carries Status, Code, Message, Field and the raw Body as []byte.
  • errors.Is matches it against velo.ErrQuotaExceeded for a 402, velo.ErrPermissionDenied for a 403 or a body whose code is permission_denied, velo.ErrRateLimited for a 429 and velo.ErrNotFound for a 404. There is no subtype to assert: the sentinel comparison is the whole mechanism.
  • errors.As reaches the details when you want the field a validation error named, or the exact code to log.
  • *velo.ConnectionError means the request never produced a response. It carries Method, Path and Err, and it unwraps, so errors.Is(err, context.Canceled) and errors.Is(err, context.DeadlineExceeded) both work through it.
  • When a response is not a Velo envelope, Code becomes http_{status} and Message holds the first 512 bytes of the body.

A quota failure is an expected state, not a bug: the project is live and out of minutes. Branch on it first, tell the person what happened, and leave the retry loop for ErrRateLimited. Not every 402 is about minutes: recording and live streaming are not on the free plan, so on a free project StartRecording, StartBroadcast, CreateIngress and CreateDestination match ErrQuotaExceeded with the code feature_not_in_plan and the refused feature in Field. The list, get, stop and delete calls are never refused for that reason. Full status meanings are in Authentication.

Where to go next

  • The REST API is the full surface every method here maps onto, request body by request body.
  • Webhooks is how your Go service hears about a room ending or a recording completing without polling ListSessions.
  • The Web SDK is what receives the token this package mints. The Android, iOS, Flutter and React Native packages take the same token.
  • The Python SDK is the same surface for a Python backend, method for method.

Was this page useful?

Edit this page on GitHub