> ## Documentation Index
> Fetch the complete documentation index at: https://code.storage/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Pierre SDK

> Install the Pierre SDK for TypeScript, Python, or Go, set its credentials, and use the options, responses, pages, errors, and local helpers that apply to every call.

The Pierre SDK has one package for each language: `@pierre/sdk` for TypeScript and JavaScript,
`pierre-sdk` for Python, and `github.com/pierrecomputer/sdk/go` for Go. This page explains what
applies to every call. The reference page of each operation shows the call of that operation in each
language.

> [!NOTE]
>
> The earlier Code Storage SDKs, `@pierre/storage`, `pierre-storage`, and `code-storage-go`, have
> their own docs: [Storage SDK](https://code.storage/docs/sdk/overview.md). To move code to this SDK, use the [`pierre-migrate`
> skill](https://github.com/pierrecomputer/skills/tree/main/skills/pierre-migrate).

## Install

```bash TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
# An ES module for Node.js ^22.23.4 || ^24.22.1 || >=26.12.1,
# a browser, or an edge runtime
npm install @pierre/sdk
```

```bash Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
# Python 3.10 or later. It depends on httpx and cryptography.
# Import it as pierre.
pip install pierre-sdk
```

```bash Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
# Go 1.25 or later.
# Its packages are pierre, option, auth, git, and webhooks.
go get github.com/pierrecomputer/sdk/go
```

The TypeScript samples in these docs use `import` and top-level `await`. In a Node.js project, set
`"type": "module"` in your `package.json`, or use `.mts` files:

```json package.json theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
{
  "type": "module"
}
```

## Create a client

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import { Pierre } from '@pierre/sdk';

const pierre = new Pierre(); // reads PIERRE_ORG and PIERRE_PRIVATE_KEY
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
from pierre import Pierre

pierre = Pierre()  # reads PIERRE_ORG and PIERRE_PRIVATE_KEY
```

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import (
	"context"
	"fmt"

	pierre "github.com/pierrecomputer/sdk/go"
)

ctx := context.Background()
client, err := pierre.NewClient() // reads PIERRE_ORG and PIERRE_PRIVATE_KEY
if err != nil {
	return err
}
```

The client reads its settings from these environment variables. An empty variable counts as not set.

```bash theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
# Your organization name. With a token and no PIERRE_ORG,
# the client reads it from the iss claim of the token.
export PIERRE_ORG="acme"

# One credential: a PKCS #8 PEM private key,
# the path of a PEM file, or a fixed token.
export PIERRE_PRIVATE_KEY="$(cat pierre-key.pem)"
# export PIERRE_PRIVATE_KEY_FILE="/run/secrets/pierre-key.pem"
# export PIERRE_TOKEN="<token>"

# Only for hosts other than https://api.{org}.code.storage
# and {org}.code.storage:
# export PIERRE_API_BASE_URL="https://api.example.com"
# export PIERRE_GIT_HOST="git.example.com"
```

With a private key, the client signs a 5-minute token for each request, with only the scopes of the
operation. With a fixed token, the server checks the scopes of the token.

Give one credential: a private key, a token, or a token function. When the environment holds more
than one, the client uses `PIERRE_TOKEN` first, then `PIERRE_PRIVATE_KEY`, then
`PIERRE_PRIVATE_KEY_FILE`. With no credential, or no organization for the default hosts, the client
raises `PierreError`, or `pierre.NewClient` returns an error.

The **Settings** page of your organization in the dashboard shows your API host and Git host. Set
`PIERRE_API_BASE_URL` to the API origin and `PIERRE_GIT_HOST` to the Git host of remote URLs only
when they are not the defaults. Give the API origin with no path. The client adds the path of each
operation, which starts with `/api`.

TypeScript and Python read `PIERRE_PRIVATE_KEY_FILE` when they sign the first token. TypeScript
needs `node:fs` for it. Go reads the key at once, also from the file, and returns an error for a key
that it cannot read.

### Explicit options

An option replaces its variable. A credential in the options replaces every credential in the
environment. The comments name the variable of each option. `PIERRE_PRIVATE_KEY_FILE` has no option.

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import { readFileSync } from 'node:fs';

const pem = readFileSync('/run/secrets/pierre-key.pem', 'utf8');
const acme = new Pierre({
  org: 'acme', // PIERRE_ORG
  privateKey: pem, // PIERRE_PRIVATE_KEY
  baseUrl: 'https://api.acme.code.storage', // PIERRE_API_BASE_URL
  gitHost: 'acme.code.storage', // PIERRE_GIT_HOST
});
console.log(acme.options.baseUrl);

const jwt = readFileSync('/run/secrets/pierre-token', 'utf8');
const fixed = new Pierre({ token: jwt }); // PIERRE_TOKEN
console.log(fixed.options.org);
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
from pathlib import Path

pem = Path("/run/secrets/pierre-key.pem").read_text()
acme = Pierre(
    org="acme",  # PIERRE_ORG
    private_key=pem,  # PIERRE_PRIVATE_KEY
    base_url="https://api.acme.code.storage",  # PIERRE_API_BASE_URL
    git_host="acme.code.storage",  # PIERRE_GIT_HOST
)
print(acme.options.base_url)

jwt = Path("/run/secrets/pierre-token").read_text()
fixed = Pierre(token=jwt)  # PIERRE_TOKEN
print(fixed.options.org)
```

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import (
	"os"

	"github.com/pierrecomputer/sdk/go/option"
)

pem, err := os.ReadFile("/run/secrets/pierre-key.pem")
if err != nil {
	return err
}
acme, err := pierre.NewClient(
	option.WithOrg("acme"),             // PIERRE_ORG
	option.WithPrivateKey(string(pem)), // PIERRE_PRIVATE_KEY
	// PIERRE_API_BASE_URL and PIERRE_GIT_HOST:
	option.WithBaseURL("https://api.acme.code.storage"),
	option.WithGitHost("acme.code.storage"),
)
if err != nil {
	return err
}
fmt.Println(acme.Options().BaseURL)

jwt, err := os.ReadFile("/run/secrets/pierre-token")
if err != nil {
	return err
}
// PIERRE_TOKEN
fixed, err := pierre.NewClient(option.WithToken(string(jwt)))
if err != nil {
	return err
}
fmt.Println(fixed.Options().Org)
```

A client with no private key, such as a browser, gets each token from a function: `getToken` in
TypeScript, `get_token` in Python, and `option.WithGetToken` in Go. The client calls it for each
request, with the scopes, the `repo` claim, the lifetime, and the ref policies of that request. Your
server signs the token, for example with [the token helper](#tokens).

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
const browserClient = new Pierre({
  org: 'acme',
  getToken: async ({ scopes, repo, ttl, ref_policies, signal }) => {
    const response = await fetch('/pierre-token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ scopes, repo, ttl, ref_policies }),
      signal,
    });
    const { token } = (await response.json()) as { token: string };
    return token;
  },
});
console.log(browserClient.options.org);
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import httpx

from pierre.types import TokenRequest


def fetch_token(request: TokenRequest) -> str:
    response = httpx.post("https://tokens.acme.dev/pierre", json=request)
    response.raise_for_status()
    token: str = response.json()["token"]
    return token


delegated = Pierre(org="acme", get_token=fetch_token)
print(delegated.options.org)
```

* Python: an argument that is `None` falls back to its variable, then to the default.
* Go: the function of `option.WithGetToken` gets the context of the attempt and an
  `option.TokenRequest`.

[Authentication](https://code.storage/docs/platform/authentication.md) explains keys, scopes, and token claims.

## Call an operation

The method path is the catalog ID of the operation. The params and the results use the wire names of
the HTTP API, such as `repo_name` and `target_branch`.

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
const branch = await pierre.repos.branches.create({
  repo_name: 'acme/web',
  base_ref: 'main',
  target_branch: 'feat',
});
console.log(branch.target_branch, branch.commit_sha);
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
branch = pierre.repos.branches.create(
    repo_name="acme/web",
    base_ref="main",
    target_branch="feat",
)
print(branch["target_branch"], branch.get("commit_sha"))
```

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
branch, err := client.Repos.Branches.Create(ctx, pierre.ReposBranchesCreateParams{
	RepoName:     "acme/web",
	BaseRef:      pierre.String("main"),
	TargetBranch: pierre.String("feat"),
})
if err != nil {
	return err
}
fmt.Println(branch.TargetBranch)
```

* TypeScript: `repos.branches.create` is `pierre.repos.branches.create()`. Each method takes one
  params object, and the [request options](#request-options) as its second argument.
* Python: each segment is in snake\_case, so `repos.gitCredentials.create` is
  `pierre.repos.git_credentials.create()`. Each method takes keyword arguments. An optional argument
  has the default `NOT_GIVEN`, and the client does not send it. `None` sends `null` in a body field,
  and raises `PierreError` in a query or header argument.
* Go: the path is in Go names, `client.Repos.Branches.Create`, with the params struct
  `pierre.ReposBranchesCreateParams`. Each method takes a `context.Context`, the params, and request
  options. The JSON tags are the wire names. A required field has its plain type, and an optional
  field is a [`pierre.Opt[T]`](#go-optional-fields). A params struct has no deprecated field of the
  API. A nested request type, such as `pierre.BaseRepo`, and a response type keep their deprecated
  fields, each with a `Deprecated:` comment.

The reference page of each operation shows its call, for example
[`repos.branches.create`](https://code.storage/docs/api/repos/branch-create).

## Options

### Client options

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
const tuned = new Pierre({
  timeout: 30_000, // in milliseconds
  maxRetries: 3,
  defaultHeaders: { 'X-Team': 'web' },
  fetch: globalThis.fetch,
  refPolicies: [{ pattern: 'agent/*', ops: [] }],
});
console.log(tuned.options.timeout);
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import httpx

tuned = Pierre(
    timeout=30,  # in seconds
    max_retries=3,
    default_headers={"X-Team": "web"},
    http_client=httpx.Client(),
    ref_policies=[{"pattern": "agent/*", "ops": []}],
)
print(tuned.options.timeout)
```

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import (
	"net/http"
	"time"
)

tuned, err := pierre.NewClient(
	option.WithTimeout(30*time.Second),
	option.WithMaxRetries(3),
	option.WithHeader("X-Team", "web"),
	option.WithHTTPClient(&http.Client{}),
	option.WithRefPolicies(option.RefPolicy{Pattern: "agent/*"}),
)
if err != nil {
	return err
}
fmt.Println(tuned.Options().Timeout)
```

The defaults are a timeout of 60 seconds, 2 retries, no headers, the standard HTTP client, and no
ref policies. The HTTP client is `fetch` in TypeScript, an `httpx.Client` in Python, and an
`*http.Client` in Go. The timeout applies to each attempt. The headers can replace the
`Pierre-Client` header, for example `pierre-sdk-typescript/<version>`.

The options of a client hold its settings, with no credential: `pierre.options` in TypeScript and
Python, and `client.Options()` in Go. `withOptions` (`with_options` in Python, `WithOptions` in Go)
returns a copy of the client with changed options. The copy keeps the credential and each option
that you do not change. [Ref policies](#ref-policies) shows an example.

* Python: a client keeps one httpx client. `with Pierre() as pierre:` closes the httpx client that
  the SDK made at the end of the block. An `http_client` that you give stays open. For a client that
  lives as long as the process, create it one time.
* Go: one `option.RequestOption` serves a client and one request, and an option for the wrong use
  returns an error. In `client.WithOptions`, `option.WithHeader` adds a header to the default
  headers.

### Request options

A request option holds a setting for one call, and replaces the client option for that call. It goes
in the second argument in TypeScript, in the keyword arguments in Python, and in the last arguments
in Go.

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
// controller.abort() stops the call.
const controller = new AbortController();
const repo = await pierre.repos.get(
  { repo_name: 'acme/web' },
  {
    signal: controller.signal,
    timeout: 10_000,
    maxRetries: 0,
    headers: { 'X-Team': 'web' },
    refPolicies: [{ pattern: '*', ops: ['no-push'] }],
  },
);
console.log(repo.default_branch);
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
repo = pierre.repos.get(
    repo_name="acme/web",
    timeout=10,
    max_retries=0,
    extra_headers={"X-Team": "web"},
    ref_policies=[{"pattern": "*", "ops": ["no-push"]}],
)
print(repo["default_branch"])
```

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import "time"

repo, err := client.Repos.Get(ctx,
	pierre.ReposGetParams{RepoName: "acme/web"},
	option.WithTimeout(10*time.Second),
	option.WithMaxRetries(0),
	option.WithHeader("X-Team", "web"),
	option.WithRefPolicies(option.RefPolicy{
		Pattern: "*",
		Ops:     []string{"no-push"},
	}),
)
if err != nil {
	return err
}
fmt.Println(repo.DefaultBranch)
```

* To stop a call, use the `signal` in TypeScript, cancel the `asyncio` task in Python, or cancel the
  `context.Context` in Go.
* [Response headers](#response-headers) shows the response option: `withResponse()` in TypeScript,
  `pierre.with_raw_response` in Python, and `option.WithResponseInto` in Go.

### Ref policies

The ref policies limit the refs that a write can change. The client puts the rules in the `refs`
claim of each request token. The first rule whose pattern matches a ref applies.

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
const confined = pierre.withOptions({
  refPolicies: [
    { pattern: 'agent/*', ops: [] },
    { pattern: '*', ops: ['no-push'] },
  ],
});
console.log(confined.options.refPolicies);
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
confined = pierre.with_options(
    ref_policies=[
        {"pattern": "agent/*", "ops": []},
        {"pattern": "*", "ops": ["no-push"]},
    ]
)
print(confined.options.ref_policies)
```

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
confined, err := client.WithOptions(option.WithRefPolicies(
	option.RefPolicy{Pattern: "agent/*"},
	option.RefPolicy{Pattern: "*", Ops: []string{"no-push"}},
))
if err != nil {
	return err
}
fmt.Println(confined.Options().RefPolicies)
```

* A pattern is a full ref name (`refs/heads/main`), a branch name (`main`), a prefix that ends with
  `/*`, or `*`. The ops are `no-push`, `no-force-push`, and `verify-sig`.
* A request option replaces the rules of the client for that call. An empty list removes them: `[]`,
  or `option.WithRefPolicies()` with no rule.
* The token function receives the rules.
* A client with a fixed token cannot use ref policies, because the client cannot change the claims
  of that token.
* The [token and remote URL helpers](#local-helpers) use the rules of the client when the call gives
  none, so a confined client stays confined through Git.

[Ref Policies](https://code.storage/docs/repos/ref-policies.md) explains each op.

## Responses

A call gives one of three kinds of result: a JSON body, no body, or bytes.

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
// JSON: the parsed body.
const web = await pierre.repos.get({ repo_name: 'acme/web' });
console.log(web.default_branch);

// No body: { status, headers }.
const metadata = await pierre.repos.files.metadata.get({
  repo_name: 'acme/web',
  path: 'README.md',
});
console.log(metadata.status, metadata.headers.etag);

// Bytes: the platform Response.
const file = await pierre.repos.files.get({
  repo_name: 'acme/web',
  path: 'README.md',
});
console.log(file.headers.get('etag'), await file.text());
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
# JSON: a dict, typed as a TypedDict of pierre.types.
web = pierre.repos.get(repo_name="acme/web")
print(web["default_branch"])

# No body: a HeadersResult with status and headers.
metadata = pierre.repos.files.metadata.get(
    repo_name="acme/web",
    path="README.md",
)
print(metadata["status"], metadata["headers"].get("etag"))

# Bytes: the httpx.Response, with an open stream.
file = pierre.repos.files.get(
    repo_name="acme/web",
    path="README.md",
)
try:
    print(file.headers.get("etag"), file.read().decode())
finally:
    file.close()
```

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import "io"

// JSON: a pointer to the decoded body.
web, err := client.Repos.Get(ctx, pierre.ReposGetParams{
	RepoName: "acme/web",
})
if err != nil {
	return err
}
fmt.Println(web.DefaultBranch)

// No body: a *pierre.HeadersResult[H].
metadata, err := client.Repos.Files.Metadata.Get(
	ctx,
	pierre.ReposFilesMetadataGetParams{
		RepoName: "acme/web",
		Path:     "README.md",
	},
)
if err != nil {
	return err
}
fmt.Println(metadata.Status, metadata.Headers.ETag)

// Bytes: an io.ReadCloser.
file, err := client.Repos.Files.Get(ctx, pierre.ReposFilesGetParams{
	RepoName: "acme/web",
	Path:     "README.md",
})
if err != nil {
	return err
}
defer file.Close()
content, err := io.ReadAll(file)
if err != nil {
	return err
}
fmt.Println(string(content))
```

The results use the wire names of the response. `repos.files.metadata.get` has no body, and
`repos.files.get` and `repos.archive.get` give bytes. In TypeScript, each method returns an
`APIPromise`, and the request starts when you call the method.

### Response headers

The response of a call gives its headers and its request ID.

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
const { data, headers, request_id, response } = await pierre.repos
  .grep({ repo_name: 'acme/web', query: { pattern: 'TODO' } })
  .withResponse();
console.log(data.matches.length, headers.x_cache, request_id, response.status);
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
raw = pierre.with_raw_response.repos.grep(repo_name="acme/web", query={"pattern": "TODO"})
print(len(raw.data["matches"]), raw.headers.get("x_cache"), raw.request_id)
print(raw.response.status_code)
```

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import "net/http"

var grepResponse *http.Response
matches, err := client.Repos.Grep(ctx, pierre.ReposGrepParams{
	RepoName: "acme/web",
	Query:    pierre.GrepQuery{Pattern: "TODO"},
}, option.WithResponseInto(&grepResponse))
if err != nil {
	return err
}
fmt.Println(len(matches.Matches), grepResponse.Header.Get("X-Cache"))
fmt.Println(grepResponse.Header.Get("X-Request-Id"))
```

* TypeScript and Python: `data` is the result of the call, and `response` is the response of the
  last attempt. `headers` holds the response headers that the reference page declares, with the wire
  form of each name, such as `x_cache`, `etag`, and `x_blob_sha`. `request_id` is the `X-Request-Id`
  header, or none. In a browser, the client can read only the headers that the API exposes through
  CORS.
* Go: `option.WithResponseInto` stores the `*http.Response` of the last attempt, with every header.

### Byte responses

A byte response returns when its headers arrive. You read the body, so the timeout ends with the
headers, and the client does not retry the body. In Python and Go, close the stream when you are
done, as [Responses](#responses) shows.

In TypeScript, `asResponse()` gives the platform `Response` of a JSON operation when its headers
arrive, and does not read the body. Use it to read a large body yourself. After `await` or
`withResponse()`, the client has already read the body.

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
const diffResponse = await pierre.repos.diff
  .get({ repo_name: 'acme/web', ref: 'feat', base_ref: 'main' })
  .asResponse();
console.log(diffResponse.status, await diffResponse.json());
```

## Lists and pagination

A list method returns the first page. A loop on the result gives the items of every page:

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
for await (const item of pierre.repos.branches.list({ repo_name: 'acme/web' })) {
  console.log(item.name, item.head_sha);
}
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
for item in pierre.repos.branches.list(repo_name="acme/web"):
    print(item["name"], item["head_sha"])
```

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
branches, err := client.Repos.Branches.List(ctx, pierre.ReposBranchesListParams{
	RepoName: "acme/web",
})
if err != nil {
	return err
}
for item, err := range branches.All(ctx) {
	if err != nil {
		return err
	}
	fmt.Println(item.Name, item.HeadSHA)
}
```

A page has the items in `data`, with `has_more`, `next_cursor`, `body`, and `response` (`Data`,
`HasMore`, `NextCursor`, `Body`, and `Response` in Go). `body` is the full response body, with the
fields beside the items. The page iterator gives this page, then each next page:

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
const firstPage = await pierre.repos.files.metadata.list({ repo_name: 'acme/web', limit: 100 });
for await (const page of firstPage.iterPages()) {
  console.log(page.data.length, Object.keys(page.body.commits).length);
}
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
first_page = pierre.repos.files.metadata.list(repo_name="acme/web", limit=100)
for page in first_page.iter_pages():
    print(len(page.data), len(page.body["commits"]))
```

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
firstPage, err := client.Repos.Files.Metadata.List(ctx, pierre.ReposFilesMetadataListParams{
	RepoName: "acme/web",
	Limit:    pierre.Int(100),
})
if err != nil {
	return err
}
for page, err := range firstPage.IterPages(ctx) {
	if err != nil {
		return err
	}
	fmt.Println(len(page.Data), len(page.Body.Commits))
}
```

To move one page at a time, check for a next page with `hasNextPage()`, then get it with
`getNextPage()`: `has_next_page()` and `get_next_page()` in Python, `HasNextPage()` and
`GetNextPage(ctx)` in Go. The next page sends the params and the request options of the first call.

To store a page, serialize its body, the response that the API sent. `JSON.stringify(page)` and
`json.Marshal(page)` give the body. In TypeScript, `withResponse()` and `asResponse()` on a list
give the response of the first page.

## Errors

Every error of the SDK extends `PierreError`. Import the classes from `@pierre/sdk` or `pierre`. In
Go, each error is a pointer type, such as `*pierre.NotFoundError`, that unwraps to a
`*pierre.PierreError`, and you branch with `errors.As`.

* `APIError`: the API sent an error response. It has `status`, `code`, `title`, `detail`,
  `request_id`, `headers`, `body`, and `doc_url`.
* The `APIError` of each status family. Another status, such as 412, gives a plain `APIError`.
  * `BadRequestError`: 400
  * `AuthenticationError`: 401
  * `PermissionDeniedError`: 403
  * `NotFoundError`: 404
  * `ConflictError`: 409
  * `RateLimitError`: 429
  * `InternalServerError`: 5xx
* `MergeConflictError`: a `ConflictError` for a merge conflict, with `conflict_type`,
  `conflict_paths`, and `merge_base_sha`.
* `RefGuardError`: a `ConflictError` with the code `ref_guard_failed`, with `guard` and
  `current_sha`.
* `RefUpdateError`: an `APIError` for a Git write whose ref update failed, with `result` and
  `commit`.
* `APIConnectionError`: the request got no response.
* `APITimeoutError`: an `APIConnectionError`. The attempt got no response before its timeout.
* `WebhookVerificationError`: the webhook helper rejected a delivery. `reason` says why.

Branch on the class and on `code`, not on the message. [Error codes](https://code.storage/docs/api/overview.md#error-codes)
lists the codes.

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import { NotFoundError, RefGuardError, RefUpdateError } from '@pierre/sdk';

const { branch: mainBranch } = await pierre.repos.branches.get({
  repo_name: 'acme/web',
  name: 'main',
});
try {
  await pierre.repos.commits.create({
    repo_name: 'acme/web',
    target_branch: 'main',
    expected_target_sha: mainBranch.head_sha,
    commit_message: 'Update readme',
    author: { name: 'Bot', email: 'bot@acme.dev' },
    files: [{ path: 'README.md', content: '# Web' }],
  });
} catch (error) {
  if (error instanceof RefGuardError) {
    // Another write moved main. Send current_sha as the guard of the next attempt.
    console.log(error.guard, error.current_sha);
  } else if (error instanceof RefUpdateError) {
    console.log(error.result.status); // for example 'conflict'
  } else if (error instanceof NotFoundError && error.code === 'repository_not_found') {
    console.log('acme/web does not exist');
  } else {
    throw error;
  }
}
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
from pierre import NotFoundError, RefGuardError, RefUpdateError

main_branch = pierre.repos.branches.get(repo_name="acme/web", name="main")["branch"]
try:
    pierre.repos.commits.create(
        repo_name="acme/web",
        target_branch="main",
        expected_target_sha=main_branch["head_sha"],
        commit_message="Update readme",
        author={"name": "Bot", "email": "bot@acme.dev"},
        files=[{"path": "README.md", "content": "# Web"}],
    )
except RefGuardError as error:
    # Another write moved main. Send current_sha as the guard of the next attempt.
    print(error.guard, error.current_sha)
except RefUpdateError as error:
    print(error.result["status"])  # for example "conflict"
except NotFoundError as error:
    if error.code != "repository_not_found":
        raise
    print("acme/web does not exist")
```

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import (
	"errors"
	"strings"
)

head, err := client.Repos.Branches.Get(ctx, pierre.ReposBranchesGetParams{
	RepoName: "acme/web",
	Name:     "main",
})
if err != nil {
	return err
}
_, err = client.Repos.Commits.Create(ctx, pierre.ReposCommitsCreateParams{
	RepoName:          "acme/web",
	TargetBranch:      "main",
	ExpectedTargetSHA: pierre.String(head.Branch.HeadSHA),
	CommitMessage:     "Update readme",
	Author:            pierre.GitIdentity{Name: "Bot", Email: "bot@acme.dev"},
	Files:             []pierre.CommitFile{{Path: "README.md", Content: strings.NewReader("# Web")}},
})
var guardErr *pierre.RefGuardError
var refErr *pierre.RefUpdateError
var notFound *pierre.NotFoundError
switch {
case errors.As(err, &guardErr):
	// Another write moved main. Send CurrentSHA as the guard of the next attempt.
	fmt.Println(guardErr.Guard, guardErr.CurrentSHA)
case errors.As(err, &refErr):
	switch refErr.Result.Status {
	case pierre.RefUpdateResultStatusConflict:
		fmt.Println("the change does not apply")
	default:
		fmt.Println(refErr.Result.Status)
	}
case errors.As(err, &notFound) && notFound.Code == "repository_not_found":
	fmt.Println("acme/web does not exist")
case err != nil:
	return err
}
```

* A failed ref guard raises `RefGuardError`. `guard` names the request field of the guard, for
  example `expected_target_sha`. `current_sha` is the current commit of the ref. Send it as the
  guard of the next attempt.
* Another failed ref update raises `RefUpdateError`. Its `result` has `status`, which gives the
  reason, for example `conflict`, with no cast. `result` is a `RefUpdateResult`, or a
  `NotesWriteResult` for a notes write. In Go, `Result` is always a `RefUpdateResult`, and a notes
  write fills only `Success`, `Status`, and `Message`. `code` can be empty on a `RefUpdateError`.
* A `NotFoundError` has the code `repository_not_found` when the repository does not exist. A
  missing object in the repository, such as a file or a note, has the code `not_found`.
* Go: check `*pierre.RefGuardError` before `*pierre.ConflictError`, because it unwraps to one. A
  `*pierre.RefUpdateError` unwraps to the `*pierre.APIError`, not to a `*pierre.ConflictError`. The
  end of the context of a call returns the cause of the context, and an error of your own function,
  such as `Open` of a `pierre.CommitFile`, reaches you as it is.

### Retries and timeouts

The client retries a connection error, a timeout, 408, 429, 5xx, and 409 with the code
`repository_thawing` or `sync_in_progress`. It retries only `GET`, `HEAD`, `PUT`, and `DELETE`
requests, so a write such as `repos.commits.create` never retries. It waits for `Retry-After` when
the value is 60 seconds or less. Else it backs off from 0.5 seconds, up to 8 seconds.

Each attempt has its own timeout: 60 seconds by default. The timeout covers the token, the upload,
the response headers, and a JSON body. Give a larger timeout for a large commit. When the last
attempt times out, the call raises `APITimeoutError`.

A retried `DELETE` can get a 404 when the first attempt worked and its response was lost. To turn
off the retries of a call, set its retries to 0: `maxRetries: 0`, `max_retries=0`, or
`option.WithMaxRetries(0)`. In Python, `Pierre` cannot stop a blocking `get_token`, so the attempt
fails when the token arrives after the deadline.

## Python async code

`AsyncPierre` takes the same arguments as `Pierre`, and each method is async. Its `get_token` can be
async, and its `http_client` is an `httpx.AsyncClient`.

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import asyncio

from pierre import AsyncPierre
from pierre.git import remote_url


async def main() -> None:
    async with AsyncPierre() as pierre:
        repo = await pierre.repos.get(repo_name="acme/web")
        print(repo["default_branch"])

        async for item in pierre.repos.branches.list(repo_name="acme/web"):
            print(item["name"])

        first = await pierre.repos.branches.list(repo_name="acme/web")
        print(len(first.data), first.has_more)

        raw = await pierre.with_raw_response.repos.get(repo_name="acme/web")
        print(raw.request_id)

        print(await remote_url(pierre, repo_name="acme/web"))


asyncio.run(main())
```

* A method starts its request when you await it, as a coroutine does.
* A list method returns an `AsyncPaginator`. `await` gives the first `AsyncPage`, and `async for`
  gives the items of every page. `get_next_page()` and `iter_pages()` of an `AsyncPage` are async.
* `async with` closes the httpx client that the SDK made.
* To stop a call, cancel its task.
* With `AsyncPierre`, await the result of `remote_url`. `create_token` stays synchronous.

## Go optional fields

An optional field, or a field that can be null, is a `pierre.Opt[T]`:

* The zero value is omitted. The request does not send it, so the server keeps the stored value.
* `pierre.String`, `pierre.Int` (an `int64`), `pierre.Float`, `pierre.Bool`, and `pierre.Value` give
  a value. `pierre.Value` takes any type, such as a struct.
* `pierre.Null[T]()` gives `null`, which resets the stored value.

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
omitted := pierre.Opt[string]{}  // the request does not send the field
named := pierre.String("main")   // sends "main"
cleared := pierre.Null[string]() // sends null
fmt.Println(omitted.IsZero(), cleared.IsNull())
if value, ok := named.Get(); ok {
	fmt.Println(value)
}
```

A response field that is optional or can be null is an `Opt` too. `Get` gives the value and whether
the response has one, and `IsNull` reports `null`.

An enum is a string type with a constant for each known value, such as
`pierre.RefUpdateResultStatusConflict`. A response can hold a value that this SDK version does not
list, so give each `switch` on an enum a `default` case. In every language, an enum is open.

## Go contexts

Each method takes a `context.Context` as its first argument. The context stops the call at any time,
also during the wait between two attempts. The call then returns the cause of the context.
`option.WithTimeout` limits each attempt, and a context limits the whole call, with its retries.

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
callCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
_, err = client.Repos.Get(callCtx, pierre.ReposGetParams{RepoName: "acme/web"})
if errors.Is(err, context.DeadlineExceeded) {
	fmt.Println("the context stopped the call")
}
```

## Local helpers

These helpers make no call to the API:

| Helper | TypeScript | Python | Go |
| - | - | - | - |
| Tokens | `createToken` from `@pierre/sdk/auth` | `create_token` from `pierre.auth` | `auth.CreateToken` |
| Git remote URLs | `remoteUrl` from `@pierre/sdk/git` | `remote_url` from `pierre.git` | `git.RemoteURL` |
| Webhooks | `verifyWebhook` from `@pierre/sdk/webhooks` | `verify_webhook` from `pierre.webhooks` | `webhooks.VerifyWebhook` |

* TypeScript: no subpath loads another, so a browser or edge bundle holds only what it imports.
  `@pierre/sdk/core` exports `PierreCore`, the client with no namespaces, and `@pierre/sdk/repos`
  exports `ReposNamespace`, built with `new ReposNamespace(new PierreCore())`.
* Python: `import pierre` does not load these modules, and a variable named `pierre` hides the
  module. Import them with `from pierre.<module> import ...`.

### Tokens

The token helper signs a token with the private key of a client, or with an organization and a
private key. A client with a fixed token or a token function cannot sign.

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import { createToken, SCOPES } from '@pierre/sdk/auth';

const readToken = await createToken(pierre, {
  scopes: ['git:read'],
  repo: 'acme/web',
  ttl: 900,
});
console.log(readToken.length, SCOPES);
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
from pierre.auth import SCOPES, create_token

read_token = create_token(pierre, scopes=["git:read"], repo="acme/web", ttl=900)
print(len(read_token), SCOPES)
```

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import "github.com/pierrecomputer/sdk/go/auth"

readToken, err := auth.CreateToken(ctx, client, auth.TokenParams{
	Scopes: []string{"git:read"},
	Repo:   "acme/web",
	TTL:    15 * time.Minute,
})
if err != nil {
	return err
}
fmt.Println(len(readToken), auth.Scopes)
```

* `scopes` names at least one scope. `SCOPES` (`auth.Scopes` in Go) lists the scopes of the API.
* `repo` sets the `repo` claim. Leave it out for a token whose scopes are all organization-wide.
* `ttl` is the lifetime, from 1 second to 1 hour. Default: 15 minutes.
* `ref_policies` sets the `refs` claim. Default: the ref policies of the client.

[Authentication](https://code.storage/docs/platform/authentication.md) lists the scopes and the claims.

### Git remote URLs

The remote URL helper builds `https://t:<token>@{gitHost}/{repo_name}.git` for `git clone`,
`git fetch`, and `git push`.

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import { remoteUrl } from '@pierre/sdk/git';

const remote = await remoteUrl(pierre, { repo_name: 'acme/web', ttl: 3600 });
console.log(remote); // https://t:<token>@acme.code.storage/acme/web.git
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
from pierre.git import remote_url

remote = remote_url(pierre, repo_name="acme/web", ttl=3600)
print(remote)  # https://t:<token>@acme.code.storage/acme/web.git
```

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import "github.com/pierrecomputer/sdk/go/git"

remote, err := git.RemoteURL(ctx, client, git.RemoteURLParams{RepoName: "acme/web", TTL: time.Hour})
if err != nil {
	return err
}
fmt.Println(remote) // https://t:<token>@acme.code.storage/acme/web.git
```

* The token has `git:read` and `git:write`, and lives 15 minutes. The scopes and the TTL change
  them, up to 1 hour.
* The kind `ephemeral` gives the `+ephemeral` remote, and the kind `import` gives the `+import`
  remote: `kind: 'ephemeral'`, `kind="ephemeral"`, or `Kind: git.KindEphemeral`.
* It works with a private key or a token function. A client with a fixed token cannot set the claims
  of its token, so the helper raises `PierreError`.

Git copies the remote URL and its token into `.git/config`, so keep the lifetime short. For longer
work, use the credential helper of the CLI, `pierre auth setup-git`. See
[Git Operations](https://code.storage/docs/repos/git-operations.md), [Ephemeral Branches](https://code.storage/docs/repos/ephemeral-branches.md), and
[Imports](https://code.storage/docs/repos/imports.md).

### Webhooks

The webhook helper checks the `X-Pierre-Signature` header of a delivery and returns the typed event.
It raises `WebhookVerificationError` when the check fails.

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import { verifyWebhook, WebhookVerificationError } from '@pierre/sdk/webhooks';

async function handleWebhook(request: Request, secret: string): Promise<Response> {
  try {
    const event = await verifyWebhook({
      payload: await request.text(),
      headers: request.headers,
      secret,
    });
    switch (event.type) {
      case 'push':
        console.log(event.repository.repo_name, event.ref, event.after);
        break;
      case 'repo.sync.failed':
        console.log(event.repository.repo_name, event.error);
        break;
      case 'unknown':
        console.log('Unknown event type', event.event_type);
        break;
      default:
        console.log(event.type);
    }
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) {
      return new Response(error.reason, { status: 401 });
    }
    throw error;
  }
}
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
from typing import Mapping

from pierre.webhooks import WebhookVerificationError, verify_webhook


def handle_webhook(body: bytes, headers: Mapping[str, str], secret: str) -> int:
    try:
        event = verify_webhook(payload=body, headers=headers, secret=secret)
    except WebhookVerificationError as error:
        print(error.reason)
        return 401
    if event["type"] == "push":
        print(event["repository"].get("repo_name"), event["ref"], event["after"])
    elif event["type"] == "repo.sync.failed":
        print(event["repository"].get("repo_name"), event["error"])
    elif event["type"] == "unknown":
        print("Unknown event type", event["event_type"])
    return 204
```

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import (
	"errors"
	"fmt"
	"io"
	"net/http"

	pierre "github.com/pierrecomputer/sdk/go"
	"github.com/pierrecomputer/sdk/go/webhooks"
)

func handleWebhook(w http.ResponseWriter, r *http.Request, secret string) {
	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "cannot read body", http.StatusBadRequest)
		return
	}
	event, err := webhooks.VerifyWebhook(webhooks.VerifyWebhookParams{
		Payload: body,
		Header:  r.Header,
		Secret:  secret,
	})
	var rejected *pierre.WebhookVerificationError
	if errors.As(err, &rejected) {
		http.Error(w, string(rejected.Reason), http.StatusUnauthorized)
		return
	}
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	switch e := event.(type) {
	case *pierre.GitPushEvent:
		repoName, _ := e.Repository.RepoName.Get()
		fmt.Println(repoName, e.Ref, e.After)
	case *pierre.GitSyncFailedEvent:
		fmt.Println(e.Error)
	case *webhooks.UnknownEvent:
		fmt.Println("unknown event type", e.Type)
	}
	w.WriteHeader(http.StatusNoContent)
}
```

* The payload is the raw request body. Do not pass parsed JSON.
* The headers are the request headers: a Fetch `Headers` or a record in TypeScript, a mapping in
  Python, and an `http.Header` in Go.
* The tolerance is the largest age of a delivery. Default: 5 minutes.
* An event type that this SDK version does not know gives an unknown event, with the type that
  Pierre sent and the parsed payload: `type: 'unknown'` with `event_type` and `body` in TypeScript
  and Python, and a `*webhooks.UnknownEvent` with `Type` and `Body` in Go.

[Webhooks](https://code.storage/docs/repos/webhooks.md) describes the events and their payloads.

## Related pages

* [CLI](https://code.storage/docs/cli.md): the `pierre` command and the Git credential helper.
* [Authentication](https://code.storage/docs/platform/authentication.md): keys, scopes, and token claims.
* [Access & Permissions](https://code.storage/docs/repos/access.md): repository claims and scopes for each client.
* [Git Operations](https://code.storage/docs/repos/git-operations.md): Git over HTTPS with a token.
* [Ref Policies](https://code.storage/docs/repos/ref-policies.md): limits on the refs that a token can change.
* [Webhooks](https://code.storage/docs/repos/webhooks.md): events, payloads, and signatures.
* [API Overview](https://code.storage/docs/api/overview.md): HTTP conventions, pagination, and error codes.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.