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

# Export X followers to CRM or warehouse

> Export X followers from Xquik, map CRM fields, and hand off CSV, XLSX, or JSON for import or upsert

<blockquote className="agent-llms-directive">
  For the complete documentation index, see <a href="/llms.txt">llms.txt</a>.
</blockquote>

Use this workflow when a sales, support, research, or marketing system needs an owned list of X followers with stable IDs, usernames, display names, verification state, profile images, and enrichment fields.

## Outcome

<CardGroup cols={2}>
  <Card title="1. Estimate" icon="calculator">
    Call `POST /extractions/estimate` with `follower_explorer` to check credits and projected row count.
  </Card>

  <Card title="2. Extract" icon="play">
    Call `POST /extractions` with `targetUsername` and optional `resultsLimit` to start the async job.
  </Card>

  <Card title="3. Retrieve" icon="rows-3">
    Call `GET /extractions/{id}` when a custom pipeline needs paginated JSON rows.
  </Card>

  <Card title="4. Export" icon="download">
    Call `GET /extractions/{id}/export?format=csv`, `format=json`, or `format=xlsx` for CRM files; `md`, `md-document`, `pdf`, and `txt` are also supported.
  </Card>

  <Card title="5. Import" icon="database">
    Map `User ID` to a CRM unique field for imports, upserts, segments, or warehouse loads.
  </Card>
</CardGroup>

## Choose the right path

<CardGroup cols={2}>
  <Card title="Saved follower export" icon="archive">
    Use `POST /extractions/estimate`, then `POST /extractions` with `follower_explorer` when you need a reusable job ID, cost preview, and CSV/JSON/XLSX files.
  </Card>

  <Card title="Paginated saved rows" icon="braces">
    Use `GET /extractions/{id}?limit=1000&after={nextCursor}` when a CRM sync, warehouse load, queue, or agent needs saved JSON pages.
  </Card>

  <Card title="Live follower page" icon="zap">
    Use `GET /x/users/{id}/followers?pageSize=200&cursor={next_cursor}` when you need the current follower page without creating an extraction job.
  </Card>

  <Card title="Shell or SDK handoff" icon="terminal">
    Use [CLI](/sdks/cli), [TypeScript](/sdks/typescript), [Python](/sdks/python), or [Go](/sdks/go) when scheduled imports need code-owned JSON Lines, CSV, or XLSX files.
  </Card>
</CardGroup>

Use extraction jobs for repeatable exports, cost preview, audit trails, and file downloads. Use the direct followers API for low-latency pages and small app handoffs.

## End-to-end follower export handoff

Store one checkpoint that carries the follower target through estimate, job creation, JSON pagination, file export, and CRM upsert:

```json theme={null}
{
  "workflow": "follower_export_crm",
  "request": {
    "toolType": "follower_explorer",
    "targetUsername": "elonmusk",
    "resultsLimit": 10000
  },
  "estimate": {
    "estimatedResults": 10000,
    "creditsRequired": "10000",
    "creditsAvailable": "77000",
    "allowed": true,
    "source": "resultsLimit",
    "resolvedXUserId": "44196397"
  },
  "create_receipt": {
    "id": "77777",
    "toolType": "follower_explorer",
    "status": "running",
    "poll_path": "/api/v1/extractions/77777"
  },
  "json_pages": {
    "limit": 1000,
    "page_cursor": null,
    "next_cursor": "1001",
    "has_more": true
  },
  "export_paths": {
    "csv": "/api/v1/extractions/77777/export?format=csv",
    "json": "/api/v1/extractions/77777/export?format=json",
    "xlsx": "/api/v1/extractions/77777/export?format=xlsx"
  },
  "normalized_row": {
    "x_user_id": "44196397",
    "x_username": "username",
    "display_name": "Xquik",
    "followers_count": 2400,
    "verified": true,
    "profile_image_url": "https://pbs.twimg.com/profile_images/xquik.jpg",
    "bio": "X automation platform",
    "location": "San Francisco",
    "account_created_at": "2021-03-01T12:00:00.000Z",
    "can_dm": true
  },
  "crm_import": {
    "unique_field": "x_user_id",
    "upsert_mode": "external_id",
    "file_format": "csv",
    "file_path": "x-followers-elonmusk.csv"
  },
  "handoff_state": "poll_until_completed_then_export"
}
```

<CardGroup cols={2}>
  <Card title="Estimate checkpoint" icon="calculator">
    Keep `estimatedResults`, `creditsRequired`, `creditsAvailable`, `allowed`,
    `source`, and `resolvedXUserId`; `source` is `followers` unless
    `resultsLimit` is lower than the follower count.
  </Card>

  <Card title="Job checkpoint" icon="clipboard-check">
    Store the returned job `id`, `status`, and `poll_path`; do not expect follower rows in the create response.
  </Card>

  <Card title="Cursor checkpoint" icon="shuffle">
    Store `page_cursor`, `next_cursor`, and `has_more` for saved JSON page loops, then pass `nextCursor` back as `after`.
  </Card>

  <Card title="CRM checkpoint" icon="database">
    Store the CSV, JSON, or XLSX `export_paths`, `unique_field`, and normalized follower row fields used for the upsert.
  </Card>
</CardGroup>

### Live follower page handoff

Use `GET /x/users/{id}/followers` when a CRM enrichment job, queue worker, audience sync, or agent needs the newest follower page without creating a saved extraction.

```bash cURL theme={null}
curl "https://xquik.com/api/v1/x/users/44196397/followers?pageSize=200&cursor=DAACCgACGRElMJcAAA" \
  -H "x-api-key: xq_YOUR_KEY_HERE" | jq
```

The response returns `users`, `has_next_page`, and `next_cursor`. Treat `next_cursor` as opaque, store it with the import checkpoint, and pass it back as `cursor` only when `has_next_page` is true.

Map direct API fields before import:

<CardGroup cols={2}>
  <Card title="Identity" icon="fingerprint">
    Map `users[].id` to `x_user_id`, `users[].username` to `x_username`, and `users[].name` to `display_name`.
  </Card>

  <Card title="Audience fields" icon="chart-no-axes-column">
    Map optional `followers`, `following`, `statusesCount`, `verified`, and `verifiedType` for scoring or segments.
  </Card>

  <Card title="Profile context" icon="file-text">
    Map optional `description`, `location`, `url`, `profilePicture`, `coverPicture`, `createdAt`, and `canDm` for enrichment and review queues.
  </Card>

  <Card title="Credit-aware pages" icon="coins">
    `pageSize` accepts `20` to `200`, but paid calls can return fewer users when remaining credits are lower than the requested page. If no result can be afforded, the endpoint returns `402 insufficient_credits`; lower `pageSize` or add credits before retrying.
  </Card>
</CardGroup>

For scheduled direct pulls, store one page checkpoint per import attempt. Never treat `cursor` as a stable follower ID; it only resumes the next page.

```json theme={null}
{
  "job": "direct_follower_page",
  "source_x_user_id": "44196397",
  "page_size_requested": 200,
  "rows_returned": 200,
  "cursor_used": "DAACCgACGRElMJcAAA",
  "next_cursor": "DAACCgACGE...",
  "has_next_page": true,
  "fetched_at": "2026-05-16T16:50:00.000Z",
  "page_checkpoint": "44196397:DAACCgACGE..."
}
```

For CRM or warehouse rows, map `users[].createdAt` to `account_created_at` and `users[].canDm` to `can_dm` when those fields are returned.

## Step 1: Estimate cost

Estimate first so the workflow can stop before spending credits.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://xquik.com/api/v1/extractions/estimate \
    -H "x-api-key: xq_YOUR_KEY_HERE" \
    -H "Content-Type: application/json" \
    -d '{
      "toolType": "follower_explorer",
      "targetUsername": "elonmusk",
      "resultsLimit": 10000
    }' | jq
  ```

  ```javascript Node.js theme={null}
  const estimate = await fetch("https://xquik.com/api/v1/extractions/estimate", {
    method: "POST",
    headers: {
      "x-api-key": "xq_YOUR_KEY_HERE",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      toolType: "follower_explorer",
      targetUsername: "elonmusk",
      resultsLimit: 10000,
    }),
  }).then((response) => response.json());

  if (!estimate.allowed) {
    throw new Error(
      `Need ${estimate.creditsRequired} credits, have ${estimate.creditsAvailable}`,
    );
  }
  ```

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

  estimate = requests.post(
      "https://xquik.com/api/v1/extractions/estimate",
      headers={"x-api-key": "xq_YOUR_KEY_HERE"},
      json={
          "toolType": "follower_explorer",
          "targetUsername": "elonmusk",
          "resultsLimit": 10000,
      },
  ).json()

  if not estimate["allowed"]:
      raise RuntimeError(
          f"Need {estimate['creditsRequired']} credits, have {estimate['creditsAvailable']}"
      )
  ```
</CodeGroup>

```json theme={null}
{
  "allowed": true,
  "creditsRequired": "10000",
  "creditsAvailable": "77000",
  "estimatedResults": 10000,
  "source": "resultsLimit",
  "resolvedXUserId": "44196397"
}
```

`resultsLimit` caps both the estimate and the extraction. Use it for first
imports, test segments, and nightly syncs. When the account has fewer followers
than `resultsLimit`, the estimate keeps `source: "followers"` and uses the
smaller follower count.

## Step 2: Start the extraction

Run the same request against `POST /extractions`.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://xquik.com/api/v1/extractions \
    -H "x-api-key: xq_YOUR_KEY_HERE" \
    -H "Content-Type: application/json" \
    -d '{
      "toolType": "follower_explorer",
      "targetUsername": "elonmusk",
      "resultsLimit": 10000
    }' | jq
  ```

  ```javascript Node.js theme={null}
  const job = await fetch("https://xquik.com/api/v1/extractions", {
    method: "POST",
    headers: {
      "x-api-key": "xq_YOUR_KEY_HERE",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      toolType: "follower_explorer",
      targetUsername: "elonmusk",
      resultsLimit: 10000,
    }),
  }).then((response) => response.json());

  const extractionHandoff = {
    extraction_id: job.id,
    status: job.status,
    source_username: "elonmusk",
    results_limit: 10000,
    handoff_created_at: new Date().toISOString(),
  };
  ```
</CodeGroup>

Store `extractionHandoff` in your job table. Poll by `extraction_id`, keep `source_username` and `results_limit` for audit, and replace `status` with the value returned by `GET /extractions/{id}`.

```json theme={null}
{
  "id": "77777",
  "toolType": "follower_explorer",
  "status": "running"
}
```

## Step 3: Poll until complete

Poll the job before exporting. Use the paginated endpoint when your pipeline wants JSON rows instead of a file.

```bash cURL theme={null}
curl -s "https://xquik.com/api/v1/extractions/77777?limit=1000" \
  -H "x-api-key: xq_YOUR_KEY_HERE" | jq
```

The job is ready for file export when `status` is `completed`.

### Paginated JSON handoff

Use `GET /extractions/{id}` when a custom CRM sync, warehouse load, queue, or agent needs rows before a file export. The API returns camelCase JSON fields. Only `id` and `xUserId` are guaranteed on every result; optional profile and enrichment fields are omitted when unavailable.

```json theme={null}
{
  "job": {
    "id": "77777",
    "toolType": "follower_explorer",
    "status": "completed",
    "totalResults": 10000,
    "targetUsername": "elonmusk",
    "createdAt": "2026-05-08T13:46:00.000Z",
    "completedAt": "2026-05-08T13:51:00.000Z"
  },
  "results": [
    {
      "id": "1001",
      "xUserId": "44196397",
      "xUsername": "username",
      "xDisplayName": "Xquik",
      "xFollowersCount": 2400,
      "xVerified": true,
      "xProfileImageUrl": "https://pbs.twimg.com/profile_images/xquik.jpg",
      "enrichmentData": {
        "user": {
          "followingCount": 120,
          "statusesCount": 830,
          "description": "X automation platform",
          "location": "San Francisco",
          "coverPicture": "https://pbs.twimg.com/profile_banners/xquik"
        },
        "tweet": {}
      },
      "createdAt": "2026-05-08T13:51:00.000Z"
    }
  ],
  "hasMore": true,
  "nextCursor": "1001"
}
```

Use `limit` up to `1000` and pass `nextCursor` as `after` until `hasMore` is `false`. For CRM or warehouse rows, map `xUserId` to `x_user_id`, `xUsername` to `x_username`, `xDisplayName` to `display_name`, `xFollowersCount` to `followers_count`, `xVerified` to `verified`, and `enrichmentData.user.description` or `enrichmentData.user.location` to optional enrichment columns.

Normalize each follower row before sending it downstream:

```json theme={null}
{
  "job": "follower_export",
  "source_username": "elonmusk",
  "result_id": "1001",
  "x_user_id": "44196397",
  "x_username": "username",
  "display_name": "Xquik",
  "followers_count": 2400,
  "verified": true,
  "profile_image_url": "https://pbs.twimg.com/profile_images/xquik.jpg",
  "bio": "X automation platform",
  "location": "San Francisco",
  "handoff_format": "json"
}
```

## Step 4: Export the CRM file

Use CSV for most CRM imports, XLSX for analyst review, or JSON for a data pipeline.

<CodeGroup>
  ```bash cURL theme={null}
  curl -s "https://xquik.com/api/v1/extractions/77777/export?format=csv" \
    -H "x-api-key: xq_YOUR_KEY_HERE" \
    -o x-followers-elonmusk.csv
  ```

  ```javascript Node.js theme={null}
  import { writeFile } from "node:fs/promises";

  const extractionId = "77777";
  const exportFilePath = "x-followers-elonmusk.json";
  const response = await fetch(
    `https://xquik.com/api/v1/extractions/${extractionId}/export?format=json`,
    { headers: { "x-api-key": "xq_YOUR_KEY_HERE" } },
  );

  if (!response.ok) {
    throw new Error(`Export failed with ${response.status}`);
  }

  const bytes = Buffer.from(await response.arrayBuffer());
  await writeFile(exportFilePath, bytes);

  const exportHandoff = {
    extraction_id: extractionId,
    source_username: "elonmusk",
    export_format: "json",
    export_file_path: exportFilePath,
    content_disposition: response.headers.get("Content-Disposition") ?? "",
    handoff_created_at: new Date().toISOString(),
  };
  ```

  ```python Python theme={null}
  extraction_id = "77777"
  export_file_path = "x-followers-elonmusk.csv"
  response = requests.get(
      f"https://xquik.com/api/v1/extractions/{extraction_id}/export",
      headers={"x-api-key": "xq_YOUR_KEY_HERE"},
      params={"format": "csv"},
  )
  response.raise_for_status()

  with open(export_file_path, "wb") as file:
      file.write(response.content)
  ```
</CodeGroup>

Use the local `-o` path, `exportFilePath`, or `export_file_path` as the durable
handoff value. Read `Content-Disposition` only when you need the server filename
for audit.

```json theme={null}
{
  "job": "follower_export_file",
  "extraction_id": "77777",
  "source_username": "elonmusk",
  "export_format": "csv",
  "export_file_path": "x-followers-elonmusk.csv",
  "handoff_format": "file"
}
```

Exports are free and capped at 100,000 rows per extraction, with a 10,000-row cap for PDF. Use paginated JSON for larger custom syncs.

## CRM field mapping

Map stable X identifiers first. Names, bios, and locations can change.

<CardGroup cols={2}>
  <Card title="Stable account key" icon="fingerprint">
    Map export column `User ID` to `x_user_id` as the custom unique field. Use it for CRM upserts because it is the stable X account identifier.
  </Card>

  <Card title="Handle and display name" icon="at-sign">
    Map `Username` to `x_username` or a social profile field, and map `Display Name` to `display_name` for triage and human review.
  </Card>

  <Card title="Audience metrics" icon="chart-no-axes-column">
    Map `Followers` to `followers_count`, `Following` to `following_count`, and `Posts` to `posts_count` for segmentation, scoring, and reporting.
  </Card>

  <Card title="Verification flag" icon="badge-check">
    Map `Verified` to `verified` so lead scoring, audience filters, and reviewer queues can separate verified accounts.
  </Card>

  <Card title="Profile context" icon="file-text">
    Map `Description` to a bio or notes field, and map `Location` to a regional field. Treat both as mutable enrichment, not identity.
  </Card>

  <Card title="Profile media" icon="image">
    Map `Profile Image` to an avatar URL field and `Cover Picture` to a banner URL field for internal views that show account context.
  </Card>
</CardGroup>

## Import and upsert rules

Use the CRM's own unique field rules. HubSpot's official import docs say update or dedupe imports need a unique identifier, and supported import files include CSV, XLSX, and XLS. Salesforce Bulk API 2.0 upserts use CSV data and require an external ID field for upsert jobs.

<CardGroup cols={2}>
  <Card title="HubSpot import docs" icon="external-link" href="https://knowledge.hubspot.com/import-and-export/set-up-your-import-file">
    Check file requirements, unique identifier rules, and supported spreadsheet formats before importing.
  </Card>

  <Card title="Salesforce Bulk API 2.0" icon="external-link" href="https://developer.salesforce.com/docs/marketing/marketing-cloud-growth/guide/mc-manage-objects-update-bulk.html">
    Use CSV upserts with an external ID field when loading large contact or lead datasets.
  </Card>
</CardGroup>

## Automation handoff

Use this handoff when the export runs on a schedule:

1. Store the Xquik extraction ID and source username in your job table.
2. Poll `GET /extractions/{id}` until `status` is `completed`.
3. Download `format=json` for code pipelines or `format=csv` for CRM import tools.
4. Upsert by `x_user_id`, not by display name or username.
5. Save the Xquik job ID, export format, export file path, row count, and import result for audit.
6. For JSON Lines, write one normalized follower object per line to `xquik-follower-export.jsonl`.

## Related

<CardGroup cols={2}>
  <Card title="Extraction workflow" icon="download" href="/guides/extraction-workflow">
    Estimate costs, run jobs, paginate results, and export files.
  </Card>

  <Card title="Followers API" icon="users" href="/api-reference/x/followers">
    Fetch one live follower page with `users`, `has_next_page`, and `next_cursor`.
  </Card>

  <Card title="n8n guide" icon="workflow" href="/guides/n8n">
    Send extraction rows into Sheets, Slack, and downstream automation.
  </Card>
</CardGroup>
