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

# Preview Signal

> Sample what a preset would detect, without creating a signal.

Returns up to 10 recent matches over a 30-day window. The sample is
wider than a live run: no audience filter and no dedup are applied, so
use it to check the detection filters, not to predict volume.

Requires the `signals:read` scope on the API key.



## OpenAPI

````yaml /openapi.json post /v1/signals/preview
openapi: 3.1.0
info:
  title: Topo Public API
  description: >-
    Public REST API for Topo. All requests require a bearer API key issued from
    Settings → Developers. See the **Guides** in the left sidebar for
    authentication and rate-limit details.
  version: 1.0.0
servers:
  - url: https://api.topo.io
    description: Production
security:
  - BearerApiKey: []
tags:
  - name: Authentication
    description: >-
      Every request to the Topo Public API is authenticated with a bearer API
      key.


      ```http

      Authorization: Bearer topo_live_4f8a9c2b...

      ```


      Requests without a valid bearer token receive `401 Unauthorized`. After
      repeated failures with the same token, further attempts are
      short-circuited with `429 Too Many Requests` (`data.window_seconds: 60`)
      so a stale or malformed key cannot be retried in a tight loop.


      ### Issuing keys


      API keys are issued from the dashboard under **Settings → Developers → API
      keys**. Each key is bound to a single organization — the workspace it was
      created in — and cannot be used to read or write data from any other
      organization.


      The raw key is shown **once** at creation and never displayed again. If
      the raw key is lost, revoke it and issue a new one.


      ### Scopes


      Keys carry a granular scope list of `resource:action` pairs. A request to
      an endpoint whose required scope is not on the key returns `403
      Forbidden`.


      v1 ships with the following scopes:


      - `account_exclusions:read`, `account_exclusions:write`

      - `account_lists:read`, `account_lists:write`

      - `accounts:read`, `accounts:write`

      - `activities:read`

      - `contact_exclusions:read`, `contact_exclusions:write`

      - `contact_lists:read`, `contact_lists:write`

      - `contacts:read`, `contacts:write`

      - `crm:write`

      - `events:read`, `events:write`

      - `messages:read`

      - `sequences:read`, `sequences:write`

      - `signals:read`, `signals:write`

      - `tasks:read`, `tasks:write`

      - `users:read`

      - `webhooks:read`, `webhooks:write`


      Each endpoint's required scope is listed in its description.


      ### Inspecting the current key


      `GET /v1/me` returns the principal behind the bearer token: the key id,
      its name, the organization id and name, the id of the user who issued the
      key (`null` when it was not issued by a person), the scope list, the key
      prefix, the last-used timestamp, and the creation date. Use it to confirm
      a key is wired up correctly before debugging endpoint-level errors.
  - name: Rate limits
    description: >-
      Requests are rate-limited per organization across three concurrent
      windows. The budget is shared by every API key issued for the
      organization.


      | Window | Default limit |

      |---|---|

      | Per second | 10 requests |

      | Per minute | 60 requests |

      | Per day | 10,000 requests |


      A request is accepted only if it fits inside **all three** windows.
      Exceeding any one of them returns `429`.


      ### Response headers


      Every response carries the state of all three windows. Each window is
      suffixed with its name — `second`, `minute`, or `day`:


      ```http

      X-RateLimit-Limit-second: 5

      X-RateLimit-Remaining-second: 2

      X-RateLimit-Reset-second: 1729435920

      X-RateLimit-Limit-minute: 60

      X-RateLimit-Remaining-minute: 13

      X-RateLimit-Reset-minute: 1729435932

      X-RateLimit-Limit-day: 1000

      X-RateLimit-Remaining-day: 842

      X-RateLimit-Reset-day: 1729519200

      ```


      | Header | Meaning |

      |---|---|

      | `X-RateLimit-Limit-{window}` | Capacity of that window |

      | `X-RateLimit-Remaining-{window}` | Requests left in that window before
      the next `429` |

      | `X-RateLimit-Reset-{window}` | Unix timestamp (seconds) at which that
      window is fully refilled |


      Each window is a token bucket that refills continuously, so
      `X-RateLimit-Remaining-{window}` recovers gradually under steady traffic —
      it does not snap back at a fixed boundary.


      ### When the limit is exceeded


      Requests over the budget return `429` with the standard error envelope and
      a `Retry-After` header. `data.window_seconds` identifies the tier that
      tripped (`1`, `60`, or `86400`):


      ```http

      Retry-After: 3

      ```


      ```json

      {
        "status_code": 429,
        "type": "RateLimitIssue",
        "message": "Rate limit exceeded",
        "data": {
          "limit": 60,
          "reset_at": 1729435932,
          "window_seconds": 60
        },
        "request_id": "req_01HX7K..."
      }

      ```


      `Retry-After` is the number of seconds until the next request is allowed
      and is the recommended signal for backoff. `X-RateLimit-Reset` marks when
      the window is fully refilled, which is later than `Retry-After` whenever
      the bucket has more than one slot.
  - name: Task lifecycle
    description: >-
      A task represents one action a rep is expected to take — reply to a
      message, review a new lead, make a call, or complete a standalone to-do.
      This guide covers the states a task moves through, and how the wording
      used in the Topo app maps onto the values returned by this API.


      ### Statuses


      | `status` | Meaning |

      |---|---|

      | `PENDING` | Waiting for the rep. This is where a task starts. |

      | `IN_PROGRESS` | A call task the rep has started but not yet logged. |

      | `COMPLETED` | The rep acted on the task — a reply was sent, a lead
      approved or refused, a call logged. |

      | `SKIPPED` | The rep dismissed the task without acting on it. Shown as
      **Archived** in the app. |


      `PENDING` and `IN_PROGRESS` are the actionable states. `COMPLETED` and
      `SKIPPED` are terminal, but both can be reversed with `POST
      /v1/tasks/{task_id}/reopen`.


      ### App wording vs. API wording


      The Topo app calls the dismiss action **Archive**. On this API the same
      operation is called *skip*. They are the same thing — only the label
      differs:


      | In the Topo app | On this API |

      |---|---|

      | The **Archive** button | `POST /v1/tasks/{task_id}/skip` |

      | The **Archived** badge | `status: "SKIPPED"` |

      | "Archived 2 hours ago" | `skipped_at` |

      | — | webhook event `task.skipped` |


      These names are part of the frozen `/v1` contract and will not change
      within this major version. If you are mapping API data back to what a user
      sees in the app, treat `SKIPPED` and `skipped_at` as "archived".


      ### Ending a task


      | Action | Endpoint | Result |

      |---|---|---|

      | Act on it | `POST /v1/tasks/{task_id}/execute` | `COMPLETED` +
      `completed_at`, and the task's own side effects (message sent, lead
      enrolled or refused, call logged) |

      | Mark done without acting | `POST /v1/tasks/{task_id}/complete` |
      `COMPLETED` + `completed_at` |

      | Dismiss it (archive) | `POST /v1/tasks/{task_id}/skip` | `SKIPPED` +
      `skipped_at` |


      `execute` is the only one of the three that changes anything beyond the
      task itself. `complete` and `skip` record an outcome and nothing more.


      Both terminal states emit a webhook if you are subscribed:
      `task.completed` and `task.skipped` respectively.


      ### Reopening


      `POST /v1/tasks/{task_id}/reopen` returns a `COMPLETED` or `SKIPPED` task
      to `PENDING`. It clears `completed_at` and `skipped_at`, and sets
      `reopened_at`. A task that is already actionable cannot be reopened.


      `reopen_reason` distinguishes an automatic reopen from a manual one.
      `REMINDER_RULE` means Topo brought the task back on its own because a sent
      message went unanswered; `null` means a person reopened it.


      ### Snoozing


      Deferring a task ("Snooze") does **not** change its status. The task stays
      `PENDING` and drops out of the active queue until the chosen time passes,
      then reappears on its own.


      Use `POST /v1/tasks/{task_id}/snooze` with `{ "snoozed_until": "<ISO-8601
      timestamp>" }` to defer a task. The `snoozed_until` timestamp is also
      returned on task objects in `GET /v1/tasks` and `GET /v1/tasks/{task_id}`.
  - name: me
    description: >-
      Identity of the API key in use: the workspace it belongs to and its
      scopes.
  - name: search
    description: >-
      Find records by name across kinds and load any match back. A search result
      carries a typed id (`<type>:<uuid>`) that `GET /v1/fetch` resolves into
      the full record, so a name is enough to reach a lead, an account, a
      sequence template or a task without knowing which endpoint owns it.
  - name: users
    description: List users in the authenticated organization.
  - name: contacts
    description: Manage contact lists, list entries, and contact-level exclusions.
  - name: accounts
    description: Manage account lists, list entries, and account-level exclusions.
  - name: sequences
    description: Manage outbound sequences and inspect their execution state.
  - name: sequence-templates
    description: Manage reusable sequence templates that seed new sequences.
  - name: activities
    description: >-
      Read the immutable event log produced by outbound sequences (sent, opened,
      replied, meeting booked, etc.).
  - name: events
    description: >-
      Ingest first-party events (product usage, website activity, form
      submissions) and read them back to verify ingestion and resolution. Every
      well-formed event is accepted (HTTP 202) and stored. Subject correlation
      to contacts and accounts is asynchronous and best-effort. Replays with the
      same `external_event_id` or identical event content are deduplicated.
  - name: signals
    description: >-
      Create and run buying signals: a signal watches one catalog preset
      (hiring, funding, technology adoption, news, LinkedIn activity, or your
      own first-party events), checks it daily, and imports the qualified people
      or companies into a destination list it creates. Read the imports feed to
      see what each signal brought in.
  - name: webhooks
    description: Manage webhook subscriptions to receive outbound event notifications.
  - name: tasks
    description: List and inspect tasks in the workspace.
  - name: messages
    description: Read email and LinkedIn messages exchanged in outreach sequences.
paths:
  /v1/signals/preview:
    post:
      tags:
        - signals
      summary: Preview Signal
      description: |-
        Sample what a preset would detect, without creating a signal.

        Returns up to 10 recent matches over a 30-day window. The sample is
        wider than a live run: no audience filter and no dedup are applied, so
        use it to check the detection filters, not to predict volume.

        Requires the `signals:read` scope on the API key.
      operationId: preview_signal_v1_signals_preview_post
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PreviewSignalPublicInput'
        required: true
        description: Preview Signal payload
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SignalPreviewPublicOutput'
components:
  schemas:
    PreviewSignalPublicInput:
      properties:
        signal_type:
          $ref: '#/components/schemas/SignalType'
          description: >-
            Which preset to sample. Read `GET /v1/signals/presets` first — a
            preset with `preview_supported: false` cannot be previewed.
        search_filters:
          $ref: '#/components/schemas/FilterTexts'
          description: >-
            Detection filters, keyed by the preset's `filter_keys`. Same shape
            as `POST /v1/signals`.
          default: {}
      type: object
      required:
        - signal_type
      title: PreviewSignalPublicInput
    SignalPreviewPublicOutput:
      properties:
        window_days:
          type: integer
          title: Window Days
          description: How many days back the sample looked.
        total_count:
          anyOf:
            - type: integer
            - type: 'null'
          title: Total Count
          description: >-
            How many facts the source reports over the window, when it counts
            them.
        items:
          items:
            $ref: '#/components/schemas/SignalPreviewEntryPublicOutput'
          type: array
          title: Items
          description: A sample of recent matches, newest first.
        unavailable_reason:
          anyOf:
            - type: string
            - type: 'null'
          title: Unavailable Reason
          description: >-
            Why this preset returned no sample. Empty when a sample was
            attempted.
      type: object
      required:
        - window_days
        - total_count
        - items
      title: SignalPreviewPublicOutput
    SignalType:
      type: string
      enum:
        - LINKEDIN_OWN_POST_ENGAGEMENT
        - LINKEDIN_COMPETITOR_POST_ENGAGEMENT
        - LINKEDIN_PERSON_POST_ENGAGEMENT
        - LINKEDIN_KEYWORD_MENTION
        - LINKEDIN_OWN_COMPANY_FOLLOWERS
        - LINKEDIN_COMPETITOR_FOLLOWERS
        - LINKEDIN_COMPANY_PAGE_FOLLOWERS
        - HIRING_SIGNALS
        - JOB_DESCRIPTION_KEYWORD
        - FUNDING_EVENTS
        - TECH_ADOPTION
        - NEWS_EVENTS
        - NEW_JOB
        - CUSTOM_EVENT
        - LINKEDIN_PARTNER_FOLLOWERS
        - LINKEDIN_CUSTOMER_FOLLOWERS
        - LINKEDIN_PARTNER_POST_ENGAGEMENT
        - LINKEDIN_CUSTOMER_POST_ENGAGEMENT
        - LINKEDIN_COMPANY_PAGE_POST_ENGAGEMENT
        - LINKEDIN_OWN_PROFILE_POST_ENGAGEMENT
        - LINKEDIN_OWN_EMPLOYEES_POST_ENGAGEMENT
        - LINKEDIN_COMPETITOR_EMPLOYEES_POST_ENGAGEMENT
        - LINKEDIN_PARTNER_EMPLOYEES_POST_ENGAGEMENT
        - LINKEDIN_CUSTOMER_EMPLOYEES_POST_ENGAGEMENT
        - LINKEDIN_OWN_BRAND_MENTION
        - LINKEDIN_COMPETITOR_MENTION
        - LINKEDIN_PARTNER_MENTION
        - LINKEDIN_CUSTOMER_MENTION
        - LINKEDIN_EVENT_MENTION
        - COMPETITOR_TECH_ADOPTION
        - INTEGRATION_TECH_ADOPTION
        - LINKEDIN_EVENT_PAGE_FOLLOWERS
        - CHAMPION_CHANGED_JOB
        - CRM_LIST_ENTRY
      title: SignalType
    FilterTexts:
      additionalProperties:
        items:
          type: string
        type: array
      propertyNames:
        $ref: '#/components/schemas/FilterFieldKey'
      type: object
    SignalPreviewEntryPublicOutput:
      properties:
        event_type:
          $ref: '#/components/schemas/SignalEventType'
          description: What kind of fact the source detected.
        company_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Company Name
          description: Name of the detected company.
        company_domain:
          anyOf:
            - type: string
            - type: 'null'
          title: Company Domain
          description: Domain of the detected company.
        first_name:
          anyOf:
            - type: string
            - type: 'null'
          title: First Name
          description: First name of the detected person.
        last_name:
          anyOf:
            - type: string
            - type: 'null'
          title: Last Name
          description: Last name of the detected person.
        job_title:
          anyOf:
            - type: string
            - type: 'null'
          title: Job Title
          description: Job title of the detected person.
        linkedin_url:
          anyOf:
            - type: string
            - type: 'null'
          title: Linkedin Url
          description: LinkedIn URL of the detected person.
        email:
          anyOf:
            - type: string
            - type: 'null'
          title: Email
          description: Email of the detected person.
      type: object
      required:
        - event_type
      title: SignalPreviewEntryPublicOutput
    SignalEventType:
      type: string
      enum:
        - CLIENT_POST_REACTION
        - CLIENT_POST_COMMENT
        - COMPETITOR_POST_REACTION
        - COMPETITOR_POST_COMMENT
        - PERSON_POST_REACTION
        - CLIENT_PAGE_FOLLOW
        - COMPETITOR_PAGE_FOLLOW
        - COMPANY_PAGE_FOLLOW
        - PUBLIC_BRAND_MENTION_LINKEDIN
        - PAIN_POINT_PUBLIC_POST
        - LEADERSHIP_HIRING
        - HIRING_SURGE
        - NEW_ROLE
        - TECH_ADOPTED
        - TECH_DROPPED
        - FUNDING_EVENT
        - M_AND_A_EVENT
        - LEADERSHIP_CHANGE_NEWS
        - BRAND_MENTION_NEWS
        - CUSTOM_EVENT
      title: SignalEventType
  securitySchemes:
    BearerApiKey:
      type: http
      scheme: bearer
      bearerFormat: topo_live_<64hex>
      description: Bearer API key issued from Settings → Developers.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.