A Go client for fal.ai, a 1:1 behavioral port of the official
Python client (fal-client). Same wire
protocol, same method surface, idiomatic Go ergonomics: one *fal.Client,
context.Context on every call, functional options, and iter.Seq2 iterators.
Requires Go 1.26+.
go get github.com/valksor/fal-goCreate a key in the fal dashboard and expose it
as FAL_KEY. Prefer a .env file or direnv over an inline
FAL_KEY=… go run … (which lands in shell history):
echo 'FAL_KEY=your-key' > .envCredentials are resolved in this order: explicit WithKey, FAL_KEY,
FAL_KEY_ID + FAL_KEY_SECRET, then the ~/.fal/auth0_token login token
(auto-refreshed).
ctx := context.Background()
result, err := fal.Subscribe(ctx, "fal-ai/fast-sdxl",
map[string]any{"prompt": "a cat riding a bicycle"},
fal.WithLogs(true),
fal.OnQueueUpdate(func(s fal.Status) {
if q, ok := s.(fal.Queued); ok {
log.Printf("queued at %d", q.Position)
}
}),
)The package-level functions (fal.Subscribe, fal.Run, ...) delegate to a shared
default client. Construct your own with fal.New(...) for custom keys, HTTP
clients, or timeouts.
API results are dynamic JSON, returned as any (mirroring Python). Decode into a
typed struct with the generic helper:
out, err := fal.As[MyResult](result)Status is a sealed interface; the only cases are Queued, InProgress and
Completed:
switch v := s.(type) {
case fal.Queued: // v.Position
case fal.InProgress: // v.Logs
case fal.Completed: // v.Metrics, v.Error
}Stream returns an iter.Seq2[map[string]any, error]. Break on error — the
error is yielded as the final pair, and breaking closes the connection:
for event, err := range c.Stream(ctx, "fal-ai/some-app", input) {
if err != nil {
log.Printf("stream error: %v", err)
break
}
fmt.Println(event)
}Upload, UploadFile and UploadImage return a public URL. The default backend
chain is the fal CDN (fal_v3) with a GCS (fal) fallback — i.e.
["fal_v3", "fal"], matching the Python client — and multipart for files over
100 MB. Configure the chain with WithRepository / WithFallbackRepository
(valid values: "fal_v3" CDN, "fal" GCS; "cdn" is a deprecated alias for
"fal_v3"). The next backend is tried on any failure of the previous one;
WithFallbackRepository() with no arguments disables the default fallback. Large
uploads routed to fal_v3 go multipart and are returned directly without falling
back.
url, err := c.UploadFile(ctx, "input.png")Realtime opens a msgpack WebSocket connection; WSConnect is the lower-level
raw-connection escape hatch.
conn, err := c.Realtime(ctx, "fal-ai/some-app")
defer conn.Close()
conn.Send(ctx, map[string]any{"prompt": "hi"})
msg, err := conn.Recv(ctx)Security note: by default the realtime JWT is placed in the WebSocket URL query string (matching the Python client), which intermediaries may log. Use
WithJWT(false)to send it as anAuthorizationheader instead.
Subscribe and Run are the primary paths. For finer control, Submit returns a
*RequestHandle you can poll yourself (handle.Status, handle.IterEvents,
handle.Get, handle.Cancel). GetHandle(application, requestID) reconstructs a
handle for a request you already have an id for.
| Type | Meaning |
|---|---|
*fal.Error |
generic client error |
*fal.HTTPError |
non-2xx API response (StatusCode, ErrorType; headers redacted) |
*fal.TimeoutError |
Subscribe exceeded its client timeout |
*fal.MissingCredentialsError |
no credentials could be resolved |
*fal.RealtimeError |
server error frame on a realtime connection |
Inspect with errors.As.
The top-level fal package is the entry point — construct a *fal.Client (or use
the package-level functions) and call its flat methods. Everything it returns and
accepts is re-exported here, so importing github.com/valksor/fal-go alone is
enough for normal use.
The implementation is organized into focused, public sub-packages that share one HTTP core; import them directly only if you want a narrower dependency:
| Package | Responsibility |
|---|---|
transport |
HTTP engine: request building, retry/backoff, CDN token cache |
auth |
credential resolution (key, key-id+secret, login-token refresh) |
option |
functional options (WithX/OnX) and the resolved CallOptions |
run, queue, stream, upload, realtime |
the feature operations |
appid, status, storage, encode, errs |
leaf types and helpers (errs is so named to avoid shadowing the stdlib errors) |
The fal.* names (fal.Status, fal.RequestHandle, fal.WithLogs, …) are
aliases/re-exports of these packages' symbols, so they stay interchangeable —
e.g. errors.As(err, new(*fal.HTTPError)) matches an error built inside
transport.
The stable, supported surface is the top-level fal package. The sub-packages
are public for narrower dependencies, but a few of their symbols are plumbing
rather than API: build a client only with fal.New (do not construct
transport.Core yourself), and treat option.CallOptions as an internal
resolution type — configure calls with the fal.WithX options, not by building a
CallOptions directly.
| Python | Go |
|---|---|
fal_client.run(app, args) |
fal.Run(ctx, app, args) |
fal_client.submit(app, args) |
fal.Submit(ctx, app, args) |
fal_client.subscribe(app, args, on_queue_update=fn) |
fal.Subscribe(ctx, app, args, fal.OnQueueUpdate(fn)) |
fal_client.status(app, id, with_logs=True) |
fal.StatusOf(ctx, app, id, true) |
fal_client.result(app, id) |
fal.Result(ctx, app, id) |
fal_client.cancel(app, id) |
fal.Cancel(ctx, app, id) |
fal_client.stream(app, args) |
for ev, err := range fal.Stream(ctx, app, args) |
fal_client.upload_file(path) |
fal.UploadFile(ctx, path) |
fal_client.realtime(app) |
fal.Realtime(ctx, app) |
keyword args (path=, hint=, ...) |
options (fal.WithPath, fal.WithHint, ...) |
Queued / InProgress / Completed |
same, as a sealed Status interface |
The module-level status() is renamed StatusOf in Go because Status is also
the result type name.