{"id":50307249,"url":"https://github.com/hra42/kagi-go-sdk","last_synced_at":"2026-05-28T17:30:40.812Z","repository":{"id":357958360,"uuid":"1239278320","full_name":"hra42/kagi-go-sdk","owner":"hra42","description":null,"archived":false,"fork":false,"pushed_at":"2026-05-15T02:23:28.000Z","size":25,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-15T03:21:58.948Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hra42.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-05-15T00:03:38.000Z","updated_at":"2026-05-15T02:23:30.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/hra42/kagi-go-sdk","commit_stats":null,"previous_names":["hra42/kagi-go-sdk"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/hra42/kagi-go-sdk","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hra42%2Fkagi-go-sdk","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hra42%2Fkagi-go-sdk/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hra42%2Fkagi-go-sdk/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hra42%2Fkagi-go-sdk/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hra42","download_url":"https://codeload.github.com/hra42/kagi-go-sdk/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hra42%2Fkagi-go-sdk/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33619965,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-28T02:00:06.440Z","response_time":99,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-28T17:30:38.504Z","updated_at":"2026-05-28T17:30:40.805Z","avatar_url":"https://github.com/hra42.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# kagi-go-sdk\n\n[![CI](https://github.com/hra42/kagi-go-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/hra42/kagi-go-sdk/actions/workflows/ci.yml)\n[![Go Reference](https://pkg.go.dev/badge/github.com/hra42/kagi-go-sdk.svg)](https://pkg.go.dev/github.com/hra42/kagi-go-sdk)\n[![Go Version](https://img.shields.io/badge/go-1.26%2B-00ADD8?logo=go)](go.mod)\n[![License: Unlicense](https://img.shields.io/badge/license-Unlicense-blue.svg)](LICENSE)\n\nA handbuilt, idiomatic Go SDK for the [Kagi Search API](https://help.kagi.com/kagi/api/overview.html).\n\nDesigned to be ergonomic, production-ready, and easy to vendor — covering the full API surface with clean types, transparent retries, rate-limit handling, and proper context propagation.\n\n- [Why this SDK](#why-this-sdk)\n- [vs. `kagisearch/kagi-openapi-golang`](#vs-kagisearchkagi-openapi-golang)\n- [Install](#install)\n- [Quick start](#quick-start)\n- [API reference](#api-reference)\n- [Configuration](#configuration)\n- [Error handling](#error-handling)\n- [Retries and rate limits](#retries-and-rate-limits)\n- [Runnable examples](#runnable-examples)\n- [Contributing](#contributing)\n- [Security](#security)\n- [License](#license)\n\n## Why this SDK\n\n- **Zero external dependencies.** Standard library only — `go.mod` has no `require` entries beyond the Go version itself. Safe to vendor, trivial to audit.\n- **Idiomatic Go.** Functional options, typed request/response structs, `context.Context` on every call, an `errors.Is` / `errors.As`-compatible error hierarchy.\n- **Production-ready.** Transparent retries with exponential backoff plus full jitter, `Retry-After` honoring, parsed error envelopes with trace IDs.\n- **Handwritten, not generated.** Clean field names, real godoc on every export, no pointer soup, typed enums instead of bare string constants.\n\n## vs. `kagisearch/kagi-openapi-golang`\n\nKagi publishes [an official client](https://github.com/kagisearch/kagi-openapi-golang) generated from their OpenAPI spec. It is the right choice if you want lockstep with the spec; pick this SDK when you'd rather have ergonomics and built-in resilience.\n\n| | `kagi-go-sdk` (this) | `kagisearch/kagi-openapi-golang` |\n|---|---|---|\n| Source | Handwritten | OpenAPI-generated |\n| External dependencies | None | Generator runtime + transitive deps |\n| Error model | Sentinel hierarchy (`ErrUnauthorized`, `ErrRateLimited`, `ErrBadRequest`, `ErrServerError`) wrapped by `*APIError` | Generic `*GenericOpenAPIError` |\n| Enums | Typed (`Workflow`, `TimeRelative`, `DomainRuleKind`) | String constants |\n| Retries on 429 / 5xx | Built-in, transparent, jittered backoff, honors `Retry-After` | Not provided — caller's responsibility |\n| `context.Context` on every call | Yes | Yes |\n| Godoc coverage | Every exported symbol | Generated stubs |\n| Vendoring footprint | Single module | Module + generated client tree |\n\n## Install\n\n```sh\ngo get github.com/hra42/kagi-go-sdk\n```\n\nRequires **Go 1.26+**.\n\n## Quick start\n\n```go\npackage main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\n\tkagi \"github.com/hra42/kagi-go-sdk\"\n)\n\nfunc main() {\n\tclient := kagi.NewClient(os.Getenv(\"KAGI_API_KEY\"))\n\n\tres, err := client.Search(context.Background(), kagi.SearchRequest{\n\t\tQuery: \"context propagation in go\",\n\t})\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tfor _, hit := range res.Data.Search {\n\t\tfmt.Printf(\"%s\\n  %s\\n\\n\", hit.Title, hit.URL)\n\t}\n}\n```\n\n## API reference\n\nThe full API surface is two methods on `*Client`. Every method takes a `context.Context` and a typed request struct, returns a typed response struct, and reports errors as `*APIError` values wrapping one of four sentinel errors (see [Error handling](#error-handling)).\n\n### `Client.Search`\n\n```go\nfunc (c *Client) Search(ctx context.Context, req SearchRequest) (*SearchResult, error)\n```\n\n`SearchRequest` fields:\n\n| Field | Type | Notes |\n|---|---|---|\n| `Query` | `string` | **Required.** The search query. |\n| `Workflow` | `Workflow` | `WorkflowSearch` (default) / `WorkflowImages` / `WorkflowVideos` / `WorkflowNews` / `WorkflowPodcasts`. |\n| `LensID` | `string` | Saved-lens ID or full `https://kagi.com/lenses/...` URL. |\n| `Lens` | `*Lens` | Inline lens (site/keyword filters, file type, time window, region). |\n| `Filters` | `*SearchFilters` | Coarse filters: `Region`, `After`, `Before`. Take priority over `Lens`. |\n| `Extract` | `*SearchExtract` | Opt in to inline page extraction for top results (`Count`, `Timeout`). Billed at the Extract rate. |\n| `Personalizations` | `*Personalizations` | Per-request `DomainRule` and `RegexRule` ranking tweaks (up to 1000 each). |\n| `Page` | `int` | 1-indexed page, server range 1..10. |\n| `Limit` | `int` | Result cap, server range 1..1024. |\n| `Timeout` | `float64` | Server-side time budget in seconds (0.5..4). |\n| `SafeSearch` | `*bool` | Override server default (`true`). Leave `nil` to inherit. |\n\n`SearchResult.Data` groups hits into category buckets — `Search`, `Image`, `Video`, `News`, `Podcast`, `PodcastCreator`, `AdjacentQuestion`, `DirectAnswer`, `InterestingNews`, `InterestingFinds`, `Infobox`, `Code`, `PackageTracking`, `PublicRecords`, `Weather`, `RelatedSearch`, `Listicle`, `WebArchive`. Each is a `[]SearchHit`; category-specific extras are exposed as raw JSON via `SearchHit.Props`.\n\nA richer call combining a lens, ranking rules, and inline extraction:\n\n```go\nsafe := false\nres, err := client.Search(ctx, kagi.SearchRequest{\n\tQuery: \"kagi search api\",\n\tLens: \u0026kagi.Lens{\n\t\tSitesIncluded: []string{\"help.kagi.com\"},\n\t\tTimeRelative:  kagi.TimeRelativeMonth,\n\t},\n\tPersonalizations: \u0026kagi.Personalizations{\n\t\tDomains: []kagi.DomainRule{\n\t\t\t{Domain: \"blog.kagi.com\", Kind: kagi.DomainRuleRaise},\n\t\t\t{Domain: \"reddit.com\", Kind: kagi.DomainRuleBlock},\n\t\t},\n\t},\n\tExtract:    \u0026kagi.SearchExtract{Count: 3, Timeout: 2.0},\n\tLimit:      10,\n\tSafeSearch: \u0026safe,\n})\n```\n\n### `Client.Extract`\n\n```go\nfunc (c *Client) Extract(ctx context.Context, req ExtractRequest) (*ExtractResult, error)\n```\n\n`ExtractRequest` fields:\n\n| Field | Type | Notes |\n|---|---|---|\n| `Pages` | `[]ExtractPage` | **Required.** 1..10 entries; each `URL` must be HTTPS. |\n| `Timeout` | `float64` | Bulk time budget in seconds (0.5..10). |\n\nA 200 response can include both successful `Data` entries and per-URL failures in `Errors`; partial success is not converted into a Go error.\n\n```go\nres, err := client.Extract(ctx, kagi.ExtractRequest{\n\tPages: []kagi.ExtractPage{\n\t\t{URL: \"https://blog.kagi.com/kagi-search-api\"},\n\t\t{URL: \"https://help.kagi.com/kagi/api/search.html\"},\n\t},\n})\nif err != nil {\n\tlog.Fatal(err)\n}\n\nfor _, p := range res.Data {\n\tfmt.Printf(\"=== %s ===\\n%s\\n\", p.URL, p.Markdown)\n}\nfor _, e := range res.Errors {\n\tfmt.Printf(\"failed %s: %s\\n\", e.Location, e.Message)\n}\n```\n\n### Enums\n\n| Type | Values |\n|---|---|\n| `Workflow` | `WorkflowSearch`, `WorkflowImages`, `WorkflowVideos`, `WorkflowNews`, `WorkflowPodcasts` |\n| `TimeRelative` | `TimeRelativeDay`, `TimeRelativeWeek`, `TimeRelativeMonth` |\n| `DomainRuleKind` | `DomainRuleBlock`, `DomainRuleLower`, `DomainRuleRaise`, `DomainRulePin` |\n\n## Configuration\n\nThe client uses the functional options pattern:\n\n```go\nclient := kagi.NewClient(apiKey,\n\tkagi.WithTimeout(30*time.Second),\n\tkagi.WithRetries(3),\n\tkagi.WithBackoff(500*time.Millisecond, 30*time.Second),\n\tkagi.WithUserAgent(\"my-app/1.0\"),\n\tkagi.WithHTTPClient(myHTTPClient),\n\tkagi.WithBaseURL(\"https://kagi.com/api/v1\"),\n)\n```\n\n| Option | Default | Effect |\n|---|---|---|\n| `WithTimeout(d time.Duration)` | `30s` | Sets `http.Client.Timeout`. Ignored when `d \u003c= 0`. |\n| `WithHTTPClient(c *http.Client)` | `\u0026http.Client{Timeout: 30s}` | Replaces the transport. The client is shallow-copied at construction, so later mutation of your `*http.Client` does not affect the SDK. |\n| `WithBaseURL(s string)` | `https://kagi.com/api/v1` | Override endpoint. Must be an absolute URL with scheme + host; invalid values are silently ignored. |\n| `WithRetries(n int)` | `3` | Max retries on 429 / 5xx / transient network errors. `0` disables retries; negative values clamp to `0`. |\n| `WithBackoff(base, max time.Duration)` | `500ms`, `30s` | Exponential backoff bounds; each delay is jittered uniformly in `[0, window)`. `max` is also the ceiling for `Retry-After`. Non-positive values keep the default. |\n| `WithUserAgent(s string)` | `kagi-go-sdk` | `User-Agent` header. Whitespace-only values are ignored. |\n\nThe `*Client` is safe for concurrent reuse — construct it once and share it.\n\n## Error handling\n\nNon-2xx responses are returned as `*APIError`, which unwraps to one of four sentinel errors:\n\n| Sentinel | Trigger |\n|---|---|\n| `ErrUnauthorized` | HTTP 401 — missing or invalid API key. |\n| `ErrRateLimited` | HTTP 429 — quota exhausted. `APIError.RetryAfter` is set when the server provides a `Retry-After` header. |\n| `ErrBadRequest` | HTTP 400 and other client-side 4xx (except 401). |\n| `ErrServerError` | HTTP 5xx — treated as transient by the retry layer. |\n\nUse `errors.Is` to classify and `errors.As` to read the response detail:\n\n```go\nres, err := client.Search(ctx, req)\nif err != nil {\n\tswitch {\n\tcase errors.Is(err, kagi.ErrRateLimited):\n\t\tvar apiErr *kagi.APIError\n\t\tif errors.As(err, \u0026apiErr) {\n\t\t\tlog.Printf(\"rate limited, retry after %s (trace %s)\", apiErr.RetryAfter, apiErr.TraceID)\n\t\t}\n\tcase errors.Is(err, kagi.ErrUnauthorized):\n\t\tlog.Fatal(\"check KAGI_API_KEY\")\n\tcase errors.Is(err, kagi.ErrBadRequest):\n\t\tvar apiErr *kagi.APIError\n\t\tif errors.As(err, \u0026apiErr) {\n\t\t\tfor _, d := range apiErr.Details {\n\t\t\t\tlog.Printf(\"  %s @ %s: %s\", d.Code, d.Location, d.Message)\n\t\t\t}\n\t\t}\n\tdefault:\n\t\tlog.Printf(\"transport or server error: %v\", err)\n\t}\n\treturn\n}\n```\n\nUseful fields on `*APIError`:\n\n- `StatusCode int` / `Status string` — raw HTTP status.\n- `Kind error` — the sentinel this wraps.\n- `Details []ErrorDetail` — parsed entries (`Code`, `URL`, `Message`, `Location`) from the response envelope.\n- `RetryAfter time.Duration` — populated for 429 and 5xx when a `Retry-After` header is present.\n- `TraceID string` — `meta.trace` from the response envelope; include this when contacting Kagi support.\n- `Body []byte` — raw (capped) response body, preserved when the envelope failed to parse.\n\n## Retries and rate limits\n\nRequests that fail with HTTP 429, 5xx, or a transient network error are retried transparently. The retry layer:\n\n- Honors any server-provided `Retry-After` header (delta-seconds or HTTP-date), capped at `WithBackoff`'s `max`.\n- Otherwise sleeps with exponential backoff plus full jitter: each attempt's window doubles up to `max`, and the actual delay is uniform in `[0, window)`.\n- Aborts immediately if the request context is cancelled.\n\nOnly the final failure after the retry budget is exhausted is returned to the caller. Set `WithRetries(0)` to disable.\n\nFor manual rate-limit handling (for example, deferring work to a queue), check `apiErr.RetryAfter` once the retry budget has been exhausted.\n\n## Runnable examples\n\nEach example is a self-contained `package main`. Set `KAGI_API_KEY` and run:\n\n```sh\ngo run ./_examples/search           # minimal search\ngo run ./_examples/extract          # markdown extraction with per-URL error handling\ngo run ./_examples/search-advanced  # lens, filters, domain rules, inline extract\ngo run ./_examples/custom-client    # all Options + custom *http.Client + APIError classification\n```\n\n## Contributing\n\nPlease read [`CONTRIBUTING.md`](CONTRIBUTING.md) before opening a PR. Short version: zero external dependencies — production code **and** tests — and every new exported symbol carries godoc.\n\n## Security\n\nPlease do not file public issues for security reports. See [`SECURITY.md`](SECURITY.md) — report privately via GitHub's [security advisory flow](https://github.com/hra42/kagi-go-sdk/security/advisories/new).\n\n## License\n\nReleased into the public domain under the [Unlicense](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhra42%2Fkagi-go-sdk","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhra42%2Fkagi-go-sdk","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhra42%2Fkagi-go-sdk/lists"}