saved.sh

SDK

A typed client over the REST API, for building backups into your own software.

The SDK is a thin, typed client over the REST API. It adds types, retries and sane defaults; it does not add capabilities, and there is nothing it can do that a bare HTTP call cannot.

Go is the reference implementation, because the worker that runs your backups is written in Go and uses this same client. It cannot drift from what the product actually does without the worker breaking first.

Install

go get github.com/savedhq/saved-go

Construct a client

import "github.com/savedhq/saved-go"

client := saved.New(saved.Config{
	APIKey:  os.Getenv("SAVED_API_KEY"),
	BaseURL: os.Getenv("SAVED_API_URL"), // https://api.saved.sh/v1
	Timeout: 30 * time.Second,
})
OptionNotes
APIKeyThe only credential. Read it from the environment or a secret store, never hardcode it
BaseURLEnvironment-specific. A key issued for one environment does not work against another
TimeoutPer request
Retry, HTTPClient, UserAgentOverridable

Environments are fully isolated. A key minted against the test environment will not authenticate against production, and vice versa. This is deliberate, not a limitation.

The surface

Every method maps to one endpoint and requires the same permission that endpoint does. A call without it returns 403.

GroupMethodPermission
WorkspacesWorkspaces.List / GetHuman session surface
BackupsBackups.List(ctx, wid)backups:read
Backups.Get(ctx, bid)backups:read
Backups.Create(ctx, wid, name)backups:write
Backups.Configure(ctx, bid, spec)backups:write
Backups.Trigger(ctx, bid)backups:write
RunsRuns.Create(ctx, bid)artifacts:upload
Runs.UploadURL(ctx, runID)artifacts:upload
Runs.Confirm(ctx, runID, ...)artifacts:confirm
Runs.Get(ctx, runID)backups:read
ArtifactsArtifacts.List(ctx, wid)artifacts:read
Artifacts.DownloadURL(ctx, aid)artifacts:download
Artifacts.Delete(ctx, aid)artifacts:delete

Trigger a backup from your application

run, err := client.Backups.Trigger(ctx, backupID)
if err != nil {
	return fmt.Errorf("trigger: %w", err)
}
log.Printf("run %s started", run.ID)

Upload something you produced yourself

This is the manual path: no source, no worker, no schedule. You produce the bytes, we store them under the same retention and protection rules as anything else.

run, err := client.Runs.Create(ctx, backupID)
if err != nil {
	return err
}

url, err := client.Runs.UploadURL(ctx, run.ID)
if err != nil {
	return err
}

// PUT the bytes straight to object storage; they do not pass through the API.
if err := putFile(ctx, url, "./export.tar.gz.gpg"); err != nil {
	return err
}

_, err = client.Runs.Confirm(ctx, run.ID, saved.Confirm{
	SizeBytes: size,
	Checksum:  sum,
})

Encrypt before you upload. The manual path stores exactly the bytes you give it, so anything you have not encrypted yourself arrives as plaintext. The other paths encrypt on your machine automatically.

Errors and retries

Errors carry the API's stable code, so branch on that rather than on a message:

var apiErr *saved.Error
if errors.As(err, &apiErr) && apiErr.Code == "quota_exceeded" {
	// raise a limit, or delete something
}

The client retries idempotent requests on 429 and 5xx with exponential backoff and honours Retry-After. It does not retry 4xx other than 429: a refused permission or a rejected body will be refused identically the second time.

Restore is not here

There is no Restore method, and that is a design decision rather than a gap. A restore is Artifacts.DownloadURL followed by decryption with a key we have never held. Putting a Restore() in the SDK would imply we could perform one, and we cannot.

On this page