<!-- source: https://docs.redpine.ai/docs/sdks -->
# SDKs

> Official Python, TypeScript and Go clients for the Redpine API.

Typed clients for the three languages most integrations are written in. All three are generated from the same OpenAPI spec as the [API reference](/docs/api-reference), so they expose the same endpoints, field names and errors as the REST API.

| Language             | Package                                | Source                                                                            |
| -------------------- | -------------------------------------- | --------------------------------------------------------------------------------- |
| Python 3.10+         | `redpine-sdk`, import `redpine`        | [redpine-ai/redpine-sdk-python](https://github.com/redpine-ai/redpine-sdk-python) |
| TypeScript, Node 20+ | `@redpine-ai/sdk`                      | [redpine-ai/redpine-sdk-js](https://github.com/redpine-ai/redpine-sdk-js)         |
| Go 1.24+             | `github.com/redpine-ai/redpine-sdk-go` | [redpine-ai/redpine-sdk-go](https://github.com/redpine-ai/redpine-sdk-go)         |

<Callout title="Early release">
  The SDKs are at 0.x. The surface is stable for search, assisted search, collections and quota, but expect additive changes before 1.0. Report problems as issues on the language repository.
</Callout>

## Install

<CodeBlockTabs defaultValue="Python" groupId="lang">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="TypeScript">
      TypeScript
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Go">
      Go
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="Python">
    ```bash  
    pip install redpine-sdk
    ```
  </CodeBlockTab>

  <CodeBlockTab value="TypeScript">
    ```bash  
    npm install @redpine-ai/sdk
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Go">
    ```bash  
    go get github.com/redpine-ai/redpine-sdk-go@latest
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## Your first request

Create an API key from your [API Keys](https://app.redpine.ai/api-keys?utm_source=docs\&utm_medium=referral) page. Start with a **sandbox** key (`sk_test_`): it returns canned results at no cost, so you can get the request shape right without spending credits. Swapping it for a live key (`sk_live_`) later changes nothing else about your integration.

Put it in your environment. The clients read it from there; cURL passes it in the header.

```bash
export REDPINE_API_KEY=sk_test_YOUR_SANDBOX_KEY
```

Then search a collection with a query. That is the whole first request. Don't know a collection name yet? `GET /api/v1/search/collections` lists every collection your key can reach, at no cost. See [Collections](/docs/collections).

<CodeBlockTabs defaultValue="cURL" groupId="first-request">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="cURL">
      cURL
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="TypeScript">
      TypeScript
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Go">
      Go
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="cURL">
    ```bash  
    curl -X POST "https://api.redpine.ai/api/v1/search/query" \
      -H "Authorization: Bearer $REDPINE_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"collection": "my-collection", "query": "your search query"}'
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Python">
    ```python  
    from redpine import Redpine

    client = Redpine()  # reads REDPINE_API_KEY

    r = client.search("your search query", collection="my-collection")
    for hit in r.results:
        print(hit.text[:200])
    ```
  </CodeBlockTab>

  <CodeBlockTab value="TypeScript">
    ```typescript  
    import { Redpine } from "@redpine-ai/sdk";

    const client = new Redpine(); // reads REDPINE_API_KEY

    const r = await client.search("your search query", { collection: "my-collection" });
    for (const hit of r.results) {
      console.log(hit.text.slice(0, 200));
    }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Go">
    ```go  
    package main

    import (
        "context"
        "fmt"

        redpine "github.com/redpine-ai/redpine-sdk-go"
    )

    func main() {
        client, err := redpine.New() // reads REDPINE_API_KEY
        if err != nil {
            panic(err)
        }

        r, err := client.Search(context.Background(), "your search query",
            redpine.SearchOptions{Collection: "my-collection"})
        if err != nil {
            panic(err)
        }
        for _, hit := range r.Results {
            fmt.Println(hit.Text)
        }
    }
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Results come back ranked by relevance. Each one carries the matched text, the collection it came from, and any metadata from the original document. Because this used a sandbox key the results are synthetic: note `synthetic` and the `sandbox-` id. A live key returns the same shape with real content.

```json
{
  "results": [
    {
      "id": "sandbox-0001-chunk-0",
      "text": "Lorem ipsum dolor sit amet...",
      "collection": "my-collection",
      "metadata": { "title": "Lorem ipsum dolor", "synthetic": true }
    }
  ],
  "queryId": "qry_a1b2c3d4e5f6",
  "latencyMs": 42
}
```

## Configure

Every client reads `REDPINE_API_KEY` from the environment. Pass the key explicitly if you would rather not use the environment. A sandbox key (`sk_test_`) works everywhere a live key does and costs nothing. See [Authentication](/docs/authentication).

<CodeBlockTabs defaultValue="Python" groupId="lang">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="TypeScript">
      TypeScript
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Go">
      Go
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="Python">
    ```python  
    from redpine import Redpine

    client = Redpine()                              # reads REDPINE_API_KEY
    client = Redpine(api_key="sk_test_...", timeout=30.0, max_retries=2)
    ```
  </CodeBlockTab>

  <CodeBlockTab value="TypeScript">
    ```typescript  
    import { Redpine } from "@redpine-ai/sdk";

    const client = new Redpine();                   // reads REDPINE_API_KEY
    const client2 = new Redpine({ apiKey: "sk_test_...", timeoutMs: 30_000, maxRetries: 2 });
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Go">
    ```go  
    import redpine "github.com/redpine-ai/redpine-sdk-go"

    client, err := redpine.New()                    // reads REDPINE_API_KEY
    client, err = redpine.New(
        redpine.WithAPIKey("sk_test_..."),
        redpine.WithTimeout(30*time.Second),
        redpine.WithMaxRetries(2),
    )
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Without a timeout of your own, the Python and Go clients wait 30 seconds for each call and 120 seconds for assisted search, which can take longer on a [long query](/docs/assisted-search#long-queries). A timeout you set applies to every call, assisted search included. The TypeScript client sets no timeout unless you pass `timeoutMs`.

## Search

Pass exactly one of `collection` (a single collection) or `collections` (several at once). The response shape is the one documented under [Search](/docs/search): `results`, `queryId`, `latencyMs`. Python exposes the same fields in snake case.

<CodeBlockTabs defaultValue="Python" groupId="lang">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="TypeScript">
      TypeScript
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Go">
      Go
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="Python">
    ```python  
    r = client.search("your search query", collection="my-collection", limit=10)
    for hit in r.results:
        print(hit.id, hit.text[:200])
    print(r.query_id)
    ```
  </CodeBlockTab>

  <CodeBlockTab value="TypeScript">
    ```typescript  
    const r = await client.search("your search query", { collection: "my-collection", limit: 10 });
    for (const hit of r.results) {
      console.log(hit.id, hit.text.slice(0, 200));
    }
    console.log(r.queryId);
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Go">
    ```go  
    r, err := client.Search(ctx, "your search query", redpine.SearchOptions{
        Collection: "my-collection",
        Limit:      10,
    })
    if err != nil {
        return err
    }
    for _, hit := range r.Results {
        fmt.Println(hit.Id, hit.Text)
    }
    fmt.Println(r.QueryId)
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Don't know a collection name? `collections()` lists every collection the key can reach, at no cost.

<CodeBlockTabs defaultValue="Python" groupId="lang">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="TypeScript">
      TypeScript
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Go">
      Go
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="Python">
    ```python  
    for c in client.collections().collections:
        print(c.name)
    ```
  </CodeBlockTab>

  <CodeBlockTab value="TypeScript">
    ```typescript  
    for (const c of (await client.collections()).collections) console.log(c.name);
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Go">
    ```go  
    cs, err := client.Collections(ctx)
    for _, c := range cs.Collections {
        fmt.Println(c.Name)
    }
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## Filters

`F` builds the structured filter documented in [Filtering](/docs/filtering) without hand-writing JSON. Operators are `eq`, `ne`, `in`, `not_in`, `gt`, `gte`, `lt`, `lte` and `between`. Combine with and, or and not. A raw filter object is accepted anywhere a built filter is.

<CodeBlockTabs defaultValue="Python" groupId="lang">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="TypeScript">
      TypeScript
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Go">
      Go
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="Python">
    ```python  
    from redpine import F

    recent_reviews = (
        F("publication_date").gte("2022-01-01")
        & F("article_type").eq("review")
        & ~F("issn").in_(["1234-5679"])
    )
    r = client.search("crispr delivery", collections=["corpus"], filters=recent_reviews)
    ```
  </CodeBlockTab>

  <CodeBlockTab value="TypeScript">
    ```typescript  
    import { F } from "@redpine-ai/sdk";

    const recentReviews = F("publication_date").gte("2022-01-01")
      .and(F("article_type").eq("review"))
      .and(F("issn").in(["1234-5679"]).not());
    const r = await client.search("crispr delivery", { collections: ["corpus"], filters: recentReviews });
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Go">
    ```go  
    recentReviews := redpine.F("publication_date").Gte("2022-01-01").
        And(redpine.F("article_type").Eq("review")).
        And(redpine.F("issn").In("1234-5679").Not())
    r, err := client.Search(ctx, "crispr delivery", redpine.SearchOptions{
        Collections: []string{"corpus"},
        Filters:     recentReviews,
    })
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## Assisted search

[Assisted search](/docs/assisted-search) plans its own searches and verifies every result. Check `status` before reading `results`: it is `results`, `clarification_needed` or `no_relevant_results`.

<CodeBlockTabs defaultValue="Python" groupId="lang">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="TypeScript">
      TypeScript
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Go">
      Go
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="Python">
    ```python  
    a = client.assisted_search("what delivery vectors work for in vivo crispr?", collection="my-collection")
    if a.status == "results":
        for hit in a.results:
            print(hit.relevance, hit.text[:200])
    elif a.status == "clarification_needed":
        print(a.clarification.question)
    ```
  </CodeBlockTab>

  <CodeBlockTab value="TypeScript">
    ```typescript  
    const a = await client.assistedSearch("what delivery vectors work for in vivo crispr?", { collection: "my-collection" });
    if (a.status === "results") {
      for (const hit of a.results ?? []) console.log(hit.relevance, hit.text.slice(0, 200));
    } else if (a.status === "clarification_needed") {
      console.log(a.clarification?.question);
    }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Go">
    ```go  
    a, err := client.AssistedSearch(ctx, "what delivery vectors work for in vivo crispr?", redpine.AssistedSearchOptions{
        Collection: "my-collection",
    })
    if err != nil {
        return err
    }
    switch a.Status {
    case "results":
        for _, hit := range *a.Results {
            fmt.Println(hit.Relevance, hit.Text)
        }
    case "clarification_needed":
        fmt.Println(a.Clarification.Question)
    }
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## Preview and unlock

[Preview](/docs/preview-unlock) runs the search for free and returns teaser rows with a price per row; `unlock` pays for the rows you choose. Both return the same shape: `results` with `locked`, `tokens` and `cost` per row, plus `costToUnlockRemaining` and `costCharged`.

<CodeBlockTabs defaultValue="Python" groupId="lang">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="TypeScript">
      TypeScript
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Go">
      Go
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="Python">
    ```python  
    p = client.preview("your search query", collection="my-collection")
    worth_it = [row.id for row in p.results if isinstance(row.tokens, int) and row.tokens < 300]
    u = client.unlock(p.query_id, result_ids=worth_it)   # result_ids=None unlocks every row
    for row in u.results:
        if not row.locked:
            print(row.text)
    print("charged", u.cost_charged)
    ```
  </CodeBlockTab>

  <CodeBlockTab value="TypeScript">
    ```typescript  
    const p = await client.preview("your search query", { collection: "my-collection" });
    const worthIt = p.results.filter((row) => (row.tokens ?? Infinity) < 300).map((row) => row.id);
    const u = await client.unlock(p.queryId, worthIt); // omit resultIds to unlock every row
    for (const row of u.results) {
      if (!row.locked) console.log(row.text);
    }
    console.log("charged", u.costCharged);
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Go">
    ```go  
    p, err := client.Preview(ctx, "your search query", redpine.PreviewOptions{Collection: "my-collection"})
    if err != nil {
        return err
    }
    var worthIt []string
    for _, row := range p.Results {
        if row.Tokens != nil && *row.Tokens < 300 {
            worthIt = append(worthIt, row.Id)
        }
    }
    u, err := client.Unlock(ctx, p.QueryId, worthIt) // nil unlocks every row
    if err != nil {
        return err
    }
    for _, row := range u.Results {
        if !row.Locked {
            fmt.Println(row.Text)
        }
    }
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Re-sending an id you already paid for within the same preview (`queryId`) costs nothing, so retrying that unlock is safe. Unlocking the same result from a new preview, or receiving it again through a direct search, charges again.

## Re-fetch results and check quota

Every search returns a `queryId`. Fetching it again within 7 days returns the same results without a new charge. Quota returns the daily and monthly counters described in [Rate limits](/docs/rate-limits).

<CodeBlockTabs defaultValue="Python" groupId="lang">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="TypeScript">
      TypeScript
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Go">
      Go
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="Python">
    ```python  
    again = client.get_results(r.query_id)
    q = client.quota()
    print(q.daily_remaining, q.monthly_remaining)
    ```
  </CodeBlockTab>

  <CodeBlockTab value="TypeScript">
    ```typescript  
    const again = await client.getResults(r.queryId);
    const q = await client.quota();
    console.log(q.dailyRemaining, q.monthlyRemaining);
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Go">
    ```go  
    again, err := client.GetResults(ctx, r.QueryId)
    q, err := client.Quota(ctx)
    fmt.Println(q.DailyRemaining, q.MonthlyRemaining)
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## Errors and retries

Non-2xx responses raise one typed error per status, carrying `status`, `code`, `message` and `requestId` from the [error envelope](/docs/errors). The clients retry `429` and `503` automatically, honouring `Retry-After` and otherwise backing off exponentially with jitter, up to `maxRetries` (default 2). Nothing else is retried.

| HTTP  | Python                          | TypeScript                     | Go                                   |
| ----- | ------------------------------- | ------------------------------ | ------------------------------------ |
| 401   | `AuthError`                     | `AuthError`                    | `*AuthError`                         |
| 402   | `InsufficientCredits`           | `InsufficientCredits`          | `*InsufficientCreditsError`          |
| 403   | `AccessDenied`                  | `AccessDenied`                 | `*AccessDeniedError`                 |
| 404   | `NotFound`                      | `NotFound`                     | `*NotFoundError`                     |
| 410   | `Expired`                       | `Expired`                      | `*ExpiredError`                      |
| 422   | `ValidationError`               | `ValidationError`              | `*ValidationError`                   |
| 429   | `QuotaExceeded` (`retry_after`) | `QuotaExceeded` (`retryAfter`) | `*QuotaExceededError` (`RetryAfter`) |
| 503   | `AssistedUnavailable`           | `AssistedUnavailable`          | `*AssistedUnavailableError`          |
| other | `RedpineError`                  | `RedpineError`                 | `*APIError`                          |

<CodeBlockTabs defaultValue="Python" groupId="lang">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="TypeScript">
      TypeScript
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Go">
      Go
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="Python">
    ```python  
    from redpine import QuotaExceeded, RedpineError

    try:
        r = client.search("...", collection="my-collection")
    except QuotaExceeded as e:
        print("retry after", e.retry_after)
    except RedpineError as e:
        print(e.status, e.code, e.message, e.request_id)
    ```
  </CodeBlockTab>

  <CodeBlockTab value="TypeScript">
    ```typescript  
    import { QuotaExceeded, RedpineError } from "@redpine-ai/sdk";

    try {
      await client.search("...", { collection: "my-collection" });
    } catch (e) {
      if (e instanceof QuotaExceeded) console.log("retry after", e.retryAfter);
      else if (e instanceof RedpineError) console.log(e.status, e.code, e.message, e.requestId);
      else throw e;
    }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Go">
    ```go  
    r, err := client.Search(ctx, "...", redpine.SearchOptions{Collection: "my-collection"})
    var quota *redpine.QuotaExceededError
    var apiErr *redpine.APIError
    if errors.As(err, &quota) {
        fmt.Println("retry after", quota.RetryAfter)
    } else if errors.As(err, &apiErr) {
        fmt.Println(apiErr.Status, apiErr.Code, apiErr.Message, apiErr.RequestID)
    }
    ```
  </CodeBlockTab>
</CodeBlockTabs>

## Async Python

`AsyncRedpine` mirrors `Redpine` method for method on top of `httpx`. Use it as an async context manager or call `aclose()` when done.

```python
from redpine import AsyncRedpine

async with AsyncRedpine() as client:
    r = await client.search("your search query", collection="my-collection")
```

## Which one should I use?

Calling the API from a language not listed here, or from an environment where a dependency is unwelcome? The REST API in [Get started](/) needs nothing beyond an HTTP client. Connecting an AI agent rather than writing code? Use [MCP](/docs/mcp).