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

# Get Streams (Bulk)

> Retrieve confirmed streams using multi-valued filters in the request body, with optional pagination

## Overview

`POST /v2/streams/bulk` returns confirmed streams using the same `Stream` objects as [Get Streams](/sdk/streams/get-streams). This is a public endpoint. No API key is required.

It uses `POST` only so that multi-valued filters can be sent in the request body. Despite the verb, it is a **read-only query**: nothing is created or modified, and the result set is **not** scoped to any API key's organization.

This endpoint is the bulk counterpart of [Get Streams](/sdk/streams/get-streams). Reach for it when [Get Streams](/sdk/streams/get-streams) is too limited, specifically when you need to:

1. **Filter by multiple values at once**: several stream IDs, several organizations, one or more target-token chains, or a set of `(chainId, address)` target tokens, instead of the single-value query params accepted by [Get Streams](/sdk/streams/get-streams).
2. **Paginate** the result set with the optional `page` and `limit` query params.

Filters of different kinds are combined with `AND`; multiple values within a single filter are combined with `OR`. As with [Get Streams](/sdk/streams/get-streams), only confirmed streams are ever returned.

<Info>
  `targetChainIds` and `targetTokens` filter on each stream's **target token** (the token tracked via `customArgs.targetTokenId`), not its reward token. Use the decimal EVM chain IDs listed in [Get Tokens](/sdk/streams/get-tokens).
</Info>

## Endpoint

### Filter by multiple stream IDs

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST "https://earn.turtle.xyz/v2/streams/bulk" \
    -H "Content-Type: application/json" \
    -d '{
      "ids": [
        "550e8400-e29b-41d4-a716-446655440000",
        "6f0f2a1c-1c2d-4f3e-9a8b-7c6d5e4f3a2b"
      ]
    }'
  ```

  ```typescript TypeScript theme={null}
  const response = await fetch('https://earn.turtle.xyz/v2/streams/bulk', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      ids: [
        '550e8400-e29b-41d4-a716-446655440000',
        '6f0f2a1c-1c2d-4f3e-9a8b-7c6d5e4f3a2b',
      ],
    }),
  });

  const data = await response.json();
  ```
</CodeGroup>

### Filter by chain and token (no org ID)

This is the no-auth-context path for a frontend that only needs to read reward streams for specific tokens on specific chains. It passes no distributor or organization ID and needs no API key. Send `targetChainIds`, `targetTokens`, or both.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST "https://earn.turtle.xyz/v2/streams/bulk?limit=50" \
    -H "Content-Type: application/json" \
    -d '{
      "targetChainIds": [1, 8453],
      "targetTokens": [
        { "chainId": 1, "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" }
      ]
    }'
  ```

  ```typescript TypeScript theme={null}
  const response = await fetch(
    'https://earn.turtle.xyz/v2/streams/bulk?limit=50',
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        targetChainIds: [1, 8453],
        targetTokens: [
          { chainId: 1, address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' },
        ],
      }),
    }
  );

  const data = await response.json();
  ```
</CodeGroup>

### Filter by organizations and target tokens

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST "https://earn.turtle.xyz/v2/streams/bulk" \
    -H "Content-Type: application/json" \
    -d '{
      "organizationIds": ["9f51b66a-d13a-4b55-8515-ae6e4ef7cf25"],
      "targetChainIds": [1, 8453],
      "targetTokens": [
        { "chainId": 1, "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" }
      ],
      "withSnapshots": false,
      "usersCount": true
    }'
  ```

  ```typescript TypeScript theme={null}
  const response = await fetch('https://earn.turtle.xyz/v2/streams/bulk', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      organizationIds: ['9f51b66a-d13a-4b55-8515-ae6e4ef7cf25'],
      targetChainIds: [1, 8453],
      targetTokens: [
        { chainId: 1, address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' },
      ],
      withSnapshots: false,
      usersCount: true,
    }),
  });

  const data = await response.json();
  ```
</CodeGroup>

### Paginate the result set

Pagination is opt-in. Send `page` and/or `limit` as **query parameters** to receive one page plus a `pagination` object in the response.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST "https://earn.turtle.xyz/v2/streams/bulk?page=1&limit=50" \
    -H "Content-Type: application/json" \
    -d '{ "organizationIds": ["9f51b66a-d13a-4b55-8515-ae6e4ef7cf25"] }'
  ```

  ```typescript TypeScript theme={null}
  const response = await fetch(
    'https://earn.turtle.xyz/v2/streams/bulk?page=1&limit=50',
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        organizationIds: ['9f51b66a-d13a-4b55-8515-ae6e4ef7cf25'],
      }),
    }
  );

  const data = await response.json();
  ```
</CodeGroup>

## Parameters

### Request Body

All body fields are optional. Sending an empty body is allowed, but only together with a bounded `limit` query parameter (see [Response Semantics](#response-semantics)).

<ParamField body="ids" type="uuid[]">
  Filter by stream IDs. Returns streams whose `id` is in this list.
</ParamField>

<ParamField body="organizationIds" type="uuid[]">
  Filter by owning organization IDs. Returns streams belonging to any of these organizations.
</ParamField>

<ParamField body="targetChainIds" type="integer[]">
  Filter by streams whose target token is on any of these chains. Values are decimal EVM chain IDs. See [Get Tokens](/sdk/streams/get-tokens) for the supported list.
</ParamField>

<ParamField body="targetTokens" type="EvmToken[]">
  Filter by streams whose target token matches any of these EVM `(chainId, address)` tokens.
</ParamField>

<ParamField body="withSnapshots" type="boolean" default="false">
  Include the full historical `snapshots` array for each returned stream. Behaves exactly as on [Get Streams](/sdk/streams/get-streams).
</ParamField>

<ParamField body="usersCount" type="boolean" default="false">
  Add `userCount` and `activeUserCount` to each included snapshot object. Behaves exactly as on [Get Streams](/sdk/streams/get-streams).
</ParamField>

#### `EvmToken`

<ParamField body="chainId" type="integer" required>
  Decimal EVM chain ID of the target token.
</ParamField>

<ParamField body="address" type="string" required>
  Target token contract address on that chain. Matching is case-insensitive.
</ParamField>

### Query Parameters

Pagination is optional. Omit both parameters to return **all** matching streams in a single response, without a `pagination` object.

<ParamField query="page" type="integer">
  Page number for the paginated result set. Providing `page` or `limit` switches the endpoint into paginated mode. Values lower than `1` are normalized by the backend to `1`.
</ParamField>

<ParamField query="limit" type="integer">
  Number of streams to return per page. Providing `page` or `limit` switches the endpoint into paginated mode. Values lower than `1` are normalized to `20`. Values above `500` are rejected with `400 Invalid Argument`.
</ParamField>

## Response Example

When pagination is requested, the response includes a `pagination` object. The `streams` array holds the same `Stream` objects documented in [Get Streams](/sdk/streams/get-streams).

```json theme={null}
{
  "streams": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "chainId": 1,
      "contractAddress": "0x4C6F5a1aA2B9985d4A8b9189C2118E7f55E2f701",
      "orgId": "9f51b66a-d13a-4b55-8515-ae6e4ef7cf25",
      "type": 2,
      "strategy": "Fixed APR",
      "startTimestamp": "2026-03-11T00:00:00Z",
      "endTimestamp": "2026-04-11T00:00:00Z",
      "lastSnapshot": { "...": "see Get Streams for the full Stream object" },
      "snapshots": [],
      "rewardToken": { "...": "see Get Streams" }
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 50,
    "total": 128,
    "totalPages": 3,
    "hasNext": true,
    "hasPrevious": false
  }
}
```

<Info>
  The `Stream` object is identical to the one returned by [Get Streams](/sdk/streams/get-streams). See that page for the complete field-by-field schema, including `customArgs`, `adapters`, `point`, `lastSnapshot`, `committedSnapshot`, `snapshots`, and `rewardToken`.
</Info>

## Response Semantics

<AccordionGroup>
  <Accordion title="Filters combine with AND, values within a filter with OR">
    Sending `organizationIds` and `targetChainIds` together returns streams that belong to one of the organizations **and** whose target token is on one of the chains. Within a single filter, multiple values are matched with `OR` (an `IN` lookup).
  </Accordion>

  <Accordion title="Pagination is opt-in">
    If you omit both `page` and `limit`, the endpoint returns every matching stream and no `pagination` object. As soon as either query parameter is present, the response is paginated and includes a `pagination` object.
  </Accordion>

  <Accordion title="A bounded limit is required when no filter is provided">
    To avoid loading the entire stream table, an empty body (no `ids`, `organizationIds`, `targetChainIds`, or `targetTokens`) must be paired with a bounded `limit` query parameter. Without a filter and without a `limit`, the request is rejected with `400 Invalid Argument`.
  </Accordion>

  <Accordion title="Only confirmed streams are returned">
    Like [Get Streams](/sdk/streams/get-streams), this endpoint only returns streams whose creation has been confirmed. Pending streams are never included.
  </Accordion>

  <Accordion title="An empty result is valid">
    If no stream matches the provided filters, the endpoint returns `streams: []` (together with a `pagination` object when pagination was requested) rather than failing.
  </Accordion>
</AccordionGroup>

## Response Fields

```typescript theme={null}
streams: Stream[]
pagination?: PaginationResponse
```

<ResponseField name="streams" type="Stream[]" required>
  List of confirmed streams matching the provided filters. Each item is a `Stream` object. See [Get Streams](/sdk/streams/get-streams) for the full schema.
</ResponseField>

<ResponseField name="pagination" type="PaginationResponse">
  Pagination metadata describing the current page and the total size of the result set. Present only when `page` or `limit` was supplied; omitted otherwise.
</ResponseField>

### `PaginationResponse`

<ResponseField name="page" type="integer">
  Current page returned by the backend after normalization.
</ResponseField>

<ResponseField name="limit" type="integer">
  Number of items returned per page after normalization or capping.
</ResponseField>

<ResponseField name="total" type="integer">
  Total number of streams matching the filters across all pages.
</ResponseField>

<ResponseField name="totalPages" type="integer">
  Total number of available pages for the current `limit`.
</ResponseField>

<ResponseField name="hasNext" type="boolean">
  Whether a page after the current one exists.
</ResponseField>

<ResponseField name="hasPrevious" type="boolean">
  Whether a page before the current one exists.
</ResponseField>

## Error Handling

<AccordionGroup>
  <Accordion title="Missing filter without a bounded limit">
    **Status Code:** 400 Bad Request

    ```json theme={null}
    {
      "error": {
        "status": "INVALID_ARGUMENT",
        "error": "a bounded limit (max 500) is required when no filter is provided"
      }
    }
    ```

    **Solution:** Provide at least one filter (`ids`, `organizationIds`, `targetChainIds`, or `targetTokens`), or supply a bounded `limit` query parameter.
  </Accordion>

  <Accordion title="Too many filter values">
    **Status Code:** 400 Bad Request

    Each filter has an upper bound: 100 `ids`, 20 `organizationIds`, 5 `targetChainIds`, and 100 `targetTokens`. Exceeding any of these returns `INVALID_ARGUMENT` with a message such as `too many stream ids (max 100)`.

    **Solution:** Split large requests into multiple calls that stay within the per-filter limits.
  </Accordion>

  <Accordion title="Limit too high">
    **Status Code:** 400 Bad Request

    ```json theme={null}
    {
      "error": {
        "status": "INVALID_ARGUMENT",
        "error": "limit too high (max 500)"
      }
    }
    ```

    **Solution:** Request a `limit` of 500 or fewer and page through the result set.
  </Accordion>

  <Accordion title="Unexpected internal error">
    **Status Code:** 500 Internal Server Error

    **Solution:** Retry the request and contact Turtle if the issue persists.
  </Accordion>
</AccordionGroup>
