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.Contextfirst. Cancelling it cancels the request in flight. - Responses decode into structs with
time.Timetimestamps. Opaque JSON, such asRoom.Metadata,Destination.ConfigandWebhookDelivery.Payload, stays asjson.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.
go get github.com/judeotine/velo/sdks/goThe 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.
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.
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_isErrInvalidAPIKey. 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.
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 ishttps://api.usevelo.xyz. Whitespace and trailing slashes are trimmed; if that leaves nothing,NewreturnsErrMissingBaseURL. - 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.WithTimeoutlayered over the context you pass.velo.DefaultTimeoutis 30 seconds. Pass0to 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.
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.
| Field | Type | Required | Description |
|---|---|---|---|
| Room | string | Yes | The room name, unnamespaced, as you created it. |
| Identity | string | Yes | Who the token admits. It is the identity every other call refers to. |
| Name | string | No | The display name other participants see. |
| Metadata | string | No | An opaque string carried on the participant. It is not read by Velo. |
| TTLSeconds | int | No | How long the token stays valid. Omitted when zero, which takes the server default. |
| Role | string | No | A role from the template the room uses. It fixes publish sources, the subscribe graph, permissions, priority and capacity. |
| Permissions | *TokenPermissions | No | CanPublish, CanSubscribe and CanPublishData, each a *bool so that unset and false are different things. |
| IdempotencyKey | string | No | Sent 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.StreamKeyis 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:
GetUsageandGetPlan.UsageFilters.FromandUsageFilters.ToareYYYY-MM-DDday strings, while timestamp filters such asAfterandBeforeare*time.Timeand 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.
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
LimitorOffsetis left out of the query string entirely, so the server default applies rather than a literal zero. ListDestinationsis the one exception to the shape: the endpoint returns a plain array, so it returns[]Destination.- A simple list such as
ListRoomstakes*PageOptionsdirectly, andnilmeans 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.
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
IdempotencyKeyfor methods that have other options:CreateRoom,CreateToken,SendData,ChangeParticipantRole,StartRecording,StartBroadcast,CreateTemplate,CreateDestinationandCreateRoomCode. - It arrives as
*velo.IdempotencyOptionswhere it is the only option:MuteTrack,StopRecording,StopBroadcast,CreateWebhookEndpointandRetryWebhookDelivery. Passingnilomits it. - Nothing else takes one.
CreateIngress, the update calls andPutRolehave 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.
{
"error": {
"code": "quota_exceeded",
"message": "monthly participant minutes exhausted"
}
}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.APIErroris any non-2xx response. It carriesStatus,Code,Message,Fieldand the rawBodyas[]byte.errors.Ismatches it againstvelo.ErrQuotaExceededfor a 402,velo.ErrPermissionDeniedfor a 403 or a body whose code ispermission_denied,velo.ErrRateLimitedfor a 429 andvelo.ErrNotFoundfor a 404. There is no subtype to assert: the sentinel comparison is the whole mechanism.errors.Asreaches the details when you want the field a validation error named, or the exact code to log.*velo.ConnectionErrormeans the request never produced a response. It carriesMethod,PathandErr, and it unwraps, soerrors.Is(err, context.Canceled)anderrors.Is(err, context.DeadlineExceeded)both work through it.- When a response is not a Velo envelope,
Codebecomeshttp_{status}andMessageholds 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?