> ## Documentation Index
> Fetch the complete documentation index at: https://test-8ad8522e-feat-ai-sre.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Flashduty Go SDK

> go-flashduty is the official open-source Go SDK for Flashduty — a typed, strictly 1:1 wrapper over the Open API.

## Overview

***

`go-flashduty` is the official open-source Go client for Flashduty, covering every REST endpoint of the Flashduty Open API. It follows the same design as [go-github](https://github.com/google/go-github) — service groups, typed requests and responses, a composable transport layer — and stays strictly 1:1 with the OpenAPI spec: each method maps to exactly one HTTP call, returns `(*T, *Response, error)`, and performs no implicit cross-endpoint aggregation or enrichment.

The SDK provides typed API operations generated from the Flashduty OpenAPI spec, covered by unit tests, and end-to-end verified against the live API.

<Note>
  The SDK is deliberately "thin." Consumer-side logic such as short-ID resolution and cross-endpoint orchestration belongs in the caller (CLI / MCP), not stuffed into the SDK or shoehorned into an endpoint. This keeps the SDK strictly one-to-one with the API — predictable, generatable, and verifiable.
</Note>

The module path is `github.com/flashcatcloud/go-flashduty`, the package name is `flashduty`, and the source is open-sourced under Apache-2.0 at [flashcatcloud/go-flashduty](https://github.com/flashcatcloud/go-flashduty).

<CardGroup cols={2}>
  <Card title="Open API reference" icon="book" href="/en/openapi/introduction">
    Request parameters and response fields for every endpoint.
  </Card>

  <Card title="Command-line tool" icon="code" href="/en/developer/cli">
    The CLI for operating Flashduty directly from your terminal.
  </Card>
</CardGroup>

## Installation

***

<Steps>
  <Step title="Requires Go 1.24+">
    Make sure your local Go toolchain is at least 1.24.
  </Step>

  <Step title="Get the dependency">
    ```bash theme={null}
    go get github.com/flashcatcloud/go-flashduty
    ```
  </Step>

  <Step title="Import the package">
    ```go theme={null}
    import flashduty "github.com/flashcatcloud/go-flashduty"
    ```
  </Step>
</Steps>

## Quick start

***

Here is a minimal runnable example: construct the client, list incidents in the "Triggered" state, and handle the returned triple `(data, *Response, error)`.

```go theme={null}
package main

import (
	"context"
	"fmt"
	"log"

	flashduty "github.com/flashcatcloud/go-flashduty"
)

func main() {
	client, err := flashduty.NewClient("YOUR_APP_KEY")
	if err != nil {
		log.Fatal(err)
	}

	list, resp, err := client.Incidents.List(context.Background(), &flashduty.ListIncidentsRequest{
		Progress:    "Triggered",
		ListOptions: flashduty.ListOptions{Limit: 20},
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("request_id=%s total=%d has_next=%t\n", resp.RequestID, resp.Total, resp.HasNextPage)
	for _, inc := range list.Items {
		fmt.Printf("[%s] %s\n", inc.IncidentSeverity, inc.Title)
	}
}
```

Every call returns three values:

| Return value | Type                  | Description                                                                             |
| ------------ | --------------------- | --------------------------------------------------------------------------------------- |
| data         | `*T`                  | The endpoint's typed response body (e.g. `*ListIncidentsResponse`); `nil` on failure    |
| `*Response`  | `*flashduty.Response` | Wraps `*http.Response` and carries envelope metadata such as `RequestID` and pagination |
| `error`      | `error`               | `*ErrorResponse` on failure, `*RateLimitError` on 429                                   |

<Tip>
  `app_key` is used for authentication, and the SDK injects it as a query parameter on every request automatically. Obtain the `app_key` from "Push integrations" or team configuration in the Flashduty console.
</Tip>

## Create a client

***

`NewClient` takes an `app_key` plus zero or more `Option`s. An empty `app_key` returns an error directly. The default Base URL is `https://api.flashcat.cloud`, the default HTTP timeout is 30 seconds, and the default User-Agent is `go-flashduty`.

```go theme={null}
client, err := flashduty.NewClient("YOUR_APP_KEY",
	flashduty.WithBaseURL("https://api.flashcat.cloud"),
	flashduty.WithTimeout(10*time.Second),
	flashduty.WithUserAgent("my-app/1.0"),
	flashduty.WithHTTPClient(customHTTPClient),
	flashduty.WithTransport(customRoundTripper),
	flashduty.WithLogger(myLogger),
	flashduty.WithRequestHeaders(staticHeaders),
	flashduty.WithRequestHook(func(req *http.Request) { /* e.g. inject traceparent */ }),
)
```

| Option                                      | Description                                                                                                                                                                   |
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `WithBaseURL(raw string)`                   | Override the API base URL (default `https://api.flashcat.cloud`). Use it to point at your own gateway for private deployments; an invalid URL errors at the `NewClient` stage |
| `WithTimeout(d time.Duration)`              | Set the overall timeout of the underlying HTTP client                                                                                                                         |
| `WithUserAgent(ua string)`                  | Set the `User-Agent` header carried on every request                                                                                                                          |
| `WithHTTPClient(hc *http.Client)`           | Replace the underlying `*http.Client`; ignored when `nil`                                                                                                                     |
| `WithTransport(rt http.RoundTripper)`       | Set a custom `http.RoundTripper`, the idiomatic hook for middleware such as retry, caching, tracing, and rate limiting; ignored when `nil`                                    |
| `WithLogger(l Logger)`                      | Set a custom logger; ignored when `nil`                                                                                                                                       |
| `WithRequestHeaders(h http.Header)`         | Set static headers appended to every request, applied after the SDK's own headers (`Content-Type`, `Accept`, `User-Agent`)                                                    |
| `WithRequestHook(hook func(*http.Request))` | Register a callback invoked before each request is sent, for injecting per-request headers (such as W3C `traceparent`)                                                        |

<Info>
  **Private deployment**: point the client at your own Flashduty gateway address with `WithBaseURL` — everything else stays exactly the same.
</Info>

## Services and methods

***

Endpoints are grouped by service and hang off the client: the call convention is uniformly `client.<Service>.<Method>(ctx, req)`, returning `(*T, *Response, error)`. For example, `client.Incidents.List(ctx, req)` or `client.Sessions.Info(ctx, req)`.

| Service field                     | Description                                       |
| --------------------------------- | ------------------------------------------------- |
| `client.Incidents`                | Incidents                                         |
| `client.Alerts`                   | Alerts                                            |
| `client.Channels`                 | Channels                                          |
| `client.Schedules`                | Schedules                                         |
| `client.Licenses`                 | On-call licenses                                  |
| `client.Calendars`                | Calendars                                         |
| `client.StatusPages`              | Status pages                                      |
| `client.Members`                  | Members                                           |
| `client.Teams`                    | Teams                                             |
| `client.RolesPermissions`         | Roles and permissions                             |
| `client.Account`                  | Account                                           |
| `client.AuditLogs`                | Audit logs                                        |
| `client.AlertRules`               | Alert rules                                       |
| `client.AlertEnrichment`          | Alert enrichment                                  |
| `client.DataSources`              | Data sources                                      |
| `client.Integrations`             | Integrations                                      |
| `client.ImIntegrations`           | IM integrations                                   |
| `client.NotificationTemplates`    | Notification templates                            |
| `client.Changes`                  | Changes                                           |
| `client.Diagnostics`              | Diagnostics                                       |
| `client.Analytics`                | Analytics                                         |
| `client.A2aAgents`                | A2A Agents                                        |
| `client.Artifacts`                | AI SRE artifacts                                  |
| `client.Automations`              | AI SRE automations                                |
| `client.Knowledge`                | AI SRE knowledge base (knowledge packs and files) |
| `client.McpServers`               | MCP Servers                                       |
| `client.Sessions`                 | AI SRE sessions                                   |
| `client.Skills`                   | Skills                                            |
| `client.Applications`             | RUM applications                                  |
| `client.DataQuery`                | RUM data query                                    |
| `client.ErrorIngestionRules`      | RUM error ingestion rules                         |
| `client.Facets`                   | RUM fields and facets                             |
| `client.IssuePresetSeverityRules` | RUM issue preset severity rules                   |
| `client.Issues`                   | RUM issues                                        |
| `client.Resources`                | RUM resources                                     |
| `client.SessionReplay`            | RUM session replay                                |
| `client.Sourcemaps`               | RUM sourcemaps                                    |

`client.StatusPages.DraftCreate` (`POST /status-page/draft/create`) stores a status page event draft for a human to review and publish from the console; it is never directly visible to the public: `draft` is arbitrary JSON stored verbatim, up to 64 KB serialized, with `page_id`, `type` (`incident` or `maintenance`), `name`, and `message` validated; `change_id` (> 0 appends an update to an existing event), `status`, and `affected_components` are optional, and for a new maintenance you can set `start_time` / `end_time` (Unix seconds) for the window. The request-level `source` is an opaque marker of the drafting origin (up to 64 characters, e.g. `ai_sre:sess_xxx`); the response returns a `draft_id` matching `draft_[A-Za-z0-9]{22}`, which the console review link carries.

`client.Knowledge` covers the 9 operations under `/safari/knowledge/*`: on the pack side `PackReadGet` (get the account pack), `PackReadList` (list packs), `PackWriteEnsure` (ensure a pack exists), `PackWriteUpdate` (change a pack's scope), and `PackWriteDelete` (delete a pack); on the file side `FileReadGet`, `FileReadList`, `FileWritePut` (upload/overwrite), and `FileWriteDelete`. Exported types include `KnowledgePackItem`, `KnowledgeFileItem`, `KnowledgeWarning`, and the various `Knowledge*Request` / `Knowledge*Response` structs.

`client.Artifacts` (AI SRE artifacts) covers the 11 operations under `/safari/artifact/*`: on the gallery read side `ReadGet` (get a single published artifact by ID), `ReadList` (list artifacts visible to the caller, with title substring search and `scope` (`all` / `personal` / `team`) plus `team_ids` filtering), and `ReadGetFileState` (probe up to 50 presented-file IDs (`pf_` prefix) in one call for ones that already have a live published artifact); on the file side `ReadSign` (issue short-lived download/preview URLs for a presented file, valid 5 minutes, `expires_in` is fixed at 300) and `ReadStream` (download or preview the file's bytes with a signed token — the success body is a file, not a JSON envelope, with the raw bytes on `Response.Raw`); on the write side `WritePublish` (publish a session-produced file to the gallery), `WriteUpdate` (rename the artifact or transfer it between personal and team scope), and `WriteDelete` (detach it from the gallery; the source file stays with its session); public sharing `WriteShareEnable` (turn on anonymous public sharing and return the public link — anyone with the link can view it, no login required), `WriteShareRevoke` (turn sharing off; the link stops resolving immediately), and `WriteShareSync` (refresh the public snapshot with the latest content — when `share_enabled` is true and `share_file_id` differs from `file_id`, the snapshot is stale and calling this refreshes it). Exported types include `PublishedArtifactItem`, `ArtifactShareState`, `SignedUrLs`, and the various `Artifact*Request` / `Artifact*Response` structs.

`client.Diagnostics.QueryData` runs a synchronous query via `POST /monit/query/data` and returns a stable `query_result.v1` structured result (`result.kind` is one of `frames`, `records`, or `samples`). This API requires monit-edge v0.65.0 or later. For log-pattern and metric-trend analysis, use `client.DataSources.ToolsInvoke` with `prometheus.metric_trends`, `loki.log_patterns`, or `victorialogs.log_patterns`.

`client.DataSources.ToolsInvoke` (`POST /monit/datasource/tools/invoke`, `monit-datasource-tools-invoke`) executes one deterministic tool against a configured datasource: `tool` is a single tool name prefixed by the datasource type (e.g. `mysql.overview`), and `params` is the tool-specific JSON parameters (omitted means `{}`; an explicit `null` is invalid). Alongside diagnostic tools, the entry supports `<type>.query` query tools (`prometheus`, `mysql`, `postgres`, `oracle`, `clickhouse`, `elasticsearch`, `loki`, `victorialogs`, `sls`, `tencent_cls`); the `/monit/query/data` entry stays unchanged. It requires all currently online routable Edge sessions in the cluster to support the v0.71.0 base invoke protocol (individual tools may require a newer implementation), and there is no tool catalog, no automatic replay, and no fallback to legacy diagnose. The request body limit is 128 KiB, the complete success response limit is 10 MiB, and the tool timeout is at most 25 seconds; the response is a `DatasourceToolResult` (`data` is tool-specific JSON, never null, `summary` is optional, and a `truncated` object with `reason` indicates truncation).

For `client.DataSources`, the `payload` selects a type-specific configuration block by `type_ident`. Fifteen `type_ident` values are allowed: `prometheus`, `loki`, `mysql`, `oracle`, `postgres`, `clickhouse`, `elasticsearch`, `sls`, `tencent_cls`, `victorialogs`, plus the new diagnostic-only types `redis_node`, `redis_sentinel`, `mongodb_mongod`, `mongodb_mongos`, and `kafka` — diagnostic-only types always have `alerting_enabled` false (which does not block non-alerting queries or tools) and reject true. Connection address rules: Redis/MongoDB diagnostic types take a single `host:port` (bracket IPv6), with no URI, userinfo, or query; `kafka` takes 1–32 unique comma-separated `host:port` bootstrap addresses (at most 4096 characters after normalization, and the payload has no broker list); for `mongodb_mongod` / `mongodb_mongos` the configuration block's `auth_source` defaults to `admin`, username and password must be configured together, and client certificates are unsupported; the Redis node configuration's `database` defaults to 0. Sensitive fields such as the diagnostic types' `password` and Kafka's `tls_key` support `${env:NAME}` references: literal values are omitted from responses (only `${env:...}` references are echoed back), omitting the fields on update preserves the stored values, and explicitly sending an empty string clears them.

`enabled` and `alerting_enabled` are independent: `enabled` (whether business execution is enabled) defaults to true on create; `alerting_enabled` (whether alert evaluation is allowed; alerting also requires `enabled` true and an alerting-capable type) defaults to true for alerting types and false for diagnostic-only types on create. On update, omitting either preserves the current value and an explicit `null` is invalid; disabling (`enabled` false) is rejected with a conflict when enabled rules reference the datasource. Also note that `payload` is always `null` in `/monit/datasource/list` responses (the list query does not read the payload column); it is populated in create/update/info responses.

<Note>
  All identifiers, service field names, and method names match the generated code. For exactly which methods each service has and their request and response types, rely on `services_gen.go` and the per-service files, plus the [Open API reference](/en/openapi/introduction).
</Note>

## Response timestamps

***

Time fields in responses are no longer bare integers but self-describing `Timestamp` (Unix seconds) or `TimestampMilli` (milliseconds) types. They serialize to RFC3339 strings in the local time zone, so JSON, logs, and LLM-facing output are directly readable; the raw epoch is still one method call away.

* **Serialization (outbound)**: a non-zero value serializes to a quoted RFC3339 string (`TimestampMilli` uses RFC3339Nano to preserve millisecond precision). A zero value serializes to the bare integer `0` — an "unset" sentinel rather than a 1970 date, and dropped by `json:",omitempty"`.
* **Deserialization (inbound)**: it accepts both numeric epoch (the raw wire form) and RFC3339 strings (so a serialized value round-trips losslessly), and also accepts `null` (→ 0).

```go theme={null}
inc := list.Items[0]

fmt.Println(inc.StartTime)            // 2026-05-30T14:37:11+08:00  (String / fmt / TOON)
b, _ := json.Marshal(inc.StartTime)   // "2026-05-30T14:37:11+08:00"
epoch := inc.StartTime.Unix()         // 1779514631  (raw wire value)
t := inc.StartTime.Time()             // time.Time
zero := inc.StartTime.IsZero()        // whether it's the unset sentinel
```

| Method      | Returns     | Description                                                                       |
| ----------- | ----------- | --------------------------------------------------------------------------------- |
| `.Time()`   | `time.Time` | Get the standard time value                                                       |
| `.Unix()`   | `int64`     | Get the raw wire value (`Timestamp` in seconds, `TimestampMilli` in milliseconds) |
| `.IsZero()` | `bool`      | Whether it's the unset sentinel (0)                                               |
| `.String()` | `string`    | RFC3339 in the local time zone; `"0"` when unset                                  |

<Warning>
  **Request-side time fields are still `int64`** — the API expects a numeric epoch on the wire. Note: most endpoints take **seconds**, but RUM and webhook history-related endpoints take **milliseconds**.
</Warning>

## Pagination

***

All list endpoints share `ListOptions`, which you embed in the request struct. Zero values are omitted and never override server defaults (the backend defaults to `p=1`, `limit=20`).

| Field            | Type     | Wire field         | Description                                                                                             |
| ---------------- | -------- | ------------------ | ------------------------------------------------------------------------------------------------------- |
| `Page`           | `int`    | `p`                | 1-based page number                                                                                     |
| `Limit`          | `int`    | `limit`            | Max items returned per page                                                                             |
| `SearchAfterCtx` | `string` | `search_after_ctx` | The opaque cursor echoed by the previous page, for deep pagination; pass it back to fetch the next page |

On the response side, `*Response` carries `Total`, `HasNextPage`, and `SearchAfterCtx`. We recommend walking page by page with the search-after cursor:

```go theme={null}
req := &flashduty.ListIncidentsRequest{
	ListOptions: flashduty.ListOptions{Limit: 50},
}

// Cap iterations to avoid an infinite loop if the cursor misbehaves.
for page := 0; page < 100; page++ {
	list, resp, err := client.Incidents.List(ctx, req)
	if err != nil {
		log.Fatal(err)
	}

	for _, inc := range list.Items {
		fmt.Printf("%s: %s\n", inc.IncidentID, inc.Title)
	}

	if !resp.HasNextPage {
		break
	}
	// Advance the cursor to fetch the next page.
	req.ListOptions.SearchAfterCtx = list.SearchAfterCtx
}
```

## Error handling

***

An unsuccessful call returned by the Flashduty API — whether the envelope carries an error or the HTTP status is non-2xx — returns `*ErrorResponse`. It has `Code`, `Message`, an optional `Reason`, and `RequestID` fields; when troubleshooting, give `RequestID` to the support team to pinpoint the request. `Reason` carries the server-supplied optional cause (the envelope's `DutyError` has the same field, JSON `reason`,omitempty); when non-empty it is also appended to the end of the error string, as `, reason X`.

When the API returns 429, the error is promoted to `*RateLimitError`: it embeds `*ErrorResponse` (so `errors.As` for `*ErrorResponse` still matches) and additionally carries a `RetryAfter` hint.

<Note>
  If you receive a non-2xx response with a non-JSON body, the SDK returns a plain `error`, not `*ErrorResponse`. This indicates that a gateway, load balancer, proxy, or other intermediary produced the response, usually because the request exceeded an intermediary timeout. Retry the request or split a long-running batch into smaller batches, and do not assume that `errors.As(err, &apiErr)` matches this error.
</Note>

```go theme={null}
_, _, err := client.Incidents.Info(ctx, &flashduty.IncidentInfoRequest{
	IncidentID: "does-not-exist",
})

var rl *flashduty.RateLimitError
if errors.As(err, &rl) {
	// Back off as the server requests, then retry.
	time.Sleep(rl.RetryAfter)
	return
}

var apiErr *flashduty.ErrorResponse
if errors.As(err, &apiErr) {
	fmt.Printf("api error code=%s request_id=%s\n", apiErr.Code, apiErr.RequestID)
	return
}
```

Typed predicate functions save string comparisons and see through wrapped errors (using `errors.As` internally):

| Helper                    | Description                                                                                                          |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `IsNotFound(err)`         | Whether the resource does not exist                                                                                  |
| `IsRateLimited(err)`      | Whether requests are too frequent (429)                                                                              |
| `IsUnauthorized(err)`     | Whether unauthorized                                                                                                 |
| `IsAccessDenied(err)`     | Whether access is denied                                                                                             |
| `IsInvalidParameter(err)` | Whether a parameter is invalid                                                                                       |
| `ErrorCodeOf(err)`        | Extract the error code, returning an `ErrorCode` constant (such as `ErrorCodeAccessDenied`, `ErrorCodeUnauthorized`) |

```go theme={null}
switch flashduty.ErrorCodeOf(err) {
case flashduty.ErrorCodeAccessDenied, flashduty.ErrorCodeUnauthorized:
	// Handle auth failure
}
```

## Retry

***

The core client has **no** built-in automatic retry. Compose the optional `retry` subpackage through the transport layer — a safe-by-default retrying `http.RoundTripper`.

Features of `github.com/flashcatcloud/go-flashduty/retry`:

* **Retry conditions**: HTTP 429, any 5xx (status ≥ 500), and transport errors. Other 4xx and all 2xx/3xx return immediately.
* **Backoff policy**: deterministic exponential backoff (`MinWait * 2^attempt`, capped at `MaxWait` each time); no random jitter. When a valid integer `Retry-After` header is present, it takes precedence (also capped at `MaxWait`).
* **Safe replay**: retries only when the request body is replayable (`req.Body` is nil or `req.GetBody` is non-nil), rebuilds the body on a clone of the request for each retry, and never mutates the caller's original `*http.Request`. All requests the SDK builds set `GetBody`, so POST bodies are safely replayable.
* **Respects cancellation**: if the request context is canceled while waiting out a backoff, it returns the context error immediately.

```go theme={null}
import "github.com/flashcatcloud/go-flashduty/retry"

client, err := flashduty.NewClient("YOUR_APP_KEY",
	flashduty.WithTransport(retry.New(
		retry.WithMaxRetries(3),
	)),
)
```

| Option                                   | Default                 | Description                                                           |
| ---------------------------------------- | ----------------------- | --------------------------------------------------------------------- |
| `retry.WithMaxRetries(n int)`            | `3`                     | Max retries after the first attempt; a negative number disables retry |
| `retry.WithMinWait(d time.Duration)`     | `500ms`                 | Base backoff duration (the wait before the first retry)               |
| `retry.WithMaxWait(d time.Duration)`     | `30s`                   | Upper bound on a single backoff wait                                  |
| `retry.WithBase(base http.RoundTripper)` | `http.DefaultTransport` | The underlying RoundTripper that actually performs the request        |

<Tip>
  The `retry` subpackage is pure `net/http` and deliberately does not import the parent `flashduty` package, so it never introduces a circular dependency. Both `retry.New()` and `&retry.Transport{}` (zero value) work out of the box.
</Tip>

## Streaming export

***

`client.Sessions.Export` exports the full event transcript of an AI SRE session, returning an `io.ReadCloser` (an NDJSON stream, `application/x-ndjson`) rather than a JSON envelope. The first line is always a `session_meta` envelope, and each subsequent line is a session event; when `req.IncludeSubagents` is true, each `subagent_dispatch` line is followed by the subagent's own full event stream.

Because the response body can be large, read it line by line and write directly to a file — do **not** buffer the whole transcript into memory. The returned `io.ReadCloser` is the live HTTP response body, held by the caller and which you **must** `Close` (a `defer` close is correct). Pair it with `NewExportScanner` to scan line by line and `DecodeExportLine` to decode a line into an `ExportLine`:

```go theme={null}
rc, _, err := client.Sessions.Export(ctx, &flashduty.SessionExportRequest{
	SessionID:        "your-session-id",
	IncludeSubagents: true,
})
if err != nil {
	return err
}
defer rc.Close()

sc := flashduty.NewExportScanner(rc)
for sc.Scan() {
	line, err := flashduty.DecodeExportLine(sc.Bytes())
	if err != nil {
		return err
	}
	// Use line.Type to distinguish: session_meta, user_message, llm_call,
	// tool_call, subagent_dispatch, final_answer, agent_text, error
	_ = line
	// You can also write sc.Bytes() to a file as-is.
}
return sc.Err()
```

<Note>
  `NewExportScanner` is configured with a per-line buffer large enough to hold the wider event lines in a transcript (such as tool output or LLM calls), free of the default 64KB token limit. On any non-2xx status, the response body is still a regular JSON error envelope — `Export` reads and closes it and returns a typed error (`*ErrorResponse`, or `*RateLimitError` on 429), with the `io.ReadCloser` being `nil`, consistent with the other generated endpoints.
</Note>
