Help us shape the future of Go! Take the 2026 Go Developer Survey

voltkit

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 23, 2026 License: MIT Imports: 22 Imported by: 0

README

VoltKit RTC server SDK for Go

Server SDK for VoltKit RTC: rooms, tokens, chat, usage, billing and verified webhooks, from a Go backend. Go 1.22 or newer, standard library only.

Your backend does two things: decide who may do what, and hand the client a short-lived token. It is never in the media path: there is no stream to proxy and nothing that scales with viewers.

go get github.com/voltkit-rtc/voltkit-rtc-server-go
import voltkit "github.com/voltkit-rtc/voltkit-rtc-server-go"
Quickstart NewStreamClient NewStreamClientFromEnv
Rooms EnsureRoom CreateRoom CreateToken GetRoom ListRooms ListParticipants DeleteRoom
Chat CreateChatToken SendMessage ListMessages DeleteMessage MuteChat UnmuteChat ListChatMutes GetChatInfo
Usage GetUsage GetUsageSeries
Billing GetBalance GetRates ParseCu
Configuration Config Ptr
Errors Error ErrorCodes RESTErrorCodes ErrorInfo
Webhooks ParseWebhook VerifyWebhook VerifyOptions StreamEvent
Broadcast CreateStream CreateStreamToken GetStream ListStreams UpdateStream DeleteStream (not offered on VoltKit cloud today)

Quickstart

Create one client when your server starts and share it: it is safe for concurrent use. Both values come from your project's Credentials tab; the secret is server-side only.

client, err := voltkit.NewStreamClient(voltkit.Config{
	BaseURL: os.Getenv("VOLTKIT_RTC_URL"),    // e.g. https://api.voltkit.in
	APIKey:  os.Getenv("VOLTKIT_RTC_SECRET"), // sk_...: never in a web page or a mobile app
})
if err != nil {
	log.Fatal(err) // a missing or malformed option, caught at startup; the error never quotes the key
}

NewStreamClientFromEnv() reads the same two variables, plus the optional VOLTKIT_API_URL for GetRates, and nothing else.

A "go live" and a "let someone in", with the role your rules decide:

room, err := client.EnsureRoom(ctx, voltkit.EnsureRoomParams{
	RoomID:       "show-" + show.ID, // YOUR id, so webhooks read back usefully
	HostIdentity: "user-" + user.ID,
})
// room.HostToken, room.WSURL, room.ExpiresAt, room.Existed

tok, err := client.CreateToken(ctx, voltkit.CreateTokenParams{
	RoomID:   "show-" + show.ID,
	Identity: "user-" + viewer.ID,
	Role:     voltkit.RoleViewer, // host, cohost, moderator or viewer; signed into the token
})
// tok.Token, tok.WSURL, tok.ExpiresAt: hand them to that one client, never log them

A complete token endpoint on net/http, with the error mapping, is in examples/tokenserver; a webhook receiver in examples/webhookreceiver.

Every method takes a context.Context first. Cancelling it, or its deadline passing, stops the call and its retries and returns the context's error.

Rooms

A room is the interactive kind of session: everyone in it can be given the right to speak, and media goes through the SFU.

// Create the room, or get it back with a fresh host token if it already exists.
room, err := client.EnsureRoom(ctx, voltkit.EnsureRoomParams{RoomID: "show-42", HostIdentity: "user-7"})
if room.Existed { /* a host reconnecting, not a new "go live" */ }

// The strict version: fails with room-exists (409) if the id is taken; makes up an id if RoomID is empty.
room, err = client.CreateRoom(ctx, voltkit.CreateRoomParams{RoomID: "show-42", HostIdentity: "user-7"})

// Opaque, room-scoped state every participant receives on join. Applied only when the room is created.
room, err = client.EnsureRoom(ctx, voltkit.EnsureRoomParams{
	RoomID: "show-42", HostIdentity: "user-7",
	Metadata: map[string]any{"seatFormat": 20},
})

// Let someone in. Your backend decides the role; the SFU enforces it.
tok, err := client.CreateToken(ctx, voltkit.CreateTokenParams{RoomID: "show-42", Identity: "user-99", Role: voltkit.RoleViewer})

// The authoritative live view: the room, who is in it, what they publish.
view, err := client.GetRoom(ctx, "show-42")
// view.Room.Participants, view.Participants, view.Producers

list, err := client.ListRooms(ctx)   // list.Rooms: every live room (the wire's object, kept)
people, err := client.ListParticipants(ctx, "show-42") // []Participant: identity, role, handRaised, muted, joinedAt, metadata
err = client.DeleteRoom(ctx, "show-42")                // ends it for everyone
  • A room token lasts ten minutes and authorises the handshake; a connected client is refreshed by the controller, so a call can run for hours.
  • Poll GetRoom for a live count rather than adding up webhook joins and leaves: delivery is at-least-once and unordered, so a counter built from events drifts.
  • Metadata is any value encoding/json can encode (a map, a struct, a json.RawMessage); replies carry it back as json.RawMessage. Left nil, it is left out of the body; a nil json.RawMessage or a typed nil pointer was given, and is sent as null.
  • Room ids are [A-Za-z0-9._-]{1,128} and scoped to your project: another project's room is a 404, never a 403.

Chat

Every room has a chat channel of the same name, and a channel does not need a room: show-42 can be chat for a broadcast, a lobby, or nothing but chat. Someone in the room already has chat on their room connection; for anyone else, mint a chat token:

chat, err := client.CreateChatToken(ctx, voltkit.CreateChatTokenParams{Channel: "show-42", Identity: "user-99"})
// chat.Token, chat.WSURL (ends in /ws/chat), chat.Role (viewer by default), chat.ExpiresAt

Your backend takes part without holding a socket:

_, err = client.SendMessage(ctx, voltkit.SendMessageParams{Channel: "show-42", Text: "The show starts in 5 minutes", Type: "system"})
_, err = client.SendMessage(ctx, voltkit.SendMessageParams{Channel: "show-42", Type: "gift", From: "user-7", Metadata: map[string]any{"gift": "rose", "count": 5}})
_, err = client.SendMessage(ctx, voltkit.SendMessageParams{Channel: "show-42", Type: "reaction", Metadata: map[string]any{"emoji": "heart"}, Ephemeral: true}) // not stored
_, err = client.SendMessage(ctx, voltkit.SendMessageParams{Channel: "show-42", To: "user-13", Text: "Please keep it friendly"})                                  // a direct message

messages, err := client.ListMessages(ctx, "show-42", &voltkit.ListMessagesOptions{Limit: 100}) // oldest first
older, err := client.ListMessages(ctx, "show-42", &voltkit.ListMessagesOptions{Before: messages[0].ID})
err = client.DeleteMessage(ctx, "show-42", messageID)
err = client.MuteChat(ctx, "show-42", "user-13")   // survives reconnecting; they can still read
err = client.UnmuteChat(ctx, "show-42", "user-13")
muted, err := client.ListChatMutes(ctx, "show-42") // []string
info, err := client.GetChatInfo(ctx, "show-42")    // info.Members

From defaults to system, and you may speak as one of your own users: your backend is the authority on who they are, which is what makes a gift announcement trustworthy in a way a client-sent one is not. Backend messages skip the per-connection rate limit and the mute list. ListMessages returns the newest Limit messages (50 by default, at most 100), oldest first, direct messages included: your backend owns the conversation. A message needs text or metadata; text is capped at 4096 bytes. SendMessage is never retried.

Usage

usage, err := client.GetUsage(ctx)
// usage.MinutesRemaining (-1 on an unlimited plan), usage.Today, usage.Month, usage.Lifetime (participantSeconds, chatMessages, ...)
// usage.Allowance: free minutes left today and this month, the paid balance, the caps, and Blocked with the reason

days, err := client.GetUsageSeries(ctx, 30) // one entry per UTC day, oldest first, ending today; 0 means 30, at most 90

Days with no usage are present and zero, so a chart drawn from the series has no gaps. When an account has nothing left, new rooms, tokens and messages fail with IsOutOfMinutes, and anything live is warned and then ended.

Billing

Balance
b, err := client.GetBalance(ctx)
// b.BillingMode ("legacy" or "wallet"), b.CanJoin, b.Blocked (*string, nil when CanJoin),
// b.Legacy (minutes spent first), b.Wallet (*WalletFunds, nil for a legacy project),
// b.Burn, b.RemainingSeconds, b.WalletLow, b.PricingVersion (*int), b.UpdatedAt

What your project can spend right now, to show a balance, warn before a call runs out, or decide whether to start one. Read only: recharges happen on the website. CanJoin says whether a newcomer would be admitted; when it is false, Blocked is the code a join or a token would be refused with:

if !b.CanJoin {
	switch *b.Blocked {
	case "tenant-suspended":    // 403: contact support
	case "billing-unavailable": // 503: billing is being set up
	default:                    // quota-exhausted, daily-limit-reached, monthly-limit-reached: recharge
	}
}

Amounts are strings. Money is in credit units (cu), 1 credit = 6,000,000 cu (voltkit.CuPerCredit), and every cu amount (PaidCu, AvailableCu, ProjectCuPerSecond, ...) is a decimal string, because a JSON number is a float and rounds a large balance without telling you. Do arithmetic with ParseCu and math/big; show AvailableCredits, which is already formatted:

if b.Wallet != nil {
	cu, err := voltkit.ParseCu(b.Wallet.AvailableCu)              // *big.Int
	credits := new(big.Int).Div(cu, big.NewInt(voltkit.CuPerCredit)) // whole credits
	fmt.Println(b.Wallet.AvailableCredits)                        // "31", "10.62": at most four decimals, rounded down
}
Rates
client, err := voltkit.NewStreamClient(voltkit.Config{
	BaseURL:      os.Getenv("VOLTKIT_RTC_URL"),
	APIKey:       os.Getenv("VOLTKIT_RTC_SECRET"),
	RatesBaseURL: "https://api.voltkit.in", // VoltKit's website API: the origin, without /api/v1
})
rates, err := client.GetRates(ctx)
// rates.Version, rates.Tiers ([{ID: "video_30", Credits: "99.00", CuPerParticipantSecond: "9900", ...}]),
// rates.Surcharges, rates.IdleTier, rates.Modes, rates.Conversion, rates.Notes

The public price list: each tier in credits per 1,000 participant-minutes, the media modes that are switched on, and the conversion for new recharges. Prices are set on the website, so GetRates reads GET /api/v1/billing/rtc/rates/current from the website API named by RatesBaseURL, and sends it no credential: no API key, no Authorization header. Without RatesBaseURL it fails at once with rates-unconfigured and sends nothing. The website caches the list for 30 seconds; cache it on your side too if you read it often. A tier's CuPerParticipantSecond is exact, so a cost is integer arithmetic:

price, _ := voltkit.ParseCu(video30.CuPerParticipantSecond)
cost := new(big.Int).Mul(price, big.NewInt(4*30*60)) // 4 people for 30 minutes, in cu

Configuration

client, err := voltkit.NewStreamClient(voltkit.Config{
	BaseURL:      "https://api.voltkit.in",
	APIKey:       secret,
	RatesBaseURL: "https://api.voltkit.in", // optional, for GetRates
	Timeout:      5 * time.Second,          // each attempt: headers and body (default 10 s)
	Retries:      voltkit.Ptr(1),           // at most one more attempt (default 2; Ptr(0) turns retries off)
	HTTPClient:   &http.Client{Transport: myTransport},
})
Field Default What it does
BaseURL required The controller's origin, exact scheme and host, as the Credentials tab shows it. The API key is sent here and nowhere else. A trailing / is dropped.
APIKey required Your project's API secret. Whitespace around it is ignored.
RatesBaseURL unset The website API's origin, where GetRates reads the price list with no credential. "" and "/" mean unset.
Timeout 10 s The deadline for each attempt, from sending the request to reading the last byte of the reply. Zero means the default; above 2^31−1 ms it is held there.
Retries nil = 2 How many more attempts a call that is safe to repeat gets after a transient failure. voltkit.Ptr(0) turns retries off.
HTTPClient a fresh http.Client Your own client, for a proxy, custom TLS or instrumentation. It is copied, never modified: the copy for the controller never follows a redirect, the copy for the rates host keeps your redirect policy, and both leave the deadline to the context.

The constructor checks every field and returns a plain error for a mistake, so a wrong value fails at startup instead of on a user's first call; none of these errors quotes the key. A Config prints under every fmt verb, marshals to JSON and logs through log/slog with the key hidden; it is still a plain struct, so keep it out of reflection-based dumps (see Safe to log). A client is safe to share between goroutines and holds no state outside a call; it logs nothing. Every request carries User-Agent: voltkit-rtc-go/<Version> and nothing else about your system.

Errors

A call that fails returns an *voltkit.Error. Branch on Code or the predicates, and log the message: it is always safe to log.

tok, err := client.CreateToken(ctx, voltkit.CreateTokenParams{RoomID: roomID, Identity: identity, Role: role})
if err != nil {
	var e *voltkit.Error
	if !errors.As(err, &e) {
		return err // not an answer from VoltKit: the context was cancelled, or a bug in this handler
	}
	log.Printf("join: %v", e) // the API secret is never in it
	switch {
	case e.IsNotFound():          // 404 no-room: this show has ended
	case e.IsOutOfMinutes():      // 402 quota-exhausted, daily-limit-reached, monthly-limit-reached
	case e.Code == "viewer-quota": // 429: this show is full
	case e.IsCapacityExhausted(): // 503 capacity-exhausted, no-nodes: alert, new rooms are failing right now
	case e.Retryable:             // the SDK has already retried: try again later, with Retry-After
	default:                      // 401, 403, 400: a problem on your side
	}
}
Field or method Meaning
Code The server's code (no-room, quota-exhausted, ...). network when no reply arrived; http-<status> when a reply named no code; redirected; rates-unconfigured; bad-request with Status 0 for an id refused before sending. A code this release does not know is passed through unchanged.
Status The HTTP status. 0 when nothing reached the server or the SDK refused the call itself; the 3xx for redirected.
Message <METHOD> <path>: <what went wrong>, such as POST /v1/rooms/show-42/tokens: room not found. Also what Error() returns.
Retryable true for network, 429 and 5xx (except billing-unavailable): trying again later could work. On a call the SDK repeats, it means the retries have been spent.
IsNotFound() Status is 404: no such room, stream or message, or not yours. broadcast-disabled and chat-disabled are 404s too, so test IsBroadcastDisabled() first.
IsOutOfMinutes() Status is 402, or the code is quota-exhausted, daily-limit-reached or monthly-limit-reached.
IsQuota() The code is room-quota, or Status is 429: rate-limited, viewer-quota and stream-quota included.
IsCapacityExhausted() capacity-exhausted or no-nodes: no capacity for a new room right now. The one to alert on.
IsBroadcastDisabled() broadcast-disabled: broadcast is not switched on for this deployment.
IsBillingUnavailable() billing-unavailable: the project is billed from the wallet and no price list is active yet, or the controller has no wallet billing.

The predicates are safe on a nil *Error.

What to do, code by code

Every code a call can fail with, from the shared registry (RESTErrorCodes). "Retried" means the SDK retries it on a call that is safe to repeat; CreateRoom, CreateStream and SendMessage are sent once.

Code Status Retried What it means, and what to do
unauthorized 401 no A wrong or revoked secret, or BaseURL is another project's. Check VOLTKIT_RTC_SECRET; alert yourself, tell the user only that something failed. A cheap read at startup, such as GetUsage, catches it before your users do.
tenant-suspended 403 no The account is suspended. Contact support.
quota-exhausted 402 no Out of minutes or credits. The project owner recharges; GetBalance says so before anyone tries.
daily-limit-reached 402 no The daily cap is reached. Wait for the UTC day to reset, or raise the cap.
monthly-limit-reached 402 no The monthly cap is reached. Wait for the month to reset, or raise the cap.
billing-unavailable 503 no Wallet billing is not ready (no active price list, or no wallet billing on this controller). An operator fixes it; a retry does not help.
no-room 404 no The room is gone, or was never yours. Treat the show as ended.
no-message 404 no The message is gone. Nothing to do.
not-found 404 no The mute, or whatever the call named, is not there. Nothing to do.
no-stream 404 no The stream is gone. Treat it as off air.
broadcast-disabled 404 no Broadcast is not available on this deployment (VoltKit cloud today). Do not retry.
chat-disabled 404 no Chat is not available on this deployment. Do not retry.
room-exists 409 no The id is taken. Use EnsureRoom, or another id.
stream-exists 409 no The stream id is taken. Use the existing stream, or another id.
bad-request 400 no Fix the call: the message names the field (an id outside [A-Za-z0-9._-]{1,128}, an empty message, a bad cursor). With Status 0, the SDK refused an id of "", "." or ".." before sending.
bad-role 400 no Role is not host, cohost, moderator or viewer. Fix the call.
operator-only 403 no An operator endpoint with a project key. Not something an SDK call reaches.
bad-signature 401 no From ParseWebhook: the delivery did not verify. Answer 401; VoltKit retries it.
rate-limited 429 yes Your project's API rate limit (600 per minute by default). Slow down; spread calls out.
room-quota 429 yes At your concurrent-room limit. End rooms you no longer need, or raise the limit.
viewer-quota 429 yes The room is at its viewer limit. Tell the viewer it is full.
stream-quota 429 yes At your concurrent-stream limit. Delete streams you no longer need.
capacity-exhausted 503 yes The fleet is full: new rooms are failing right now. Ask the user to try again shortly, and alert.
no-nodes 503 yes No media node is registered. As above.
unavailable 503 yes The service could not answer (a read failed). Try again later; tell us if it repeats.
internal 500 yes A fault on the service. Try again later; tell us if it repeats.
network 0 yes No reply: DNS, a refused or dropped connection, the timeout. Try again later; if it persists, check your server's outbound network.
redirected 3xx no The server answered a request carrying the key with a redirect. Fix BaseURL: see Redirects are never followed.
rates-unconfigured 0 no GetRates without RatesBaseURL. Set it to the website API's origin.
http-<status> that status only a 429 or 5xx A reply that named no code: a proxy's error page, or a success that is not JSON. Check BaseURL, and anything between your server and VoltKit.
RATE_LIMITED, NOT_FOUND, ... as sent by status The website API's own codes, from GetRates, passed through unchanged.

ErrorCodes is the whole shared registry in its order, RESTErrorCodes the codes above, and ErrorInfo(code) gives a code's origin and flags, for a bounded metrics label or a dashboard:

label := "other"
if _, known := voltkit.ErrorInfo(e.Code); known {
	label = e.Code
}
Mistakes caught before a request

These are plain error values, never an *Error, so a handler cannot mistake them for an answer from VoltKit. They are never retried and never quote the key.

Mistake How it fails
BaseURL or APIKey empty; BaseURL or RatesBaseURL not an absolute http(s) URL with a host, or carrying a query or fragment (a path prefix is fine) NewStreamClient returns an error
APIKey only whitespace, such as a bare line break from a file NewStreamClient returns an error
APIKey holding a character no HTTP header can carry (a line break or NUL inside, another control character, DEL, anything above U+00FF), a backslash, or a character outside ASCII such as an accented letter (no VoltKit key holds one) NewStreamClient returns an error saying to check how the key is loaded
Timeout negative, Retries negative NewStreamClient returns an error
An id of "", "." or ".." for a room, channel, message, identity or stream The call returns *Error bad-request, Status 0, and sends nothing: a URL reads those as steps through the path. Every other id is percent-encoded and sent as given
A body encoding/json cannot encode (a channel, a func, a cycle in Metadata) The call returns a plain error naming it, such as PUT /v1/rooms/show-42: the body cannot be sent as JSON: ..., with the encoder's error wrapped
GetRates without RatesBaseURL The call returns *Error rates-unconfigured, Status 0
VerifyWebhook with no secret, or a negative Tolerance Panics (a loud mistake, like the Node SDK's TypeError); ParseWebhook returns a plain error instead

Retries

Transient failures are retried with jittered backoff on every call that is safe to repeat: token minting, reads (GetBalance and GetRates included), EnsureRoom, UpdateStream, mutes and deletes. Transient means a network error (no reply, a dropped connection, the timeout), a 429, or a 5xx other than billing-unavailable, the one 503 that lasts until an operator acts. Any other 4xx is a decision the server made and will make again; redirected and a 2xx that is not JSON are not transient either.

The waits are 200 ms, 400 ms, 800 ms, ... plus up to 100 ms at random, so a fleet of servers does not retry in step (the doubling is held at 2^31−1 ms, the deadline's cap, so a very large Retries never turns into instant retries). A call can take up to (Retries + 1) × Timeout plus the waits: about 31 seconds with the defaults. In a request a user is waiting on, lower both, or give the call a context with a deadline: the SDK stops retrying the moment the context ends and returns the context's error. When an *Error reaches you with Retryable true, the retries have been spent: try again later, from a job or the user's next action, not at once in a loop.

Calls that are never repeated

CreateRoom, CreateStream and SendMessage are sent once, however they fail. If a reply is lost, repeating them would turn a success into a spurious room-exists or stream-exists, or post the message twice. Every other call is safe to repeat: tokens are independent, reads change nothing, EnsureRoom returns the room that exists, and a mute or a delete done twice ends the same way.

When one of the three fails with Retryable true, you decide:

  • A 429, whatever its code, is a refusal: nothing was done. Send it again after a pause.
  • A network error or a 5xx may have done the work before the reply was lost. Check first:
Call Check, then repeat
CreateRoom Call EnsureRoom with the same RoomID: it returns the room if your call made it, or makes it. Use ids nobody else uses.
CreateStream Call GetStream. If the stream is there, mint a publish URL with CreateStreamToken{Action: voltkit.ActionPublish}, since only CreateStream returns one. If IsNotFound, create it again.
SendMessage Put your own id in Metadata, and look for it in ListMessages before sending again. An Ephemeral message is never stored, so it cannot be found: send it again or drop it.
// Announce a gift once, even when a reply is lost.
func announceGift(ctx context.Context, channel string, gift Gift) (*voltkit.ChatMessage, error) {
	msg := voltkit.SendMessageParams{Channel: channel, Type: "gift", From: gift.From, Metadata: map[string]any{"giftId": gift.ID, "gift": gift.Name}}
	sent, err := client.SendMessage(ctx, msg)
	var e *voltkit.Error
	if err == nil || !errors.As(err, &e) || !e.Retryable {
		return sent, err
	}
	if e.Status != 429 {
		// It may have been stored before the reply was lost: look before sending again.
		recent, err := client.ListMessages(ctx, channel, &voltkit.ListMessagesOptions{Limit: 100})
		if err != nil {
			return nil, err
		}
		for i := range recent {
			var meta struct{ GiftID string `json:"giftId"` }
			if json.Unmarshal(recent[i].Metadata, &meta) == nil && meta.GiftID == gift.ID {
				return &recent[i], nil
			}
		}
	}
	time.Sleep(time.Second)
	return client.SendMessage(ctx, msg)
}
Deletes that lost their reply

DeleteRoom, DeleteMessage, UnmuteChat and DeleteStream hold across a lost reply. When an attempt fails in a way that may have reached the server (a network error or a 5xx) and the retry answers 404, the first attempt did the deleting, so the call succeeds. A 404 on the first attempt still fails, as does one after a 429 (which deleted nothing) or one saying chat or broadcast is switched off (chat-disabled, broadcast-disabled). To treat "already gone" as done, catch it:

if err := client.DeleteRoom(ctx, roomID); err != nil {
	var e *voltkit.Error
	if !errors.As(err, &e) || !e.IsNotFound() {
		return err
	}
	// it had already ended
}

Where your API secret goes

The SDK sends your API secret in the X-API-Key header of requests to BaseURL, and nowhere else:

  • GetRates sends nothing to RatesBaseURL.
  • A redirect is never followed with it (below).
  • It never appears in an error or in the printed client (below).
Redirects are never followed

A request that carries the secret never follows a redirect: Go's client would keep X-API-Key on the way to whatever host the Location names. A 3xx answer fails at once with the code redirected: Status is the 3xx, the message names where it pointed, and it is not retried, because the same BaseURL redirects the same way. If your BaseURL reached the service through a redirect (an http:// address upgraded to https://, or a www. rewrite in front), set it to the exact scheme and host the Credentials tab shows. GetRates sends no secret and may follow redirects.

Safe to log

An error never carries your API secret, or the credentials of a URL (user:pass@ in BaseURL or RatesBaseURL), so log.Printf("%v", err) and your logger can take it whole. Wherever the transport, a dropped connection, a redirect or the service's reply quotes either one, Message and Code say [redacted] instead, however it was encoded: as is, URL-encoded by any encoder in either hex case, form-encoded, JSON-escaped (\/, +), as HTML character references (&sol;, &#47;, &#x2F;), or any of those escaped once more (%252F, \&quot;). A key over 256 characters is not looked for: words at least as long as it are shown as [not shown: it could not be checked for the API key], and shorter words, such as error codes, are kept; a key holding a tab could be split across words, so then the whole message is withheld unless it is shorter than the key. A long message is cut only after that, so a cut cannot leave part of the secret behind.

The client object keeps its secrets out of sight too. String, GoString, Format, MarshalJSON and LogValue all give the same redacted view, so fmt.Printf with any verb, json.Marshal(client) and slog.Info("ready", "client", client) print voltkit.StreamClient{BaseURL: https://[redacted]@x.test, RatesBaseURL: unset, Timeout: 10s, Retries: 2} and never the key. The key, the URLs as given and the redactor live in closures, which reflection cannot read: a debugger's variable view or a reflection-based dumper sees four function addresses. A Config prints, marshals and logs the same way, but it is a plain struct whose APIKey field reflection can read, so keep a Config out of dumps. Requests still go to the URLs as given.

What a call returns is another matter: Token, HostToken and Publish.WHIPURL are credentials for one room or stream. Hand each to the one client that needs it, and never log them.

Webhooks

VoltKit signs every delivery: X-Stream-Signature: t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 over <timestamp>.<raw body> with your signing secret. ParseWebhook verifies and decodes in one step, so a handler cannot forget to check:

// The current secret, and the previous one during a rotation (see below).
secrets := []string{os.Getenv("VOLTKIT_RTC_WEBHOOK_SECRET"), os.Getenv("VOLTKIT_RTC_WEBHOOK_SECRET_OLD")}

mux.HandleFunc("POST /hooks/voltkit", func(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20)) // the raw bytes: the signature covers them exactly
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	event, err := voltkit.ParseWebhook(body, r.Header.Get("X-Stream-Signature"), secrets, nil)
	if err != nil {
		var e *voltkit.Error
		if errors.As(err, &e) {
			w.WriteHeader(http.StatusUnauthorized) // bad-signature: not from VoltKit, or too old
			return
		}
		log.Printf("webhook: %v", err) // a plain error: no secret, a negative tolerance; fix your setup
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusOK) // answer first
	queue <- event               // then do the work
})
  • Pass the raw body. A parsed and re-serialized body can never verify. Read r.Body yourself, before any JSON middleware; with a framework, use its raw-body accessor (chi and Gin hand you the same r.Body).
  • One header value. VoltKit sends the signature as one header, which r.Header.Get returns. If something in front of your server splits it into two header lines, pass strings.Join(r.Header.Values("X-Stream-Signature"), ",") instead: that is what the Node SDK does with a list.
  • What it checks. The timestamp must be within Tolerance of your clock (DefaultTolerance, five minutes, in either direction, inclusive), so a captured request cannot be replayed later, and a v1 signature must match one of your secrets, compared in constant time with hmac.Equal, every secret against every signature. Empty entries in secrets are skipped, so an unset VOLTKIT_RTC_WEBHOOK_SECRET_OLD is fine.
  • What it returns. The event, or an *Error of code bad-signature (Status 401) when the delivery does not verify: answer 401, and VoltKit retries it. A plain error (no secret at all, a negative Tolerance, a verified body that is not JSON) is a mistake in your setup.
  • VerifyWebhook takes the same arguments and returns true or false instead of the event; a mistake in the setup makes it panic rather than return a false that would look like an attack.
  • Options. &voltkit.VerifyOptions{Tolerance: voltkit.Ptr(10 * time.Minute)} widens the window; Now replaces the clock in tests.

The event is a StreamEvent: ID, Type, TenantID, RoomID, CreatedAt (unix milliseconds) and Data as json.RawMessage, whose shape depends on Type. Three things to get right on your side:

  • Answer first. A slow handler is retried, so your latency becomes duplicate deliveries. Retries come at 5 s, 30 s, 2 min, 10 min and 30 min; a 410 disables the endpoint.
  • Deduplicate on event.ID. Delivery is at-least-once by design; the id is stable across retries (also in the X-Stream-Delivery header, with X-Stream-Attempt).
  • Do not assume order. A participant.left can arrive before the participant.joined it followed; pair them by identity and joinedAt.

Every event type and its fields: https://voltkit.in/docs/webhooks. A complete receiver is in examples/webhookreceiver.

Rotating the signing secret

When you press Rotate secret with an overlap (24 hours by default), VoltKit signs every delivery with both the new secret and the previous one until the overlap ends: t=<ts>,v1=<new>,v1=<previous>. ParseWebhook and VerifyWebhook accept a delivery when any v1 value matches any secret you pass, so you can rotate without dropping a delivery:

  1. Deploy the handler above, which takes a list. With the old-secret variable unset, it verifies exactly as before.
  2. Press Rotate secret. Move the current secret to VOLTKIT_RTC_WEBHOOK_SECRET_OLD, put the new one in VOLTKIT_RTC_WEBHOOK_SECRET, and deploy before the overlap ends.
  3. After the overlap, unset VOLTKIT_RTC_WEBHOOK_SECRET_OLD.

Testing

Point the client at an httptest.Server that answers the way the service does: { "error": "<code>", "message": "..." } with the status, for each branch of your error handling. Retries: voltkit.Ptr(0) keeps a failing test fast.

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	calls = append(calls, r.Method+" "+r.URL.RequestURI())
	w.WriteHeader(http.StatusNotFound)
	_, _ = w.Write([]byte(`{"error":"no-room","message":"room not found"}`))
}))
defer srv.Close()

client, _ := voltkit.NewStreamClient(voltkit.Config{BaseURL: srv.URL, APIKey: "sk_test", Retries: voltkit.Ptr(0)})
_, err := client.CreateToken(ctx, voltkit.CreateTokenParams{RoomID: "show-42", Identity: "user-99", Role: voltkit.RoleViewer})
var e *voltkit.Error
ended := errors.As(err, &e) && e.IsNotFound() // true; calls: ["POST /v1/rooms/show-42/tokens"]

For a test with no listener at all, pass HTTPClient: &http.Client{Transport: rt} where rt is your http.RoundTripper. To test a webhook handler, sign a body yourself: hex(HMAC-SHA256(secret, "<t>." + body)) in the header t=<t>,v1=<hex>, and pass &voltkit.VerifyOptions{Now: ...} to pin the clock.

Versions

voltkit.Version is the module's version, and User-Agent: voltkit-rtc-go/<Version> goes on every request. This is 0.1.0, at parity with @voltkit-rtc/server 0.4.8; CHANGELOG.md lists what each release changes. Until 1.0, a breaking change is a minor bump and everything else a patch.

Help

Documentation: https://voltkit.in/docs/go-sdk. Questions and bug reports: contact@voltkit.in. Send the error's Code and Message, which never contain your secret, and never the secret itself.

Broadcast

Not offered on VoltKit cloud today. These methods are kept for parity with the API and the other server SDKs; on a deployment without a media server every one of them fails with broadcast-disabled (IsBroadcastDisabled).

A broadcast is one publisher and an audience of any size, served as LL-HLS through a CDN and over WebRTC.

live, err := client.CreateStream(ctx, voltkit.CreateStreamParams{StreamID: "show-42", Playback: voltkit.PlaybackPublic})
// live.StreamKey, live.State ("idle"), live.PlaybackURLs.LLHLS, live.PlaybackURLs.WebRTC,
// live.Publish.WHIPURL (carries the publish token: returned ONLY here; treat it like a password)

pub, err := client.CreateStreamToken(ctx, voltkit.CreateStreamTokenParams{StreamID: "show-42", Action: voltkit.ActionPublish}) // a fresh publish URL
play, err := client.CreateStreamToken(ctx, voltkit.CreateStreamTokenParams{StreamID: "show-42", Identity: "user-99", TTLSeconds: 3600}) // play (the default), for Playback token
// play.LLHLS, play.WebRTC, play.Token, play.ExpiresAt

s, err := client.GetStream(ctx, "show-42")                                                      // state, viewers, peakViewers
streams, err := client.ListStreams(ctx)                                                         // []BroadcastStream
s, err = client.UpdateStream(ctx, "show-42", voltkit.UpdateStreamParams{Playback: voltkit.PlaybackToken}) // who may watch, or metadata
err = client.DeleteStream(ctx, "show-42")                                                       // takes it off air; its URLs stop working

TTLSeconds is capped at 24 hours. Viewers counts the playback sessions the media server serves itself, not an audience behind a CDN. CreateStream is never retried, for the same reason as CreateRoom.

Documentation

Overview

Package voltkit is the VoltKit RTC server SDK for Go: rooms, tokens, chat, broadcast streams, usage, billing and verified webhooks, with the standard library only.

Your backend does two things: decide who may do what, and hand the client a short-lived token. It is never in the media path; there is no stream to proxy and nothing that scales with viewers.

client, err := voltkit.NewStreamClient(voltkit.Config{
	BaseURL: os.Getenv("VOLTKIT_RTC_URL"),    // from your project's Credentials tab
	APIKey:  os.Getenv("VOLTKIT_RTC_SECRET"), // server-side only, never sent to a client
})
if err != nil {
	log.Fatal(err) // a missing or malformed option, caught at startup
}

room, err := client.EnsureRoom(ctx, voltkit.EnsureRoomParams{RoomID: "show-42", HostIdentity: "user-7"})
tok, err := client.CreateToken(ctx, voltkit.CreateTokenParams{RoomID: "show-42", Identity: "user-99", Role: voltkit.RoleViewer})

Every method takes a context.Context first; cancelling it stops the call, retries included, and returns the context's error. A call that fails returns an *Error carrying the server's Code, the HTTP Status and whether it is Retryable, reachable with errors.As; its Message is always safe to log: the API key and any URL credentials are redacted before they can reach it. A mistake in your own setup (a bad Config, a body JSON cannot carry) is a plain error, so a handler cannot mistake it for an answer from VoltKit.

The API key is tenant-wide. It is sent to exactly one place, the controller named by Config.BaseURL, in the X-API-Key header: never across a redirect (a 3xx fails with the code redirected), never to the rates host, never into an error message or a printed client.

Transient failures (a network error, a 429, a 5xx other than billing-unavailable) are retried with jittered backoff on every call that is safe to repeat; CreateRoom, CreateStream and SendMessage are sent once, because a lost reply would turn a success into a spurious "already exists" or a duplicate message. The README says what to do then.

Webhooks: VerifyWebhook and ParseWebhook check X-Stream-Signature over the raw request body, with a freshness window and a list of secrets for a rotation. The broadcast methods are kept for parity with the API and are not offered on VoltKit cloud today.

Index

Constants

View Source
const CuPerCredit = 6_000_000

CuPerCredit is how many credit units (cu) one credit is. Every cu amount on the wire is a decimal string; see ParseCu.

View Source
const DefaultTolerance = 300 * time.Second

DefaultTolerance is how far out of date a delivery's timestamp may be, in either direction, when VerifyOptions.Tolerance is nil: five minutes. It guards against a captured request being replayed later.

View Source
const Version = "0.1.0"

Version is the version of this module, as published. It is the one place the number lives: the User-Agent header of every request carries it (voltkit-rtc-go/<Version>, spec section 8), and the repository's contract check compares it with docs/sdk-contract.json.

Variables

View Source
var ErrorCodes = func() []string {
	codes := make([]string, len(registry))
	for i, info := range registry {
		codes[i] = info.Code
	}
	return codes
}()

ErrorCodes is every code in the shared registry, in its order. Most of these only ever reach a client SDK on its socket; RESTErrorCodes are the ones a call from this SDK can fail with. Treat the slice as read-only.

View Source
var RESTErrorCodes = []string{
	"internal",
	"no-room",
	"viewer-quota",
	"quota-exhausted",
	"daily-limit-reached",
	"monthly-limit-reached",
	"tenant-suspended",
	"billing-unavailable",
	"bad-request",
	"rate-limited",
	"no-message",
	"unauthorized",
	"bad-signature",
	"operator-only",
	"bad-role",
	"room-exists",
	"room-quota",
	"no-nodes",
	"capacity-exhausted",
	"unavailable",
	"not-found",
	"broadcast-disabled",
	"chat-disabled",
	"no-stream",
	"stream-exists",
	"stream-quota",
	"network",
	"rates-unconfigured",
	"redirected",
}

RESTErrorCodes is the registry codes whose surfaces include the tenant REST API, in the registry's order: what a call from this SDK can fail with, besides http-<status> for a reply that names no code and, from GetRates, the website API's own codes (RATE_LIMITED, NOT_FOUND, ...), which are passed through unchanged. Treat the slice as read-only.

Functions

func ParseCu

func ParseCu(amount string) (*big.Int, error)

ParseCu reads a cu amount as the wire sends it ("186000000", a decimal string, never a number: a float would round a large balance) into a *big.Int for exact arithmetic. It refuses anything but an optional sign and decimal digits.

func Ptr

func Ptr[T any](v T) *T

Ptr returns a pointer to v: for the optional fields that distinguish "left out" from zero, Config.Retries (voltkit.Ptr(0) turns retries off) and VerifyOptions.Tolerance.

func VerifyWebhook

func VerifyWebhook(body []byte, header string, secrets []string, opts *VerifyOptions) bool

VerifyWebhook reports whether a delivery is genuine and fresh: body is the request body exactly as received (never re-serialized), header the X-Stream-Signature value, secrets the signing secret and, during a rotation, the previous one too. Every secret is tried against every v1 value, each compared in constant time, and one match is enough.

A mistake in the setup is loud, not a quiet false that would look like an attack: it panics when secrets is empty or holds only empty strings, or when Tolerance is negative, as the Node SDK throws a TypeError or RangeError. Use ParseWebhook to get those as errors instead.

Types

type Allowance

type Allowance struct {
	Plan                 string `json:"plan"`
	FreeDailyMinutes     int64  `json:"freeDailyMinutes"`
	FreeMonthlyMinutes   int64  `json:"freeMonthlyMinutes"`
	DailyCapMinutes      int64  `json:"dailyCapMinutes"`
	MonthlyCapMinutes    int64  `json:"monthlyCapMinutes"`
	UsedTodaySeconds     int64  `json:"usedTodaySeconds"`
	UsedMonthSeconds     int64  `json:"usedMonthSeconds"`
	FreeUsedTodaySeconds int64  `json:"freeUsedTodaySeconds"`
	FreeUsedMonthSeconds int64  `json:"freeUsedMonthSeconds"`
	FreeRemainingSeconds int64  `json:"freeRemainingSeconds"`
	PaidRemainingSeconds int64  `json:"paidRemainingSeconds"`
	RemainingSeconds     int64  `json:"remainingSeconds"`
	// Blocked is why the account cannot be used right now, if it cannot:
	// quota-exhausted, daily-limit-reached, monthly-limit-reached or
	// suspended.
	Blocked string `json:"blocked,omitempty"`
}

Allowance is where an account stands against every limit, in seconds; -1 means no limit.

type BroadcastStream

type BroadcastStream struct {
	StreamID string `json:"streamId"`
	TenantID string `json:"tenantId"`
	// StreamKey is the name the media server knows the stream by. Random, so
	// a playback URL cannot be derived from a stream id.
	StreamKey string `json:"streamKey"`
	// State is idle or live.
	State    string         `json:"state"`
	Playback PlaybackPolicy `json:"playback"`
	// CreatedAt, StartedAt and EndedAt are unix milliseconds.
	CreatedAt int64 `json:"createdAt"`
	StartedAt int64 `json:"startedAt,omitempty"`
	EndedAt   int64 `json:"endedAt,omitempty"`
	// Viewers counts the playback sessions the media server serves itself.
	// An audience behind a CDN is not in this number.
	Viewers      int             `json:"viewers"`
	PeakViewers  int             `json:"peakViewers"`
	Metadata     json.RawMessage `json:"metadata,omitempty"`
	PlaybackURLs PlaybackURLs    `json:"playbackUrls"`
}

BroadcastStream is a stream as the controller describes it.

type BroadcastStreamWithPublish

type BroadcastStreamWithPublish struct {
	BroadcastStream
	Publish PublishInfo `json:"publish"`
}

BroadcastStreamWithPublish is CreateStream's answer: the only reply that ever carries a publish token. Treat Publish like a password.

type BurnView

type BurnView struct {
	// LiveParticipants are joined participants across all the project's rooms.
	LiveParticipants int `json:"liveParticipants"`
	// ProjectCuPerSecond is cu per second for this project's rooms and
	// broadcasts; WalletCuPerSecond for every project drawing the same wallet.
	ProjectCuPerSecond string `json:"projectCuPerSecond"`
	WalletCuPerSecond  string `json:"walletCuPerSecond"`
}

BurnView is what is being spent right now.

type ChatInfo

type ChatInfo struct {
	Channel string `json:"channel"`
	// Members is how many identities are connected to the channel, in the
	// room or only in its chat.
	Members int `json:"members"`
}

ChatInfo is GetChatInfo's answer.

type ChatMessage

type ChatMessage struct {
	// ID is monotonic within a channel: the oldest id you hold is a paging
	// cursor for ListMessages.
	ID      string `json:"id"`
	Channel string `json:"channel"`
	From    string `json:"from"`
	// To is set on a direct message.
	To   string `json:"to,omitempty"`
	Type string `json:"type"`
	// Text is delivered exactly as sent. Render it as text, never as HTML.
	Text        string          `json:"text,omitempty"`
	Metadata    json.RawMessage `json:"metadata,omitempty"`
	ClientMsgID string          `json:"clientMsgId,omitempty"`
	// TS is unix milliseconds.
	TS int64 `json:"ts"`
}

ChatMessage is one stored message.

type ChatToken

type ChatToken struct {
	Token string `json:"token"`
	// WSURL is where a chat-only client connects. Ends in /ws/chat.
	WSURL    string `json:"wsUrl"`
	Role     Role   `json:"role"`
	Identity string `json:"identity"`
	Channel  string `json:"channel"`
	// RoomID is the room of the same name, when the wire names it.
	RoomID string `json:"roomId,omitempty"`
	// ExpiresAt is unix seconds.
	ExpiresAt int64 `json:"expiresAt"`
}

ChatToken is a chat-only token, from CreateChatToken.

type CodeInfo

type CodeInfo struct {
	// Code is the registry code, as Error.Code carries it.
	Code string
	// Origin is who raises it: the server, or an SDK itself.
	Origin Origin
	// Terminal concerns client SDKs: a join refused with this code must not
	// be retried with the same token. Nothing in this SDK acts on it.
	Terminal bool
	// Retryable is the registry's flag: trying the same call again later can
	// succeed. Error.Retryable on a failed call follows the HTTP status rule
	// of section 3.6 and agrees with this for every REST code.
	Retryable bool
}

CodeInfo is one entry of the shared error registry (docs/sdk-contract.json in the VoltKit RTC repository): what ErrorInfo returns.

func ErrorInfo

func ErrorInfo(code string) (CodeInfo, bool)

ErrorInfo looks a code up in the shared registry: its origin and its terminal and retryable flags. The second value is false for a code outside the registry, such as http-<status> or one a newer controller sends.

type Config

type Config struct {
	// BaseURL is the controller's origin, with its exact scheme and host:
	// https://api.voltkit.in for VoltKit cloud. The API key is sent here and
	// nowhere else. A trailing slash is dropped.
	BaseURL string
	// APIKey is the project API secret (sk_ and 64 hex digits). Server-side
	// only; whitespace around it is ignored. It is never quoted in an error.
	APIKey string
	// RatesBaseURL is the website API's origin (https://api.voltkit.in),
	// where GetRates reads the public price list with no credential. Only
	// GetRates needs it; "" and "/" mean unset.
	RatesBaseURL string
	// Timeout is the deadline for each attempt, from sending the request to
	// reading the last byte of the reply. Zero means 10 seconds; it must not
	// be negative. In whole milliseconds; above 2^31-1 ms it is held there.
	Timeout time.Duration
	// Retries is how many more attempts a call that is safe to repeat gets
	// after a transient failure. nil means 2; Ptr(0) turns retries off; it
	// must not be negative.
	Retries *int
	// HTTPClient is the transport: a client of your own for a proxy, custom
	// TLS or instrumentation. nil means a fresh http.Client on the default
	// transport. It is copied, never modified: the copy for the controller
	// never follows a redirect, the copy for the rates host keeps your
	// redirect policy, and both leave the deadline to the context.
	HTTPClient *http.Client
}

Config is what NewStreamClient needs. The zero value of an optional field means its default.

func (Config) Format

func (c Config) Format(f fmt.State, verb rune)

Format prints the same redacted view under every fmt verb. Without it, a verb that has no meaning for a struct (%d, %t, %c, %U) makes fmt print each field as it is, key included. Config is still a plain struct: a reflection-based dumper, or %p on a value rather than a pointer (which go vet flags), reads its fields. Keep a Config out of dumps.

func (Config) GoString

func (c Config) GoString() string

GoString is String: %#v shows no more than %v does.

func (Config) LogValue

func (c Config) LogValue() slog.Value

LogValue gives log/slog the redacted view as a group.

func (Config) MarshalJSON

func (c Config) MarshalJSON() ([]byte, error)

MarshalJSON serializes the redacted view, so a Config in a structured log line gives nothing away: zap, zerolog, logrus and slog's JSON handler all go through encoding/json.

func (Config) String

func (c Config) String() string

String prints the configuration with the API key and any URL credentials hidden, so a Config in a log line or a panic message gives nothing away.

type CreateChatTokenParams

type CreateChatTokenParams struct {
	Channel  string `json:"-"`
	Identity string `json:"identity"`
	Role     Role   `json:"role,omitempty"`
}

CreateChatTokenParams is CreateChatToken's argument. Role defaults to viewer.

type CreateRoomParams

type CreateRoomParams struct {
	// RoomID is your id, so webhooks read back usefully. Left out, the
	// server makes one up.
	RoomID       string `json:"roomId,omitempty"`
	HostIdentity string `json:"hostIdentity"`
	// Metadata is any JSON value: opaque, room-scoped state every participant
	// receives when they join.
	Metadata any `json:"metadata,omitempty"`
}

CreateRoomParams is CreateRoom's argument.

type CreateStreamParams

type CreateStreamParams struct {
	StreamID string         `json:"streamId"`
	Playback PlaybackPolicy `json:"playback,omitempty"`
	Metadata any            `json:"metadata,omitempty"`
}

CreateStreamParams is CreateStream's argument.

type CreateStreamTokenParams

type CreateStreamTokenParams struct {
	StreamID   string       `json:"-"`
	Action     StreamAction `json:"action,omitempty"`
	Identity   string       `json:"identity,omitempty"`
	TTLSeconds int          `json:"ttlSeconds,omitempty"`
}

CreateStreamTokenParams is CreateStreamToken's argument. Action defaults to play; TTLSeconds to the service's default and is capped at 24 hours.

type CreateTokenParams

type CreateTokenParams struct {
	RoomID   string `json:"-"`
	Identity string `json:"identity"`
	Role     Role   `json:"role"`
}

CreateTokenParams is CreateToken's argument. Role is required: your backend decides it, and the server refuses an unknown one with bad-role.

type DailyUsage

type DailyUsage struct {
	// Day is YYYY-MM-DD, UTC.
	Day string `json:"day"`
	Usage
}

DailyUsage is one UTC day of GetUsageSeries.

type EnsureRoomParams

type EnsureRoomParams struct {
	RoomID       string `json:"-"`
	HostIdentity string `json:"hostIdentity"`
	// Metadata is applied only when the room is created.
	Metadata any `json:"metadata,omitempty"`
}

EnsureRoomParams is EnsureRoom's argument.

type Error

type Error struct {
	// Code is the server's code (no-room, quota-exhausted, ...), network when
	// no reply arrived, http-<status> when a reply named no code, redirected,
	// rates-unconfigured, or bad-request for an id refused before sending. A
	// newer controller may send a code this release does not know; it is
	// passed through unchanged.
	Code string
	// Status is the HTTP status: 0 when nothing reached the server or the SDK
	// refused the call itself, the 3xx for redirected.
	Status int
	// Message reads "<METHOD> <path>: <what went wrong>", with the path as
	// sent (ids encoded, query string included).
	Message string
	// Retryable is true when trying again later could work: network, a 429, or
	// a 5xx other than billing-unavailable. On a call the SDK repeats it means
	// the retries have already been spent (section 4.1).
	Retryable bool
}

Error is what every failed call returns (the Node SDK's StreamError, spec section 5.1). Use errors.As to reach it:

var e *voltkit.Error
if errors.As(err, &e) && e.IsNotFound() { ... }

The controller answers errors as { "error": "<code>", "message": "<text>" } with a meaningful status. Collapsing those into a generic error loses the one thing a caller needs to decide what to do: capacity-exhausted means page someone and retry, room-quota means your own cleanup is behind, bad-request means fix the call. So the code is kept, and Message is always safe to log: the API key and any URL credentials are redacted before they can reach it.

func (*Error) Error

func (e *Error) Error() string

Error returns Message, so the generic error surface shows the method, the path and the server's words.

func (*Error) IsBillingUnavailable

func (e *Error) IsBillingUnavailable() bool

IsBillingUnavailable reports billing-unavailable: the project is billed from its wallet and no price list is active yet, or the controller has no wallet billing. An operator fixes this, not a retry, so although it is a 503 the SDK never retries it.

func (*Error) IsBroadcastDisabled

func (e *Error) IsBroadcastDisabled() bool

IsBroadcastDisabled reports broadcast-disabled: broadcast is not switched on for this deployment. Not something a retry fixes.

func (*Error) IsCapacityExhausted

func (e *Error) IsCapacityExhausted() bool

IsCapacityExhausted reports capacity-exhausted or no-nodes: the fleet is full. This is the one to alert on; it is about capacity, not correctness.

func (*Error) IsNotFound

func (e *Error) IsNotFound() bool

IsNotFound reports a 404: the room, stream or message is gone, or was never yours (rooms are tenant-scoped, so those are the same answer). broadcast-disabled and chat-disabled are 404s too: test IsBroadcastDisabled first when it matters.

func (*Error) IsOutOfMinutes

func (e *Error) IsOutOfMinutes() bool

IsOutOfMinutes reports that the account cannot use more right now: a 402, or the codes quota-exhausted, daily-limit-reached and monthly-limit-reached.

func (*Error) IsQuota

func (e *Error) IsQuota() bool

IsQuota reports room-quota or any 429 (rate-limited, viewer-quota and stream-quota included): slow down, or end rooms you no longer need.

type LegacyFunds

type LegacyFunds struct {
	// Unlimited: a plan whose own minutes never run out; OwnSeconds and
	// TotalSeconds are then -1.
	Unlimited bool `json:"unlimited"`
	// FreeSeconds is the free allowance left.
	FreeSeconds int64 `json:"freeSeconds"`
	// OwnSeconds is the project's own minutes; -1 when unlimited.
	OwnSeconds int64 `json:"ownSeconds"`
	// SharedSeconds is the account balance the project draws on, if any.
	SharedSeconds int64 `json:"sharedSeconds"`
	// TotalSeconds is free + own + shared; -1 when unlimited.
	TotalSeconds int64 `json:"totalSeconds"`
}

LegacyFunds are the minute pockets spent before the wallet, in participant-seconds.

type ListMessagesOptions

type ListMessagesOptions struct {
	Limit  int
	Before string
}

ListMessagesOptions pages through history. Both are optional: Limit defaults to 50 on the server and is capped at 100; Before is the oldest id you hold.

type Origin

type Origin string

Origin says who raises a registry code: the server (the controller or a media node, passed through unchanged by every SDK) or an SDK itself.

const (
	// OriginServer marks a code the controller or a media node sends.
	OriginServer Origin = "server"
	// OriginSDK marks a code an SDK raises on its own (network, redirected).
	OriginSDK Origin = "sdk"
)

type Participant

type Participant struct {
	Identity     string          `json:"identity"`
	Role         Role            `json:"role"`
	NodeID       string          `json:"nodeId"`
	HandRaised   bool            `json:"handRaised"`
	HandMetadata json.RawMessage `json:"handMetadata,omitempty"`
	Kicked       bool            `json:"kicked,omitempty"`
	// Muted and VideoBlocked are moderator state: a moderator switched the
	// mic or camera off and the participant may not switch it back.
	Muted        bool `json:"muted"`
	VideoBlocked bool `json:"videoBlocked"`
	// JoinedAt is unix milliseconds.
	JoinedAt int64 `json:"joinedAt"`
	// Metadata is their display metadata, from the token or their own
	// setMetadata.
	Metadata json.RawMessage `json:"metadata,omitempty"`
}

Participant is one connected identity in a room.

type PlaybackPolicy

type PlaybackPolicy string

PlaybackPolicy says who may watch a broadcast stream.

const (
	// PlaybackPublic: anyone with the playback URL. The default.
	PlaybackPublic PlaybackPolicy = "public"
	// PlaybackToken: each viewer needs URLs minted with CreateStreamToken.
	PlaybackToken PlaybackPolicy = "token"
)

type PlaybackURLs

type PlaybackURLs struct {
	LLHLS  string `json:"llhls"`
	WebRTC string `json:"webrtc"`
	Token  string `json:"token,omitempty"`
	// ExpiresAt is unix seconds, when a token is present.
	ExpiresAt int64 `json:"expiresAt,omitempty"`
}

PlaybackURLs is where a stream is watched.

type Producer

type Producer struct {
	ProducerID string `json:"producerId"`
	Identity   string `json:"identity"`
	// Kind is audio or video.
	Kind   string `json:"kind"`
	NodeID string `json:"nodeId"`
	Paused bool   `json:"paused"`
	// Source is mic or music for audio, camera or screen for video; empty
	// means mic or camera.
	Source string `json:"source,omitempty"`
}

Producer is one published track in a room, as GetRoom lists them.

type ProjectBalance

type ProjectBalance struct {
	TenantID string `json:"tenantId"`
	// BillingMode is legacy (flat minutes) or wallet (credits from a wallet).
	BillingMode string `json:"billingMode"`
	// CanJoin is whether a newcomer would be admitted now.
	CanJoin bool `json:"canJoin"`
	// Blocked is, when CanJoin is false, the code a join or a token mint is
	// refused with; nil otherwise.
	Blocked *string     `json:"blocked"`
	Legacy  LegacyFunds `json:"legacy"`
	// Wallet is nil unless the project is billed from its wallet.
	Wallet *WalletFunds `json:"wallet"`
	Burn   BurnView     `json:"burn"`
	// RemainingSeconds estimates how long the funds last at the current burn;
	// -1 when nothing is being spent or nothing limits it.
	RemainingSeconds int64          `json:"remainingSeconds"`
	WalletLow        WalletLowState `json:"walletLow"`
	// PricingVersion is the price list new wallet sessions lock; nil for a
	// legacy project, and while no price list is active.
	PricingVersion *int `json:"pricingVersion"`
	// UpdatedAt is unix milliseconds.
	UpdatedAt int64 `json:"updatedAt"`
}

ProjectBalance is GetBalance's answer: the project's spendable funds right now. Read only; recharges happen on the website.

type PublishInfo

type PublishInfo struct {
	WHIPURL string `json:"whipUrl"`
	Token   string `json:"token"`
	// ExpiresAt is unix seconds.
	ExpiresAt int64 `json:"expiresAt"`
}

PublishInfo is how an encoder publishes: WHIPURL already carries the token.

type ReceiveKbps

type ReceiveKbps struct {
	Video int `json:"video"`
	Audio int `json:"audio"`
	Total int `json:"total"`
}

ReceiveKbps is what one viewer receives at the top layer.

type Role

type Role string

Role is what a participant may do in a room. Your backend decides it; it is signed into the token and the client cannot change it.

const (
	RoleHost      Role = "host"
	RoleCohost    Role = "cohost"
	RoleModerator Role = "moderator"
	RoleViewer    Role = "viewer"
)

The four roles: a host owns the room, a cohost speaks beside them, a moderator keeps order, a viewer watches and chats.

type Room

type Room struct {
	RoomID       string `json:"roomId"`
	TenantID     string `json:"tenantId"`
	OriginNode   string `json:"originNode"`
	State        string `json:"state"`
	HostIdentity string `json:"hostIdentity"`
	// CreatedAt is unix milliseconds.
	CreatedAt int64 `json:"createdAt"`
	// Nodes are the media nodes the room spans.
	Nodes []string `json:"nodes"`
	// Participants is how many are connected right now.
	Participants int  `json:"participants"`
	Demo         bool `json:"demo,omitempty"`
	// Metadata is the opaque, room-scoped state given when the room was
	// created; every participant receives it on join.
	Metadata json.RawMessage `json:"metadata,omitempty"`
}

Room is a live room as the controller describes it.

type RoomList

type RoomList struct {
	Rooms []Room `json:"rooms"`
}

RoomList is ListRooms's answer. The wire wraps the list in an object (so a later release can add fields beside it), and so does this SDK, like every VoltKit server SDK.

type RoomToken

type RoomToken struct {
	Token    string `json:"token"`
	WSURL    string `json:"wsUrl"`
	Role     Role   `json:"role"`
	Identity string `json:"identity"`
	RoomID   string `json:"roomId"`
	// ExpiresAt is unix seconds. A token lasts ten minutes; a connected
	// client is refreshed by the controller.
	ExpiresAt int64 `json:"expiresAt"`
}

RoomToken is a room token for one identity, from CreateToken.

type RoomView

type RoomView struct {
	Room         Room          `json:"room"`
	Participants []Participant `json:"participants"`
	Producers    []Producer    `json:"producers"`
}

RoomView is GetRoom's answer: the authoritative live view.

type RoomWithHostToken

type RoomWithHostToken struct {
	Room
	// HostToken is a credential for this room: hand it to the host's client,
	// never log it.
	HostToken string `json:"hostToken"`
	WSURL     string `json:"wsUrl"`
	// ExpiresAt is unix seconds.
	ExpiresAt int64 `json:"expiresAt"`
	// Existed is true when the room already existed and was returned rather
	// than created (EnsureRoom answered 200, not 201). Always false from
	// CreateRoom.
	Existed bool `json:"existed"`
}

RoomWithHostToken is what CreateRoom and EnsureRoom return: the room and a fresh host token.

type RtcConversion

type RtcConversion struct {
	// CreditsPerRupee has four decimals; the exact rate is
	// Numerator / Denominator credits per rupee.
	CreditsPerRupee string `json:"creditsPerRupee"`
	Numerator       string `json:"numerator"`
	Denominator     string `json:"denominator"`
	InrPerCredit    string `json:"inrPerCredit"`
	// Version is nil while the default (1 rupee = 1 credit) applies;
	// EffectiveAt is ISO 8601, nil for the default.
	Version     *int    `json:"version"`
	EffectiveAt *string `json:"effectiveAt"`
	// Source is conversion-version or default.
	Source string `json:"source"`
}

RtcConversion is credits per rupee for new recharges, exactly and for display.

type RtcMode

type RtcMode struct {
	ProfileID string `json:"profileId"`
	Label     string `json:"label"`
	Tier      string `json:"tier"`
	TierLabel string `json:"tierLabel"`
	Enabled   bool   `json:"enabled"`
	// Codecs are MIME types, e.g. "video/VP8".
	Codecs      []string    `json:"codecs"`
	ReceiveKbps ReceiveKbps `json:"receiveKbps"`
	Kind        string      `json:"kind"`
	Channels    int         `json:"channels,omitempty"`
	Width       int         `json:"width,omitempty"`
	Height      int         `json:"height,omitempty"`
	FPS         int         `json:"fps,omitempty"`
	Layers      int         `json:"layers,omitempty"`
}

RtcMode is an enabled media mode: what it sends, and the tier it bills as. Kind is audio (Channels set) or video (Width, Height, FPS, Layers set).

type RtcRates

type RtcRates struct {
	// Version is the price list version. A wallet session keeps the version
	// it started under.
	Version int `json:"version"`
	// Kind is standard here: a contract list is one customer's negotiated
	// rates, never public.
	Kind string `json:"kind"`
	// EffectiveAt is when this version took effect, ISO 8601.
	EffectiveAt *string        `json:"effectiveAt"`
	Unit        RtcUnit        `json:"unit"`
	Tiers       []RtcTierRate  `json:"tiers"`
	Surcharges  []RtcSurcharge `json:"surcharges"`
	// IdleTier is what a participant who is only connected is billed as:
	// audio_std or none.
	IdleTier   string        `json:"idleTier"`
	Modes      []RtcMode     `json:"modes"`
	Conversion RtcConversion `json:"conversion"`
	// Notes are the billing rules in plain words, for a pricing page.
	Notes []string `json:"notes"`
}

RtcRates is the public price list, from GetRates. Prices only, never costs.

type RtcSurcharge

type RtcSurcharge struct {
	Tier   string `json:"tier"`
	Height int    `json:"height"`
	FPS    int    `json:"fps"`
	// Credits per 1,000 participant-minutes.
	Credits string `json:"credits"`
}

RtcSurcharge is the 60 fps premium at one resolution, over the video base.

type RtcTierRate

type RtcTierRate struct {
	// ID is stable (audio_std, video_30, video_720_60, ...): usage and the
	// ledger use it. Prices are what change, from version to version.
	ID string `json:"id"`
	// Rank: higher wins when a participant does several things at once.
	Rank  int    `json:"rank"`
	Label string `json:"label"`
	Media string `json:"media"`
	// Credits per 1,000 participant-minutes, e.g. "99.00".
	Credits string `json:"credits"`
	// CuPerParticipantSecond is the same price as an exact integer, e.g. "9900".
	CuPerParticipantSecond string `json:"cuPerParticipantSecond"`
	// Base is the family price a 60 fps surcharge is added to; Surcharge the
	// surcharge included in Credits, nil for a tier without one.
	Base      string  `json:"base"`
	Surcharge *string `json:"surcharge"`
	// Available: some enabled media mode bills at this tier.
	Available bool `json:"available"`
}

RtcTierRate is a billing tier. A participant is billed at one tier at a time: the highest media they send or receive.

type RtcUnit

type RtcUnit struct {
	Price       string `json:"price"`
	Rate        string `json:"rate"`
	CuPerCredit string `json:"cuPerCredit"`
}

RtcUnit names the units of a price list, and cu per credit ("6000000").

type SendMessageParams

type SendMessageParams struct {
	Channel string `json:"-"`
	Text    string `json:"text,omitempty"`
	// Type defaults to "text". Anything else is your own vocabulary.
	Type string `json:"type,omitempty"`
	// From is who it is from. Defaults to "system".
	From string `json:"from,omitempty"`
	// To makes it a direct message to one identity.
	To       string `json:"to,omitempty"`
	Metadata any    `json:"metadata,omitempty"`
	// Ephemeral delivers without storing: for reactions and anything not
	// worth replaying.
	Ephemeral bool `json:"ephemeral,omitempty"`
}

SendMessageParams is SendMessage's argument. A message needs Text or Metadata.

type StreamAction

type StreamAction string

StreamAction is what a stream token is for.

const (
	// ActionPlay: a viewer of a token-protected stream. The default.
	ActionPlay StreamAction = "play"
	// ActionPublish: an encoder whose publish URL has expired.
	ActionPublish StreamAction = "publish"
)

type StreamClient

type StreamClient struct {
	// contains filtered or unexported fields
}

StreamClient is the client for the tenant REST API: rooms, tokens, chat, broadcast, usage, balance and rates. Make one with NewStreamClient or NewStreamClientFromEnv when your server starts and share it: it is safe for concurrent use and holds no state outside a call.

func NewStreamClient

func NewStreamClient(cfg Config) (*StreamClient, error)

NewStreamClient makes a client. Every option is checked here, so a missing or malformed value fails at startup rather than on a user's first call, and none of these errors quotes the key.

func NewStreamClientFromEnv

func NewStreamClientFromEnv() (*StreamClient, error)

NewStreamClientFromEnv makes a client from the environment: VOLTKIT_RTC_URL (BaseURL), VOLTKIT_RTC_SECRET (APIKey) and, optionally, VOLTKIT_API_URL (RatesBaseURL). Nothing else is read; for a timeout or retries, use NewStreamClient with a Config. A missing variable is an error naming it.

func (*StreamClient) CreateChatToken

func (c *StreamClient) CreateChatToken(ctx context.Context, params CreateChatTokenParams) (*ChatToken, error)

CreateChatToken admits one of your users to a channel's chat, with no media rights: for someone who is not in the room, such as an audience watching a broadcast. People in the room already have chat on the socket they hold.

func (*StreamClient) CreateRoom

func (c *StreamClient) CreateRoom(ctx context.Context, params CreateRoomParams) (*RoomWithHostToken, error)

CreateRoom creates a room and fails with room-exists (409) if the id is taken. For "go live", prefer EnsureRoom: a host reconnecting after a dropped call hits the existing-room path, and this turns that into two round trips. Sent once, never retried: a lost reply would turn a success into a spurious room-exists (see the README, "Calls that are never repeated").

func (*StreamClient) CreateStream

CreateStream creates a stream. The reply's Publish.WHIPURL is complete: hand it to the encoder as it is. This is the only call that returns a publish token; reading or listing a stream never does. Sent once, never retried, for the reason CreateRoom is not.

func (*StreamClient) CreateStreamToken

func (c *StreamClient) CreateStreamToken(ctx context.Context, params CreateStreamTokenParams) (*StreamToken, error)

CreateStreamToken mints a playback token for a viewer of a token-protected stream, or a fresh publish token for an encoder whose URL has expired. The reply carries the URLs the token belongs in.

func (*StreamClient) CreateToken

func (c *StreamClient) CreateToken(ctx context.Context, params CreateTokenParams) (*RoomToken, error)

CreateToken authorises one of your users for a room. Your backend decides the role: it is signed into the token and the client cannot change it. Minting twice is harmless, so this is retried.

func (*StreamClient) DeleteMessage

func (c *StreamClient) DeleteMessage(ctx context.Context, channel, messageID string) error

DeleteMessage removes a message. Everyone who could see it is told to drop it.

func (*StreamClient) DeleteRoom

func (c *StreamClient) DeleteRoom(ctx context.Context, roomID string) error

DeleteRoom ends a room: everyone is disconnected and the room is gone. A retry that finds the room gone after a lost reply means the lost attempt ended it, and the call succeeds.

func (*StreamClient) DeleteStream

func (c *StreamClient) DeleteStream(ctx context.Context, streamID string) error

DeleteStream deletes a stream. Its publish and playback URLs stop working at once.

func (*StreamClient) EnsureRoom

func (c *StreamClient) EnsureRoom(ctx context.Context, params EnsureRoomParams) (*RoomWithHostToken, error)

EnsureRoom creates the room, or returns it with a fresh host token if it already exists: one round trip whether or not the host has been here before. Existed says which happened. Metadata is applied only on creation.

func (StreamClient) Format

func (c StreamClient) Format(f fmt.State, verb rune)

Format prints the same redacted view under every fmt verb. fmt consults String only for %v, %s, %q, %x and %X; for a verb that has no meaning for a struct (%t, %c, %U, %f) it walks the fields instead and prints what it finds with %v, which, before this method and the closures in secrets, included the key.

func (*StreamClient) GetBalance

func (c *StreamClient) GetBalance(ctx context.Context) (*ProjectBalance, error)

GetBalance is what the project can spend right now, and whether a newcomer would be admitted. Read only: recharges happen on the website, never through this API. Amounts are cu as decimal strings; use ParseCu for arithmetic.

func (*StreamClient) GetChatInfo

func (c *StreamClient) GetChatInfo(ctx context.Context, channel string) (*ChatInfo, error)

GetChatInfo says how many identities are connected to a channel, in the room or only in its chat.

func (*StreamClient) GetRates

func (c *StreamClient) GetRates(ctx context.Context) (*RtcRates, error)

GetRates is the public price list. It comes from the website API (Config.RatesBaseURL), where prices are set, rather than the controller, and carries no credential: it is public and on another host. Without RatesBaseURL it fails at once with rates-unconfigured and sends nothing.

func (*StreamClient) GetRoom

func (c *StreamClient) GetRoom(ctx context.Context, roomID string) (*RoomView, error)

GetRoom is the authoritative live view: the room, who is in it and what they publish. Poll it for a live count rather than adding up webhook joins and leaves.

func (*StreamClient) GetStream

func (c *StreamClient) GetStream(ctx context.Context, streamID string) (*BroadcastStream, error)

GetStream reads a stream: its state, viewers and playback URLs.

func (*StreamClient) GetUsage

func (c *StreamClient) GetUsage(ctx context.Context) (*UsageSummary, error)

GetUsage is the project's balance of minutes, and its usage for today, this month and lifetime.

func (*StreamClient) GetUsageSeries

func (c *StreamClient) GetUsageSeries(ctx context.Context, days int) ([]DailyUsage, error)

GetUsageSeries is one entry per UTC day, oldest first, ending today. Days with no usage are present and zero, so a chart drawn from it has no gaps. days of 0 means the default, 30; the server caps it at 90.

func (StreamClient) GoString

func (c StreamClient) GoString() string

GoString is String: %#v shows no more than %v does.

func (*StreamClient) ListChatMutes

func (c *StreamClient) ListChatMutes(ctx context.Context, channel string) ([]string, error)

ListChatMutes lists the identities currently muted in a channel's chat.

func (*StreamClient) ListMessages

func (c *StreamClient) ListMessages(ctx context.Context, channel string, opts *ListMessagesOptions) ([]ChatMessage, error)

ListMessages is the newest messages of a channel (50 by default, at most 100), oldest first, direct messages included: your backend owns the conversation and may need to moderate it. Pass the oldest id you hold as opts.Before to page back; opts may be nil.

func (*StreamClient) ListParticipants

func (c *StreamClient) ListParticipants(ctx context.Context, roomID string) ([]Participant, error)

ListParticipants is a point-in-time read of who is in a room, for an admin screen or a reconciliation job. It is not a presence feed: for that, take the webhooks. An empty room is an empty, non-nil slice.

func (*StreamClient) ListRooms

func (c *StreamClient) ListRooms(ctx context.Context) (*RoomList, error)

ListRooms lists every live room of the project.

func (*StreamClient) ListStreams

func (c *StreamClient) ListStreams(ctx context.Context) ([]BroadcastStream, error)

ListStreams lists the project's streams.

func (StreamClient) LogValue

func (c StreamClient) LogValue() slog.Value

LogValue gives log/slog the redacted view as a group.

func (StreamClient) MarshalJSON

func (c StreamClient) MarshalJSON() ([]byte, error)

MarshalJSON serializes the same redacted view, so a client that ends up in a JSON log line or a debug dump gives nothing away.

func (*StreamClient) MuteChat

func (c *StreamClient) MuteChat(ctx context.Context, channel, identity string) error

MuteChat silences an identity in a channel's chat. Held for the channel, not the connection, so reconnecting does not undo it. They can still read.

func (*StreamClient) SendMessage

func (c *StreamClient) SendMessage(ctx context.Context, params SendMessageParams) (*ChatMessage, error)

SendMessage speaks from your backend: a system notice, a gift announcement, a warning. It skips the per-socket rate limit and the mute list, both of which exist to restrain clients. Sent once, never retried: a lost reply would become the same message twice.

func (StreamClient) String

func (c StreamClient) String() string

String prints the client with URL credentials as [redacted] and no key: what %v and %+v show. Defined on the value, so a copied or dereferenced client prints the same way.

func (*StreamClient) UnmuteChat

func (c *StreamClient) UnmuteChat(ctx context.Context, channel, identity string) error

UnmuteChat lifts a mute.

func (*StreamClient) UpdateStream

func (c *StreamClient) UpdateStream(ctx context.Context, streamID string, patch UpdateStreamParams) (*BroadcastStream, error)

UpdateStream changes who may watch, or the stream's metadata.

type StreamEvent

type StreamEvent struct {
	// ID is stable across redeliveries: deduplicate on it.
	ID       string `json:"id"`
	Type     string `json:"type"`
	TenantID string `json:"tenantId"`
	RoomID   string `json:"roomId,omitempty"`
	// CreatedAt is unix milliseconds.
	CreatedAt int64           `json:"createdAt"`
	Data      json.RawMessage `json:"data,omitempty"`
}

StreamEvent is one webhook delivery. Data's shape varies by Type; see https://voltkit.in/docs/webhooks.

func ParseWebhook

func ParseWebhook(body []byte, header string, secrets []string, opts *VerifyOptions) (*StreamEvent, error)

ParseWebhook verifies a delivery and decodes it in one step, so a handler cannot forget to check. A delivery that does not verify fails with an *Error of code bad-signature and status 401: answer 401, and VoltKit retries it. A mistake in the setup (no secret, a negative Tolerance, a verified body that is not JSON) is a plain error, never an *Error.

type StreamToken

type StreamToken struct {
	StreamID string       `json:"streamId"`
	Action   StreamAction `json:"action"`
	Token    string       `json:"token"`
	// ExpiresAt is unix seconds.
	ExpiresAt int64  `json:"expiresAt"`
	WHIPURL   string `json:"whipUrl,omitempty"`
	LLHLS     string `json:"llhls,omitempty"`
	WebRTC    string `json:"webrtc,omitempty"`
}

StreamToken is CreateStreamToken's answer: WHIPURL for publish, LLHLS and WebRTC for play.

type UpdateStreamParams

type UpdateStreamParams struct {
	Playback PlaybackPolicy `json:"playback,omitempty"`
	Metadata any            `json:"metadata,omitempty"`
}

UpdateStreamParams is UpdateStream's patch: who may watch, or the metadata.

type Usage

type Usage struct {
	ParticipantSeconds     int64 `json:"participantSeconds"`
	PublisherSeconds       int64 `json:"publisherSeconds"`
	RoomSeconds            int64 `json:"roomSeconds"`
	RoomsCreated           int64 `json:"roomsCreated"`
	PeakParticipants       int64 `json:"peakParticipants"`
	ChatMessages           int64 `json:"chatMessages"`
	ChatConnectionSeconds  int64 `json:"chatConnectionSeconds"`
	BroadcastSeconds       int64 `json:"broadcastSeconds"`
	BroadcastViewerSeconds int64 `json:"broadcastViewerSeconds"`
	StreamsStarted         int64 `json:"streamsStarted"`
	// BillableSeconds is everything that counts against an allowance: room
	// plus broadcast seconds. FreeSeconds is the part a free allowance
	// covered. Both are absent on older controllers.
	BillableSeconds int64 `json:"billableSeconds,omitempty"`
	FreeSeconds     int64 `json:"freeSeconds,omitempty"`
}

Usage is one period's counters.

type UsageSummary

type UsageSummary struct {
	TenantID string `json:"tenantId"`
	// Plan is metered or unlimited; Status active or suspended.
	Plan              string `json:"plan"`
	Status            string `json:"status"`
	MinutesLimit      int64  `json:"minutesLimit"`
	UsedMinutes       int64  `json:"usedMinutes"`
	SharedUsedMinutes int64  `json:"sharedUsedMinutes"`
	// MinutesRemaining is -1 on the unlimited plan.
	MinutesRemaining int64 `json:"minutesRemaining"`
	Today            Usage `json:"today"`
	Month            Usage `json:"month"`
	Lifetime         Usage `json:"lifetime"`
	// Allowance is free allowances, paid balance and caps, right now.
	Allowance *Allowance `json:"allowance,omitempty"`
}

UsageSummary is GetUsage's answer.

type VerifyOptions

type VerifyOptions struct {
	// Tolerance is the freshness window, inclusive at the boundary. nil means
	// DefaultTolerance; Ptr(0) accepts only the current second; a negative
	// duration is a mistake in your setup (it would refuse every delivery).
	Tolerance *time.Duration
	// Now is the clock; nil means time.Now. For tests.
	Now func() time.Time
}

VerifyOptions tunes VerifyWebhook and ParseWebhook. A nil *VerifyOptions is the defaults.

type WalletFunds

type WalletFunds struct {
	// SpendOrder is the scopes in the order they are spent:
	// project:<tenantId>, product:rtc, unallocated.
	SpendOrder []string                     `json:"spendOrder"`
	Scopes     map[string]WalletScopeAmount `json:"scopes"`
	PaidCu     string                       `json:"paidCu"`
	PromoCu    string                       `json:"promoCu"`
	// AvailableCu is PaidCu + PromoCu: everything this project may spend from
	// the wallet now.
	AvailableCu string `json:"availableCu"`
	// AvailableCredits is AvailableCu in credits, at most four decimals,
	// rounded down ("31", "10.62"). For display only.
	AvailableCredits string `json:"availableCredits"`
}

WalletFunds is the wallet credits this project may spend. Only its own scopes are listed: another project's allocation is never shown.

type WalletLowState

type WalletLowState struct {
	// Active: the project's rooms are being warned now.
	Active bool `json:"active"`
	// GraceSeconds: the warning starts when what is left covers less than
	// this many seconds at the current burn.
	GraceSeconds int64 `json:"graceSeconds"`
}

WalletLowState is the low-funds warning rooms get before their funds run out.

type WalletScopeAmount

type WalletScopeAmount struct {
	PaidCu  string `json:"paidCu"`
	PromoCu string `json:"promoCu"`
}

WalletScopeAmount is the credits in one wallet scope, by origin. Promo is spent before paid.

Directories

Path Synopsis
examples
tokenserver command
Command tokenserver is a minimal token endpoint on net/http: a client asks to join a room, this server decides the role and mints a short-lived room token with the VoltKit RTC server SDK.
Command tokenserver is a minimal token endpoint on net/http: a client asks to join a room, this server decides the role and mints a short-lived room token with the VoltKit RTC server SDK.
webhookreceiver command
Command webhookreceiver receives VoltKit RTC webhooks on net/http: it reads the raw body, verifies the signature with ParseWebhook against one or more secrets (so a rotation drops no delivery), answers 401 when a delivery does not verify, answers 200 before doing any work, and deduplicates on the event id, because delivery is at least once.
Command webhookreceiver receives VoltKit RTC webhooks on net/http: it reads the raw body, verifies the signature with ParseWebhook against one or more secrets (so a rotation drops no delivery), answers 401 when a delivery does not verify, answers 200 before doing any work, and deduplicates on the event id, because delivery is at least once.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL