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

# Refine a dataset by what each person wrote

> Judges each row's own text against a natural-language `criterion` and
keeps or drops it, then optionally sorts and caps what is left into a
NEW dataset. The source dataset is never modified.

Only datasets whose rows carry text can be refined this way (a
repliers or quoters collection). Anything else answers `400`: use the
profile filters on `POST /v1/datasets` instead.

Rows the classifier cannot judge confidently are **kept** and counted
as `unclear`, never silently dropped. Classification is best-effort:
read the kept rows before acting on them.

**This endpoint can answer before the work is finished.** Up to 100
rows with text classify inside the request and answer `200`; more than
that answers `202` with a `collecting` dataset to poll through
`GET /v1/datasets/{id}`. If nothing was kept, the answer is `200` with
`data: null` and a `note`.

**Cost.** A refinement creates a dataset, so it counts against the
same 10 collections a day this account shares with Ask SuperX
(`429 collection_quota_exceeded`), and it costs AI credits, measured
from the classifier's real cost. It spends no live-data allowance.
Needs a key with the `write` scope.




## OpenAPI

````yaml /api-reference/openapi.yaml post /v1/datasets/{id}/refine
openapi: 3.1.0
info:
  title: SuperX API
  version: 1.0.0
  description: >
    The SuperX public API: your Twitter/X content, analytics, audience and

    scheduling data over REST.


    All endpoints require an API key (`Authorization: Bearer sxk_...`) except

    `GET /v1/docs`. Keys are created in the SuperX app under Account > API / MCP
    / CLI

    and are server-side secrets.


    Timestamps are UTC ISO-8601 in both directions; inputs must carry an
    explicit

    `Z` or numeric offset. Every authenticated response carries

    `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`
    headers.
  contact:
    name: SuperX
    url: https://superx.so
servers:
  - url: https://api.superx.so
security:
  - apiKey: []
tags:
  - name: Identity
  - name: Content
  - name: Analytics
  - name: Inspiration
  - name: Audience
  - name: Contact Lists
  - name: Signals
  - name: Datasets
    description: |
      The audience collections Ask SuperX builds in the app: the repliers,
      quoters or reposters of a post, the members of an X list, your own
      posts or replies, or a research brief.

      Collect a new one with `POST /v1/datasets` (a big collection answers
      202 and finishes in the background), read it, export it, or copy the
      people in it into a contact list you created. Datasets are kept for 30
      days, and the ones Ask SuperX builds in the app show up here too.
  - name: Engage
    description: |
      The account's saved Engage feeds and the candidate posts in them, for
      an agent to score and a person to act on.

      Read-only by design. These endpoints return feed candidates for a
      person to review; replies are written and sent by a person in the
      SuperX app, which is why there is no reply endpoint here. Sending
      spammy, automated, or AI-generated replies that read as inauthentic
      may get your X account suspended under X's inauthentic-behavior
      rules and your SuperX account terminated. AI suggestions must be
      reviewed and meaningfully edited before they are sent, and you are
      solely responsible for what you post. Reply activity is logged and
      may be audited.
  - name: X Lookups
    description: |
      Read any public post, its top replies, any public profile, or an
      account's latest posts LIVE from X, rather than from SuperX's stored
      data.

      These are owner-scoped, so none of them take an `account_id`: nothing
      about a public lookup is per-X-account. They cost the tighter
      live-enrichment allowance and share a daily allowance of 300 live
      lookups with Ask SuperX inside the app, so use them for what you
      actually need rather than to sweep. Repeat lookups within 15 minutes
      may be served from a short-lived server-side cache.
  - name: Tools
    description: |
      The composer helpers from the SuperX app, over the API: rewrite a post,
      edit one selected piece of it, apply one preset rewrite, check a claim,
      or compare two versions.

      Every one of them returns TEXT and writes nothing. Nothing here is
      posted, scheduled or sent; save what you keep with
      `POST /v1/posts/draft` or `POST /v1/scheduled-posts`. They cost AI
      credits, measured from the models' real cost, and spend no live-data
      allowance.
  - name: Media
  - name: Workers
    description: |
      The posts the account's Workers have written, and the three things a
      person does with one: save it as a draft, schedule it, or dismiss it.

      A Worker is a scheduled AI writer set up in the SuperX app. Creating,
      editing and running one stays in the app, so there is no create or run
      endpoint here. What a run produces lands in the app's "To review" queue
      and is read with `GET /v1/workers/suggestions`.

      NOTHING HERE POSTS TO X. Drafting or scheduling a suggestion writes a
      post the user can see in the app and on `GET /v1/scheduled-posts`,
      exactly like the app's own buttons. AI output must be reviewed and
      meaningfully edited by a person before it goes out, so show the text to
      the user and confirm the time before you schedule anything.
  - name: Scheduling
  - name: Tags
  - name: Context
  - name: Queue
  - name: DMs
    description: |
      Queue direct messages into the account's own DM pipeline, and read or
      cancel what is waiting.

      NOTHING HERE SENDS A MESSAGE. `POST /v1/dm/campaigns` enqueues; the
      SuperX app's scheduler is what sends, within the account's daily and
      monthly DM limits, so the create response is counts, not deliveries.
      Unsent messages can be cancelled until the scheduler picks them up.

      You are responsible for the messages you queue. Unsolicited or
      automated bulk DMs may get an X account suspended under X's rules and
      a SuperX account terminated, so confirm the recipient list and the
      exact wording with the person on whose behalf you are queueing.
  - name: Articles
  - name: Meta
paths:
  /v1/datasets/{id}/refine:
    post:
      tags:
        - Datasets
      summary: Refine a dataset by what each person wrote
      description: |
        Judges each row's own text against a natural-language `criterion` and
        keeps or drops it, then optionally sorts and caps what is left into a
        NEW dataset. The source dataset is never modified.

        Only datasets whose rows carry text can be refined this way (a
        repliers or quoters collection). Anything else answers `400`: use the
        profile filters on `POST /v1/datasets` instead.

        Rows the classifier cannot judge confidently are **kept** and counted
        as `unclear`, never silently dropped. Classification is best-effort:
        read the kept rows before acting on them.

        **This endpoint can answer before the work is finished.** Up to 100
        rows with text classify inside the request and answer `200`; more than
        that answers `202` with a `collecting` dataset to poll through
        `GET /v1/datasets/{id}`. If nothing was kept, the answer is `200` with
        `data: null` and a `note`.

        **Cost.** A refinement creates a dataset, so it counts against the
        same 10 collections a day this account shares with Ask SuperX
        (`429 collection_quota_exceeded`), and it costs AI credits, measured
        from the classifier's real cost. It spends no live-data allowance.
        Needs a key with the `write` scope.
      operationId: refineDataset
      parameters:
        - $ref: '#/components/parameters/DatasetId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RefineDatasetRequest'
            example:
              criterion: supportive or neutral; not mean, sarcastic, or hostile
              keep_matching: true
              sort_by: followers
              limit: 200
      responses:
        '200':
          description: |
            The refinement finished inside the request. `data` is null when
            nothing was kept, with a `note` explaining why.
          headers:
            X-Credits-Charged:
              $ref: '#/components/headers/X-Credits-Charged'
            X-Credits-Remaining:
              $ref: '#/components/headers/X-Credits-Remaining'
            X-Credits-Reset:
              $ref: '#/components/headers/X-Credits-Reset'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    allOf:
                      - $ref: '#/components/schemas/DatasetDetail'
                    nullable: true
                  meta:
                    $ref: '#/components/schemas/RefineMeta'
                  note:
                    type: string
              example:
                data:
                  id: Jc9PxVU1FSkx4kzPuehGK
                  title: 'Refined: Repliers to my launch post'
                  source: repliers
                  status: ready
                  target_ref: '1234567890123456789'
                  x_account_id: '1178367350552305665'
                  ask_chat_id: null
                  columns:
                    - handle
                    - name
                    - bio
                    - text
                    - followers
                    - can_dm
                  row_count: 48
                  scanned_count: 64
                  total_estimate: 64
                  coverage_complete: true
                  has_people: true
                  created_at: '2026-09-10T10:20:00.000Z'
                  expires_at: '2026-10-10T10:20:00.000Z'
                meta:
                  source_dataset_id: VKcPAVU1FSkx4kzPuehGK
                  kept: 48
                  dropped: 14
                  unclear: 2
                  limited: 0
                  self_excluded: 0
                  credits_charged: 2
        '202':
          description: |
            The refinement started in the background. Poll
            `GET /v1/datasets/{id}` until `status` is `ready`. Credits are
            settled when it finishes, so no credit headers are sent here.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/DatasetDetail'
                  meta:
                    $ref: '#/components/schemas/RefineMeta'
                  note:
                    type: string
        '400':
          $ref: '#/components/responses/InvalidParameter'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/WriteForbidden'
        '404':
          $ref: '#/components/responses/DatasetNotFound'
        '409':
          description: |
            The dataset is not ready yet (`dataset_not_ready`), or another
            background collection or refinement is still running for this
            account (`collection_in_progress`). Neither started anything, and
            `collection_in_progress` does not use up one of the day's 10.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              examples:
                dataset_not_ready:
                  value:
                    error:
                      code: dataset_not_ready
                      message: >-
                        This dataset is not ready yet. Poll GET
                        /v1/datasets/{id} until status is ready.
                      dataset_status: collecting
                collection_in_progress:
                  value:
                    error:
                      code: collection_in_progress
                      message: >-
                        Another collection or refinement is still running for
                        this account. Poll it with GET /v1/datasets/{id} and
                        start this one when it finishes.
        '429':
          description: |
            Today's 10 dataset collections are used up
            (`collection_quota_exceeded`, shared with Ask SuperX in the app),
            the AI credit pool is spent (`ai_credits_exhausted`), or a plan
            cap on refinements was hit (`ai_action_limited`). Read
            `error.code`; all carry `Retry-After`.
          headers:
            Retry-After:
              description: Seconds to wait before retrying.
              schema:
                type: integer
          content:
            application/json:
              schema:
                anyOf:
                  - $ref: '#/components/schemas/CreditExhaustedError'
                  - $ref: '#/components/schemas/ActionLimitedError'
                  - $ref: '#/components/schemas/ErrorEnvelope'
              example:
                error:
                  code: collection_quota_exceeded
                  message: >-
                    You have used today's dataset collections. Existing datasets
                    still read and export; the limit resets at midnight UTC.
                  retry_after: 20400
                  limit: 10
                  reset_at: 1789430400
        '500':
          $ref: '#/components/responses/InternalError'
        '502':
          $ref: '#/components/responses/UpstreamError'
        '503':
          $ref: '#/components/responses/AccountsUnavailable'
components:
  parameters:
    DatasetId:
      name: id
      in: path
      required: true
      description: The dataset id from `GET /v1/datasets`.
      schema:
        type: string
        pattern: ^[A-Za-z0-9_-]{1,64}$
  schemas:
    RefineDatasetRequest:
      type: object
      required:
        - criterion
      properties:
        criterion:
          type: string
          minLength: 3
          maxLength: 500
          description: |
            What the rows to match look like, judged on each row's own text,
            e.g. "supportive or neutral; not mean, sarcastic, or hostile".
        keep_matching:
          type: boolean
          default: true
          description: >-
            true keeps the rows that MATCH the criterion; false keeps the rows
            that do NOT.
        sort_by:
          type: string
          enum:
            - followers
            - likes
            - none
          default: none
          description: Sort the kept rows descending before the limit is applied.
        limit:
          type: integer
          minimum: 1
          maximum: 1000
          description: Keep at most this many rows after filtering and sorting.
        title:
          type: string
          maxLength: 120
          description: Title for the new dataset. A sensible one is generated when omitted.
        account_id:
          type: string
          description: Which of your accounts to refine as. Omit for the main account.
    DatasetDetail:
      allOf:
        - $ref: '#/components/schemas/Dataset'
        - type: object
          properties:
            coverage:
              type: string
              description: An honest one-line coverage statement. Ready datasets only.
            filters:
              type: object
              additionalProperties: true
              description: The filters the collection applied. Ready datasets only.
            progress:
              $ref: '#/components/schemas/DatasetProgress'
            failure_note:
              type: string
              description: Why the collection stopped. Failed datasets only.
    RefineMeta:
      type: object
      description: >-
        Counts for the refinement. On a 202 only the source id and row count are
        known.
      properties:
        source_dataset_id:
          type: string
        rows_with_text:
          type: integer
          description: Rows that will be judged (202 only).
        kept:
          type: integer
        dropped:
          type: integer
        unclear:
          type: integer
          description: Rows the classifier could not judge. They were KEPT.
        limited:
          type: integer
          description: Kept rows cut by `limit`.
        self_excluded:
          type: integer
          description: Rows of your own account, always removed.
        credits_charged:
          type: integer
    ErrorEnvelope:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: Stable machine-readable error code.
            message:
              type: string
            retry_after:
              type: integer
              description: Seconds to wait before retrying (rate-limit errors only).
    CreditExhaustedError:
      type: object
      required:
        - error
      description: |
        The `429 ai_credits_exhausted` body: the standard envelope plus the
        credit counters. Sent with a `Retry-After` header and the
        `X-Credits-Remaining` / `X-Credits-Reset` headers. `code` is stable;
        the counters are additive fields, safe to ignore.
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              enum:
                - ai_credits_exhausted
            message:
              type: string
            credits_required:
              type: integer
              description: Credits the call needed.
            credits_remaining:
              type: integer
              nullable: true
              description: Credits available when it was refused.
            reset_at:
              type: integer
              nullable: true
              description: Unix timestamp (seconds) when the pool refills.
    ActionLimitedError:
      type: object
      required:
        - error
      description: |
        The `429 ai_action_limited` body: a per-day cap on how many times an
        expensive action can run, counted separately from the credit pool and
        from the request rate limits. Sent with a `Retry-After` header. Caps
        reset at midnight UTC.

        Read `scope` before telling a user anything: `account` means the
        plan's own daily cap for that action, `platform` means the
        platform-wide fair-use ceiling on live-data actions, which is shared
        with every SuperX account. A `platform` refusal is not about this
        account's quota, so wait and retry rather than reporting the account
        as out.
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              enum:
                - ai_action_limited
            message:
              type: string
            scope:
              type: string
              enum:
                - account
                - platform
              description: |
                Whose ceiling refused the call: this account's own per-day cap
                for the action, or the platform-wide fair-use ceiling on
                live-data actions.
            retry_after:
              type: integer
              description: Seconds to wait before the cap resets.
            reset_at:
              type: integer
              description: Unix seconds when the cap resets (the next UTC midnight).
    Dataset:
      type: object
      properties:
        id:
          type: string
          description: Pass this to the other dataset endpoints.
        title:
          type: string
        source:
          type: string
          enum:
            - repliers
            - quoters
            - reposters
            - list_members
            - my_replies
            - my_posts
            - research
          description: >-
            What the collection walked. `my_posts` / `my_replies` hold posts,
            everything else holds people.
        status:
          type: string
          enum:
            - ready
            - collecting
            - failed
        target_ref:
          type: string
          description: >-
            What the collection was pointed at - a post id, an X list id, or
            free text for a research brief.
        x_account_id:
          type: string
          description: >-
            The X account the collection ran as. Not a filter - datasets belong
            to the key owner, not to one account.
        ask_chat_id:
          type: string
          nullable: true
          description: The Ask SuperX chat the dataset was built in, when known.
        columns:
          type: array
          items:
            type: string
          description: The row keys this dataset carries, in export order.
        row_count:
          type: integer
        scanned_count:
          type: integer
          description: >-
            What the walk went through (people datasets), or the rows kept
            (own-content datasets).
        total_estimate:
          type: integer
          nullable: true
        coverage_complete:
          type: boolean
          description: False when the collection hit its row cap before the end.
        has_people:
          type: boolean
          description: >-
            False for own-content datasets - there is nobody in them to add to a
            contact list.
        created_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
          description: Datasets are kept for 30 days.
    DatasetProgress:
      type: object
      description: Live progress of a collection that is still running.
      properties:
        page:
          type: integer
        scanned:
          type: integer
        matched:
          type: integer
        total_estimate:
          type: integer
          nullable: true
        started_at:
          type: string
          format: date-time
    LegacyErrorEnvelope:
      type: object
      required:
        - error
      description: |
        Legacy shape used ONLY by the shared subscription middleware: a plain
        string `error` field. Seen on `403` when the subscription has lapsed
        (string starts with "subscription_required:") and on `500` when
        subscription verification fails (string starts with "internal_error:").
      properties:
        error:
          type: string
          example: >-
            subscription_required: The SuperX API requires an active
            subscription
  headers:
    X-Credits-Charged:
      description: >-
        AI credits kept for this call. Sent on every endpoint that settles its
        charge before answering, which is all of them except the ones that
        answer 202 and finish in the background.
      schema:
        type: integer
    X-Credits-Remaining:
      description: AI credits left in the account's pool, bonus credits included.
      schema:
        type: integer
    X-Credits-Reset:
      description: Unix timestamp (seconds) when the AI credit pool refills.
      schema:
        type: integer
  responses:
    InvalidParameter:
      description: A parameter is missing, malformed, or out of range.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: invalid_parameter
              message: since must be a UTC ISO-8601 timestamp
    Unauthorized:
      description: >-
        Missing/malformed Authorization header (`unauthorized`) or an
        unknown/revoked key (`invalid_api_key`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: invalid_api_key
              message: Unknown or revoked API key
    WriteForbidden:
      description: >-
        Read-only key (`insufficient_scope`), an account shared with you
        (`writes_main_account_only`; shared accounts are read-only through the
        API, your own linked accounts are not), or a lapsed subscription (legacy
        string envelope).
      content:
        application/json:
          schema:
            oneOf:
              - $ref: '#/components/schemas/ErrorEnvelope'
              - $ref: '#/components/schemas/LegacyErrorEnvelope'
          example:
            error:
              code: insufficient_scope
              message: >-
                This API key is read-only. Create a key with the write scope to
                use this endpoint.
    DatasetNotFound:
      description: No dataset with that id belongs to this key, or it has expired.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: dataset_not_found
              message: >-
                No dataset with that id belongs to this key, or it has expired
                (datasets are kept for 30 days)
    InternalError:
      description: >-
        Unexpected server error. May also use the legacy string envelope when
        subscription verification fails.
      content:
        application/json:
          schema:
            oneOf:
              - $ref: '#/components/schemas/ErrorEnvelope'
              - $ref: '#/components/schemas/LegacyErrorEnvelope'
          example:
            error:
              code: internal_error
              message: Failed to fetch posts
    UpstreamError:
      description: A dependent SuperX service returned an unexpected response.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: upstream_error
              message: Failed to fetch scheduled posts. Try again shortly.
    AccountsUnavailable:
      description: >-
        Linked-account verification is temporarily unavailable (fail closed).
        The main account keeps working.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: accounts_unavailable
              message: >-
                Account information is temporarily unavailable. Try again
                shortly.
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: >-
        A SuperX API key ("sxk_..."), created in the SuperX app under Account >
        API / MCP / CLI. Keys are server-side secrets.

````