> ## 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.

# Search for leads on X now

> One live keyword search over X, scored against an ideal-customer
profile, returned in the response.

**It creates nothing.** No signal agent, no saved leads, no contact
list membership: the leads exist only in this response. For an
audience that keeps filling up on its own, create a signal agent
(`POST /v1/signals/agents`) instead, and read what it finds with
`GET /v1/signals/leads`.

**It plans, then searches.** From `offer`, `icp_description` and your
seed angles, a model writes up to 10 short queries across the angles
that find buyers (the buyer's working day, their pains, the tools they
already pay for, adjacent roles), runs them all, and scores everyone it
finds. `queries_used` says how many reached X, `query_plan` shows each
query with its angle and what it contributed, and `partial` is true
when a time budget cut the run short.

So `keywords` are seed ANGLES, not the query: 2-5 short phrases of how
the BUYER talks on a normal working day, not the product's own name and
not a full sentence. `offer` is optional but it is the strongest input
you can give. `precision: high` keeps only confident matches;
`discovery` (the default) also returns adjacent ones.

The search takes up to a minute. Each lead comes from ONE matched
post, so treat `posts_count` as lifetime volume rather than proof of
current activity.

**Only recent posts count.** `max_post_age_days` sets the window
(1-90, 30 by default): a matching post older than that is skipped and
counted in `freshness.stale_skipped`, and every lead carries its
post's `posted_at` and `post_age_days`. Leads come back freshest
first within each score. An empty `leads` with a `stale_skipped`
above 0 means people do post about this, just not inside the window:
broaden the keywords, or raise `max_post_age_days` when you want
people who are active over a longer stretch rather than a fresh pain
point.

**Cost.** Reading X live costs AI credits, measured from what the
search actually spends: at least 1 credit for any search that reaches
X, typically 1-2. It also uses one of the plan's daily lead searches
(`429 ai_action_limited` with `scope: "account"`) and a slice of a
platform-wide fair-use ceiling on live-data actions (`scope:
"platform"`, shared with every account, so wait and retry rather than
assuming your own quota is gone). A search that never reaches X is
not charged.

Nothing is written by this endpoint, but it still needs a key with
the `write` scope, because it spends AI credits and live requests:
every non-GET `/v1` request needs that scope anyway. The MCP
`search_leads` tool needs the write scope for the same reason. On
both surfaces an account shared with you works, unlike the endpoints
that really write: nothing is written to the account, and the credits
come from the key owner's pool.




## OpenAPI

````yaml /api-reference/openapi.yaml post /v1/signals/leads/search
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/signals/leads/search:
    post:
      tags:
        - Signals
      summary: Search for leads on X now
      description: |
        One live keyword search over X, scored against an ideal-customer
        profile, returned in the response.

        **It creates nothing.** No signal agent, no saved leads, no contact
        list membership: the leads exist only in this response. For an
        audience that keeps filling up on its own, create a signal agent
        (`POST /v1/signals/agents`) instead, and read what it finds with
        `GET /v1/signals/leads`.

        **It plans, then searches.** From `offer`, `icp_description` and your
        seed angles, a model writes up to 10 short queries across the angles
        that find buyers (the buyer's working day, their pains, the tools they
        already pay for, adjacent roles), runs them all, and scores everyone it
        finds. `queries_used` says how many reached X, `query_plan` shows each
        query with its angle and what it contributed, and `partial` is true
        when a time budget cut the run short.

        So `keywords` are seed ANGLES, not the query: 2-5 short phrases of how
        the BUYER talks on a normal working day, not the product's own name and
        not a full sentence. `offer` is optional but it is the strongest input
        you can give. `precision: high` keeps only confident matches;
        `discovery` (the default) also returns adjacent ones.

        The search takes up to a minute. Each lead comes from ONE matched
        post, so treat `posts_count` as lifetime volume rather than proof of
        current activity.

        **Only recent posts count.** `max_post_age_days` sets the window
        (1-90, 30 by default): a matching post older than that is skipped and
        counted in `freshness.stale_skipped`, and every lead carries its
        post's `posted_at` and `post_age_days`. Leads come back freshest
        first within each score. An empty `leads` with a `stale_skipped`
        above 0 means people do post about this, just not inside the window:
        broaden the keywords, or raise `max_post_age_days` when you want
        people who are active over a longer stretch rather than a fresh pain
        point.

        **Cost.** Reading X live costs AI credits, measured from what the
        search actually spends: at least 1 credit for any search that reaches
        X, typically 1-2. It also uses one of the plan's daily lead searches
        (`429 ai_action_limited` with `scope: "account"`) and a slice of a
        platform-wide fair-use ceiling on live-data actions (`scope:
        "platform"`, shared with every account, so wait and retry rather than
        assuming your own quota is gone). A search that never reaches X is
        not charged.

        Nothing is written by this endpoint, but it still needs a key with
        the `write` scope, because it spends AI credits and live requests:
        every non-GET `/v1` request needs that scope anyway. The MCP
        `search_leads` tool needs the write scope for the same reason. On
        both surfaces an account shared with you works, unlike the endpoints
        that really write: nothing is written to the account, and the credits
        come from the key owner's pool.
      operationId: searchLeads
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LeadSearchRequest'
            example:
              keywords: cancelled today, mrr dropped, renewal call, churn rate
              icp_description: >-
                Founders of small B2B SaaS companies who are worried about
                retention and would try a churn-reduction tool.
              offer: >-
                ChurnRadar, retention analytics that flags the accounts about to
                cancel.
              precision: discovery
              max_leads: 10
              max_post_age_days: 7
      responses:
        '200':
          description: |
            The search finished. `leads` is empty when nothing qualified,
            with a `note` saying so.
          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:
                    $ref: '#/components/schemas/LeadSearchResult'
                  note:
                    type: string
              example:
                data:
                  query_used: >-
                    ("cancelled today" OR "they churned" OR "lost a customer")
                    (saas OR mrr OR retention) lang:en -filter:replies
                    -filter:retweets
                  query_source: planned
                  queries_used: 10
                  query_plan:
                    - query: >-
                        ("cancelled today" OR "they churned" OR "lost a
                        customer") (saas OR mrr OR retention) lang:en
                        -filter:replies -filter:retweets
                      angle: workflow
                      posts_in_window: 24
                      candidates: 11
                    - query: >-
                        (churnzero OR chartmogul OR baremetrics) (churn OR
                        retention OR mrr) lang:en -filter:replies
                        -filter:retweets
                      angle: competitor
                      posts_in_window: 9
                      candidates: 4
                  partial: false
                  precision: discovery
                  requests_used: 12
                  posts_scanned: 194
                  candidates_scored: 60
                  credits_charged: 2
                  freshness:
                    max_post_age_days: 7
                    posts_in_window: 31
                    stale_skipped: 4
                    oldest_skipped_days: 212
                  leads:
                    - user:
                        x_user_id: '1178367350552305665'
                        username: robj3d3
                        name: Rob
                        avatar_url: https://pbs.twimg.com/profile_images/abc_normal.jpg
                        followers_count: 8213
                        posts_count: 4102
                      icp_score: 4
                      icp_rationale: >-
                        Runs a small B2B SaaS and is actively complaining about
                        cancellations.
                      provenance:
                        action: authored
                        tweet_id: '1969318805251100000'
                        tweet_text: >-
                          third cancellation this week and I still cannot tell
                          why they left
                        target_handle: null
                        posted_at: '2026-09-12T08:41:07.000Z'
                        post_age_days: 2
                note: >-
                  Nothing was saved: this search creates no signal agent and
                  stores no leads.
        '400':
          $ref: '#/components/responses/InvalidParameter'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/WriteForbidden'
        '404':
          $ref: '#/components/responses/AccountNotFound'
        '429':
          $ref: '#/components/responses/AiActionLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '502':
          $ref: '#/components/responses/UpstreamError'
        '503':
          $ref: '#/components/responses/AccountsUnavailable'
components:
  schemas:
    LeadSearchRequest:
      type: object
      required:
        - keywords
        - icp_description
      properties:
        keywords:
          oneOf:
            - type: string
            - type: array
              items:
                type: string
          description: |
            Seed ANGLES for the search plan, not the query itself: 2-5 short
            phrases (2-3 words each) of how the BUYER talks on a normal working
            day, their workflows, the tools they already pay for, their jargon,
            a specific symptom. One comma-separated string or a list of strings
            (a list is joined with commas), 3-300 characters in total. Never the
            product's own name, never a full sentence, no search operators.
        icp_description:
          type: string
          minLength: 3
          maxLength: 500
          description: |
            Who counts as a good lead, in one or two sentences: role, domain,
            and the buying intent or pain that qualifies them.
        offer:
          type: string
          minLength: 3
          maxLength: 300
          description: |
            What you are selling, in one sentence: the product or service and
            what it does. Optional, and the single strongest input: the search
            plan is written from it, so a search with an `offer` finds people
            talking about the problem rather than only people naming the
            product.
        precision:
          type: string
          enum:
            - high
            - discovery
          default: discovery
          description: >-
            high keeps only confident matches; discovery also returns adjacent
            ones.
        max_leads:
          type: integer
          minimum: 1
          maximum: 30
          default: 10
        max_post_age_days:
          type: integer
          minimum: 1
          maximum: 90
          default: 30
          description: |
            Only posts written within the last N days count. Older matching
            posts are skipped and counted in `freshness.stale_skipped`. Use 7
            for a pain point or buying intent worth catching while it is
            fresh, 1-3 for today, 30 for anyone who is simply active.
        account_id:
          type: string
          description: Which of your accounts to search as. Omit for the main account.
    LeadSearchResult:
      type: object
      properties:
        query_used:
          type: string
          description: |
            The first query of the plan, kept for callers that read a single
            query. Read `query_plan` for everything that ran.
        query_source:
          type: string
          enum:
            - planned
            - compiled
            - keywords
          description: |
            planned = a model wrote the search plan from your offer, ICP and
            seed angles; keywords = the deterministic fallback, one query per
            seed phrase. compiled is the retired single-query path.
        queries_used:
          type: integer
          description: How many of the planned queries actually reached X.
        query_plan:
          type: array
          description: |
            Every planned query, its angle, and what it brought back. Angles are
            workflow, pain, competitor and adjacent for a planned search, and
            keywords for the fallback.
          items:
            type: object
            properties:
              query:
                type: string
              angle:
                type: string
              posts_in_window:
                type: integer
                description: Matching posts this query found inside the recency window.
              candidates:
                type: integer
                description: People this query contributed to the scored pool.
        partial:
          type: boolean
          description: |
            True when a time or request budget cut the search short, so the
            plan was not run to the end. The leads returned are still real.
        precision:
          type: string
          enum:
            - high
            - discovery
        requests_used:
          type: integer
          description: Live requests this search spent.
        posts_scanned:
          type: integer
        candidates_scored:
          type: integer
        credits_charged:
          type: integer
        freshness:
          type: object
          description: >-
            The recency window this search ran with, and what it cost in
            matches.
          properties:
            max_post_age_days:
              type: integer
              description: The window that was applied, in days.
            posts_in_window:
              type: integer
              description: Matching posts that were inside the window.
            stale_skipped:
              type: integer
              description: |
                Matching posts that were older than the window and were
                skipped. Above 0 with no leads means people do post about
                this, just not lately.
            oldest_skipped_days:
              type: integer
              nullable: true
              description: >-
                Age of the oldest skipped post, in days. null when none were
                skipped.
        leads:
          type: array
          items:
            $ref: '#/components/schemas/LeadSearchLead'
    LeadSearchLead:
      type: object
      properties:
        user:
          type: object
          properties:
            x_user_id:
              type: string
              description: The X account id, as a string (X ids exceed 2^53).
            username:
              type: string
              nullable: true
              description: X handle without the @.
            name:
              type: string
              nullable: true
            avatar_url:
              type: string
              nullable: true
            followers_count:
              type: integer
              nullable: true
            posts_count:
              type: integer
              nullable: true
              description: Lifetime posts, not recent activity.
        icp_score:
          type: integer
          nullable: true
          description: >-
            How well this person matches the profile, 2-5 (anything lower is
            dropped).
        icp_rationale:
          type: string
          nullable: true
        provenance:
          type: object
          description: >-
            How the person was found. Mirrors the provenance on stored signal
            leads.
          properties:
            action:
              type: string
              description: Always "authored" here, meaning they wrote a matching post.
            tweet_id:
              type: string
              nullable: true
              description: |
                The matched post's id, as a string, named as it is on stored
                signal leads. Pass it to the endpoints that take a post id,
                such as `POST /v1/engage/reply-draft`.
            tweet_text:
              type: string
              nullable: true
              description: The matched post, shortened.
            target_handle:
              type: string
              nullable: true
            posted_at:
              type: string
              format: date-time
              nullable: true
              description: When the matched post was written (UTC).
            post_age_days:
              type: integer
              nullable: true
              description: |
                How many days ago the matched post was written. Never above
                the request's `max_post_age_days`.
    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).
    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
    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).
  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.
    AccountNotFound:
      description: account_id is not one of your accounts.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: account_not_found
              message: No account with that id belongs to this key
    AiActionLimited:
      description: |
        Rate limited (`rate_limited`, honor `Retry-After`), the AI credit pool
        is used up (`ai_credits_exhausted`, with the credit counters), or a
        daily cap on the action was hit (`ai_action_limited`).

        On `ai_action_limited`, `error.scope` says which ceiling. `account`
        is one of your own: the plan's daily cap for a metered action, the
        once-an-hour limit on style guide regeneration (where `reset_at` is an
        hour after the last run rather than midnight), the 3 live style-guide
        attempts a day, or the 20 page reads a day shared with the SuperX app.
        `platform` is the platform-wide fair-use ceiling on live-data actions,
        shared with every SuperX account: a `platform` refusal is not about
        this account's quota, so wait for `reset_at` and retry.

        The free endpoints send `X-Credits-Remaining` and `X-Credits-Reset` on
        this 429 as they do on a 200, since nothing was charged either way.
      headers:
        Retry-After:
          description: Seconds to wait before retrying. Sent on every flavour of this 429.
          schema:
            type: integer
        X-Credits-Remaining:
          description: Credits left in the pool. Sent only on `ai_credits_exhausted`.
          schema:
            type: integer
        X-Credits-Reset:
          description: >-
            Unix seconds when the pool refills. Sent only on
            `ai_credits_exhausted`.
          schema:
            type: integer
      content:
        application/json:
          schema:
            anyOf:
              - $ref: '#/components/schemas/CreditExhaustedError'
              - $ref: '#/components/schemas/ActionLimitedError'
              - $ref: '#/components/schemas/ErrorEnvelope'
          examples:
            ai_action_limited_account:
              value:
                error:
                  code: ai_action_limited
                  message: >-
                    Daily limit reached for this action on your plan (10 lead
                    searches per day). It resets at midnight UTC.
                  scope: account
                  retry_after: 20400
                  reset_at: 1789430400
            ai_action_limited_platform:
              value:
                error:
                  code: ai_action_limited
                  message: >-
                    The platform-wide fair-use ceiling for live-data actions has
                    been reached for today. It resets at midnight UTC.
                  scope: platform
                  retry_after: 20400
                  reset_at: 1789430400
            ai_credits_exhausted:
              value:
                error:
                  code: ai_credits_exhausted
                  message: Not enough AI credits. Resets in 5 hours.
                  credits_required: 2
                  credits_remaining: 0
                  reset_at: 1790812800
    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.

````