Drıftstack docs
Docs

Go quickstart

A five-minute path to a working Go Driftstack session. For the multi-language overview see the combined quickstart.

Prerequisites

  • Go 1.22+ (the SDK uses generic constraints + slices package).
  • The Go module at v0.7.0 or newer. The examples on this page are written against it.
  • A paid plan: these steps use the AI agent, which the free trial does not include.
  • A ds_live_… customer API key. Mint one at app.driftstack.io/api-keys.
  • The id of one of your saved proxies: every agent session goes out through a proxy you chose. See Find a proxy’s id.
  • A way to pay for the AI: your plan’s monthly AI credits, or your own Anthropic key. See Who pays for the AI.

1. Install

go get github.com/driftstackdev/driftstack-go@latest

The Go SDK is published as a tagged pre-1.0 module. Commit the resulting go.mod and go.sum so production builds remain reproducible.

Moving from the old import path? A program that imports github.com/driftstackdev/driftstack-api/packages/sdk-go keeps building: the module proxy serves the versions it already has of that path (v0.4.0 and earlier). New versions land only on github.com/driftstackdev/driftstack-go. To move, change the import path, run go get github.com/driftstackdev/driftstack-go@latest and go mod tidy. The package name is still driftstack, so an import line changes only its path. The API does not carry over unchanged, though: the move also takes you from v0.4.0 to v0.5.0, a breaking release. Nine resources left the module for the dashboard — APIKeys, Auth, Mfa, Billing, CryptoOrders, Team, Legal, AuditLog and EmailPreferences — along with Profiles.Transfer and the desktop-app controls on AgentSessions (SetMode, Takeover, Handback, SendInputEvent); the compiler flags every call to those. It cannot flag the one behaviour change: AgentSessions.Create now requires ProxyID, and a create without one is refused at run time. Read Upgrading from v0.4.0 in the module’s CHANGELOG before you move.

2. Configure the client

package main

import (
    "context"
    "os"

    driftstack "github.com/driftstackdev/driftstack-go"
)

func main() {
    client := driftstack.New(os.Getenv("DRIFTSTACK_API_KEY"))
    defer client.Close()

    ctx := context.Background()
    _ = ctx
    _ = client
}

driftstack.New returns *Client. The constructor accepts options:

client := driftstack.New(
    os.Getenv("DRIFTSTACK_API_KEY"),
    driftstack.WithBaseURL("https://api.driftstack.dev"), // the default; set it only to pin the host
    driftstack.WithHTTPClient(myInstrumentedHTTPClient), // OpenTelemetry, retries, etc.
)

Close() releases the underlying http.Transport connection pool. Call once at process shutdown; idiomatic Go is defer client.Close() in main.

3. Run your first session

An agent session is an iPhone Safari browser in the cloud that an AI drives for you: you send a task in plain words, and the agent plans the taps and page loads, runs them, and answers. Every agent session goes out through one of your saved proxies, so the create names its id (proxy_id).

package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "time"

    driftstack "github.com/driftstackdev/driftstack-go"
)

func main() {
    client := driftstack.New(os.Getenv("DRIFTSTACK_API_KEY"))
    defer client.Close()
    ctx := context.Background()

    agent, err := client.AgentSessions.Create(ctx, &driftstack.CreateAgentSessionRequest{
        Mode:    "ai",
        ProxyID: os.Getenv("DRIFTSTACK_PROXY_ID"),
    }, nil)
    if err != nil {
        log.Fatal(err)
    }
    // Always close it: an open session counts against your plan's
    // concurrent-session limit.
    defer func() {
        if err := client.AgentSessions.Close(context.Background(), agent.ID); err != nil {
            log.Printf("close failed: %v", err)
        }
    }()

    // Wait up to two minutes for its browser: Status is "active" from the
    // start, so read IsReady.
    for i := 0; i < 60; i++ {
        s, err := client.AgentSessions.Get(ctx, agent.ID)
        if err != nil {
            log.Fatal(err)
        }
        if s.Status == "closed" {
            reason := "unknown"
            if s.ClosedReason != nil {
                reason = *s.ClosedReason
            }
            log.Fatal("The session did not start: " + reason)
        }
        if s.IsReady() {
            break
        }
        time.Sleep(2 * time.Second)
    }
    reply, err := client.AgentSessions.Message(ctx, agent.ID,
        "Open https://example.com and tell me the page title.",
        &driftstack.MessageOptions{IdempotencyKey: "task-" + time.Now().Format(time.RFC3339Nano)})
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(reply.Kind, reply.Answer)
}

The guide Run AI tasks from your code covers the rest a program needs: answering the agent’s questions, approving a payment it held, stopping a task that runs too long, safe retries, and limits.

4. Error handling

Each problem-type maps to a typed error class. Match with errors.As for the granular case, or errors.Is against a sentinel for category-only matching:

import (
    "errors"
    "log"
    "time"
)

if _, err := client.AgentSessions.Create(ctx, req, nil); err != nil {
    var pr *driftstack.ProxyRequiredError
    var rl *driftstack.RateLimitError
    var cl *driftstack.ConcurrencyLimitError
    var qe *driftstack.QuotaExceededError
    var forbidden *driftstack.ForbiddenError
    var other *driftstack.UnknownError
    switch {
    case errors.As(err, &pr):
        // No ProxyID: nothing was created. Pass the id of one of your saved
        // proxies.
        log.Print(pr.Message)
    case errors.As(err, &rl):
        time.Sleep(time.Duration(rl.RetryAfterSeconds) * time.Second)
        // … retry
    case errors.As(err, &cl):
        log.Printf("concurrent cap reached (%d/%d)", cl.CurrentSessions, cl.Limit)
    case errors.As(err, &qe):
        log.Printf("tier limit reached (current=%d/limit=%d)", qe.Current, qe.Limit)
    case errors.As(err, &other) && other.Status == 402:
        // The free trial has ended, or a subscription lapsed: the account keeps
        // its data, and sessions start again once it chooses a plan.
        log.Print(other.Message)
    case errors.As(err, &forbidden):
        log.Printf("forbidden: %s", forbidden.Message)
    case errors.Is(err, driftstack.ErrAuth):
        log.Print("bad API key")
    default:
        log.Fatal(err) // Network / parse / unrecoverable.
    }
}

The full mapping from problem-type to Go error class lives at /reference/errors. For low-level cases the errors also satisfy errors.As to a shared payload carrying .Status, .ProblemType, and a .Problem map[string]any with the unmapped extension fields.

5. Webhooks (optional)

import driftstack "github.com/driftstackdev/driftstack-go"

ok := driftstack.VerifyWebhookSignature(
    rawBody,
    r.Header.Get("X-Driftstack-Signature"),
    os.Getenv("DRIFTSTACK_WEBHOOK_SECRET"),
    driftstack.VerifyWebhookOptions{},
)
if !ok {
    http.Error(w, "invalid signature", http.StatusUnauthorized)
    return
}

During the 24h signing-secret rotation grace window the single X-Driftstack-Signature header carries both the new and old HMACs as two v1= entries (t=…,v1=<new>,v1=<old>) — see /webhooks/endpoints for the rotate-secret endpoint. VerifyWebhookSignature already checks every v1= entry in that header, so the call above keeps verifying through a rotation without setting any option while you roll the new secret across your verifier infra.

Next steps

Stuck? Email [email protected] with your account id (acc_…, from client.Account.Whoami(ctx)) and the failing x-request-id.