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

# Scroll Search: Paginate Large Result Sets

> Paginate through search results using scrollId.

## Overview

After an initial [Search by Query](/search-api/search-by-query) returns a `scrollId`, use this endpoint to fetch subsequent batches of results. The mechanism works similarly to Elasticsearch scroll — the server keeps the search context alive while batches are being retrieved one after another.

**The intended usage pattern is sequential and continuous**: fetch all needed results in one session, batch by batch, then process them. The scroll session is not designed for periodic polling — for example, fetching 100 profiles every hour is not a supported use case.

<Warning>
  The `scrollId` expires after **15 seconds**. Send your next request within this window or the scroll session will be lost.
</Warning>

## Understanding the 15-Second Timeout

The `scrollId` expires **15 seconds** after it is issued. This is intentional — it represents the maximum time allowed to request the *next* batch after receiving the current one.

The server keeps the search context alive for the entire duration of an active scroll session. The 15-second window applies only between consecutive requests, not to the total session length. As long as each subsequent request arrives within 15 seconds of the previous response, the session remains active regardless of total session duration.

A common misconception is treating the scroll like a bookmark — fetching a batch, processing it for several minutes, then resuming. This will not work. The correct approach:

1. Start with `searchByQuery` — receive the first batch and `scrollId`
2. Immediately send the next `scrollSearch` request using the new `scrollId`
3. Repeat until no `scrollId` is returned in the response (no more results)
4. Process all collected results after the session is complete

***

**Endpoint:** `POST https://www.signalhire.com/api/v1/candidate/scrollSearch/{requestId}`

## Request Parameters

<ParamField path="requestId" type="integer" required>
  The `requestId` from the initial `searchByQuery` response. Included in the URL path.
</ParamField>

<ParamField body="scrollId" type="string" required>
  The `scrollId` from the previous `searchByQuery` or `scrollSearch` response.
</ParamField>

## Request Example

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://www.signalhire.com/api/v1/candidate/scrollSearch/3 \
  -H 'apikey: your_secret_api_key' \
  --data '{"scrollId": "abc123"}'
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
  "https://www.signalhire.com/api/v1/candidate/scrollSearch/3",
  headers={"apikey": "your_secret_api_key"},
  json={"scrollId": "abc123"}
  )
  ```

  ```javascript Node.js theme={null}
  const axios = require('axios');

  const response = await axios.post(
  'https://www.signalhire.com/api/v1/candidate/scrollSearch/3',
  { scrollId: 'abc123' },
  { headers: { apikey: 'your_secret_api_key' } }
  );
  ```

  ```java Java theme={null}
  import java.net.http.*;
  import java.net.URI;

  HttpClient client = HttpClient.newHttpClient();

  HttpRequest request = HttpRequest.newBuilder()
  .uri(URI.create("https://www.signalhire.com/api/v1/candidate/scrollSearch/3"))
  .header("apikey", "your_secret_api_key")
  .header("Content-Type", "application/json")
  .POST(HttpRequest.BodyPublishers.ofString("{\"scrollId\": \"abc123\"}"))
  .build();

  HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
  ```

  ```ruby Ruby theme={null}
          require 'net/http'
          require 'json'

          uri = URI('https://www.signalhire.com/api/v1/candidate/scrollSearch/3')
          http = Net::HTTP.new(uri.host, uri.port)
          http.use_ssl = true

          request = Net::HTTP::Post.new(uri)
          request['apikey'] = 'your_secret_api_key'
          request['Content-Type'] = 'application/json'
          request.body = { scrollId: 'abc123' }.to_json

          response = http.request(request)
  ```
</CodeGroup>

## Response Example (HTTP 200)

The response format is identical to the initial [Search by Query](/search-api/search-by-query#response-example-http-200) response — same fields, same structure. The `total` field always reflects the overall count across all batches, not just the current one.

```json theme={null}
{
  "requestId": 3,
  "total": 12,
  "scrollId": "xyz456",
  "profiles": [
    {
      "uid": "10000000000000000000000000001001",
      "fullName": "John Smith",
      "location": "Brisbane Area, Australia",
      "experience": [
        { "company": "Super company", "title": "PHP Developer" }
      ],
      "skills": ["PHP", "HTML", "Management"],
      "openToWork": true
    }
  ]
}
```

Each response includes a new `scrollId` to use in the next request. When `scrollId` is absent from the response, all results have been retrieved and the scroll session is complete. See [Search Profile Object](/search-api/search-profile-object) for a full field reference.

<Info>
  Scroll requests do not count as new searches against the daily query quota. However, every profile returned by a scroll does count against the daily profile quota — the same limit that applies to `searchByQuery` results.
</Info>

## Response Status Codes

| Code  | Description                     |
| ----- | ------------------------------- |
| `200` | Results returned successfully   |
| `402` | Daily search quota exceeded     |
| `404` | Invalid or expired `scrollId`   |
| `429` | More than 3 concurrent requests |

## Full Example

For a complete end-to-end example showing `searchByQuery` and `scrollSearch` working together with database saving, see [Full Search Example](/search-api/full-search-example).
