Annsa Ingest API

Getting customer signal into Annsa.

Operations 3

POST /feedback Submit one piece of feedback, or a batch #
POST /transcripts/upload Upload a call transcript for segmentation #
POST /surface/submit Submit feedback with a survey key #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/annsa-ai:annsa-ai-ingest-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

annsa-ai-ingest-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Annsa Ingest API
  version: '2026-08-22'
  summary: Send customer feedback and call transcripts into Annsa, and reach its priorities and specs over MCP.
  description: 'Annsa is the source of direction for your agents. Choose what to build from your customers, roadmap, ideas and agent findings, then direct every agent and learn as you go.


    This document describes only what is public and callable today. Endpoints used by the Annsa web app on a user''s own session are deliberately absent.


    ## Rate limits


    Limits are abuse ceilings, not metering — plan quotas are separate and are enforced on your account, not on the connection. `POST /feedback` and `POST /surface/submit` allow 5000 requests an hour; `POST /transcripts/upload` allows 500, because each call does real work. `POST /mcp` and `GET /health` carry no limit of their own.


    A window is counted per authenticated caller, falling back to the client IP when there is no credential. Limited endpoints report where you stand in it on every response — `RateLimit`, plus the discrete `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` — so you never have to discover a limit by being refused. `RateLimit-Reset` is delta-seconds, not a timestamp. A 429 adds `Retry-After`. The full policy is at https://annsa.ai/api-policy.


    ## Which credential to use


    There are two separate authentication systems, and they are not interchangeable:


    - **`sessionBearer`** — a Supabase session token belonging to a signed-in user. Required by `POST /feedback` and `POST /transcripts/upload`. A survey key sent here returns 401.

    - **`surveyKey`** — the key from Settings → Integrations → Survey. It authenticates `POST /surface/submit` **only**, and travels in the JSON body as `api_key`, not in a header — so it is described on the `WidgetSubmission` schema rather than as a security scheme, because OpenAPI has no way to express a body-field credential. This key is embedded in public web pages by design, so its reach is deliberately narrow.


    For reading priorities, specs and search, use the MCP server rather than REST. It is OAuth 2.0 with PKCE and scoped `read`/`write`, and it is the surface built for agents. See `POST /mcp`.


    ## Errors


    Every error response is `application/json` in the shape `{"detail": "..."}` (the `Error` schema). `detail` is a human-readable string; it is not a stable machine code, so branch on the HTTP status rather than on the text.


    ## Versioning


    This API is unversioned in the path. The `info.version` above is the date this document was published, and this document is the contract. Additive changes — new optional fields, new endpoints — ship without notice, so clients must ignore unknown response fields. Breaking changes are announced at https://annsa.ai/changelog. A formal deprecation window with `Deprecation` and `Sunset` response headers is not yet in force; do not infer one from this document.'
  contact:
    name: Annsa
    url: https://annsa.ai/contact
    email: support@annsa.ai
  license:
    name: Proprietary
    url: https://annsa.ai/terms
  termsOfService: https://annsa.ai/terms
servers:
- url: https://api.annsa.ai
  description: Production
security:
- sessionBearer: []
tags:
- name: Ingest
  description: Getting customer signal into Annsa.
paths:
  /feedback:
    post:
      operationId: submitFeedback
      summary: Submit one piece of feedback, or a batch
      description: 'Stores customer feedback and queues it for classification, clustering and ranking.


        Send either a single item (a `text` field at the top level) or a batch (an `items` array). The two shapes return different bodies — see the responses below.


        Annsa hashes the text, so the same content submitted twice creates one item. Supply `external_id` and the second submission is skipped on that id regardless of what the text says.


        Requires a signed-in user''s session token. The survey key from Settings → Integrations → Survey does **not** authenticate this endpoint; use `POST /surface/submit` for that.


        Rate limit: 5000 requests per hour.'
      tags:
      - Ingest
      security:
      - sessionBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
              - $ref: '#/components/schemas/FeedbackItem'
              - $ref: '#/components/schemas/FeedbackBatch'
            examples:
              single:
                summary: One piece of feedback
                value:
                  text: The export is too slow on large datasets
                  customer_name: Jane Smith
                  customer_email: jane@example.com
                  revenue_band: 199
                  source: api
              batch:
                summary: Up to 1000 items in one request
                value:
                  items:
                  - text: Can't find the export button
                    customer_email: alex@example.com
                    revenue_band: 49
                  - text: Love the new dashboard layout
                    customer_name: Sam Lee
      responses:
        '200':
          description: Stored. A single submission returns the created id, or a duplicate marker if the text was already present. A batch returns one summary object covering every item.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: '#/components/schemas/FeedbackAccepted'
                - $ref: '#/components/schemas/FeedbackDuplicate'
                - $ref: '#/components/schemas/FeedbackBatchResult'
              examples:
                accepted:
                  value:
                    id: 7c2f1a94-0d6e-4a1b-9f33-2b6f0c5a71de
                duplicate:
                  value:
                    id: 7c2f1a94-0d6e-4a1b-9f33-2b6f0c5a71de
                    duplicate: true
                    original_source: slack
          headers:
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            Idempotent-Replayed:
              $ref: '#/components/headers/IdempotentReplayed'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          $ref: '#/components/responses/IdempotencyConflict'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
      parameters:
      - $ref: '#/components/parameters/AnnsaVersion'
      - $ref: '#/components/parameters/IdempotencyKey'
  /transcripts/upload:
    post:
      operationId: uploadTranscript
      summary: Upload a call transcript for segmentation
      description: 'Accepts transcript text and returns immediately; segmentation into individual signals runs in the background.


        `title` is required, which the prose guide does not currently say. `text` must be between 50 and 500,000 characters.


        If the transcript carries speaker labels, name the customer''s label in `customer_speaker` so only their words become signals. Detect the available labels first with `POST /transcripts/detect-speakers`.


        Requires a signed-in user''s session token. Rate limit: 500 requests per hour.'
      tags:
      - Ingest
      security:
      - sessionBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TranscriptUpload'
            examples:
              discovery:
                summary: A discovery call
                value:
                  title: Acme — discovery call
                  text: 'Interviewer: What made you look for something like this?

                    Jane: We were tracking feature requests in a spreadsheet and it kept going stale...'
                  transcript_type: discovery
                  transcript_date: '2026-08-14'
                  company_name: Acme
                  customer_name: Jane Smith
                  customer_speaker: Jane
      responses:
        '200':
          description: Accepted for processing.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TranscriptAccepted'
          headers:
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
            Idempotent-Replayed:
              $ref: '#/components/headers/IdempotentReplayed'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          $ref: '#/components/responses/IdempotencyConflict'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
      parameters:
      - $ref: '#/components/parameters/AnnsaVersion'
      - $ref: '#/components/parameters/IdempotencyKey'
  /surface/submit:
    post:
      operationId: submitSurveyFeedback
      summary: Submit feedback with a survey key
      description: 'The endpoint the in-product survey widget posts to, and the only one a survey key authenticates.


        The key travels in the request body as `api_key`, not in an `Authorization` header. That is deliberate: the key is embedded in public web pages, so it is scoped to this single write and nothing else. It cannot read anything, and it cannot reach `POST /feedback`.


        Rate limit: 5000 requests per hour.'
      tags:
      - Ingest
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WidgetSubmission'
            examples:
              widget:
                value:
                  api_key: your-survey-key
                  text: The export is too slow on large datasets
                  email: jane@example.com
                  feedback_type: bug
                  page_url: https://example.com/reports
                  user_metadata:
                    plan: pro
                    mrr: 199
      responses:
        '200':
          description: Stored.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FeedbackAccepted'
          headers:
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
        '401':
          description: The `api_key` was missing, unknown or revoked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                detail: Invalid API key
          headers:
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
            RateLimit-Reset:
              $ref: '#/components/headers/RateLimitReset'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
      parameters:
      - $ref: '#/components/parameters/AnnsaVersion'
components:
  headers:
    IdempotentReplayed:
      description: Present and `true` when this answer is the stored one for an `Idempotency-Key` already used, rather than a new write.
      schema:
        type: string
        enum:
        - 'true'
    RateLimitRemaining:
      description: Requests left in the current window.
      schema:
        type: integer
        example: 4999
    RateLimitReset:
      description: Seconds until the window resets. Delta-seconds, per the IETF draft — not an absolute timestamp.
      schema:
        type: integer
        example: 3599
    RateLimit:
      description: Your position in the current window, as the combined field from the later IETF drafts. Present on any response where the limit was actually evaluated.
      schema:
        type: string
        example: limit=5000, remaining=4999, reset=3599
    RateLimitLimit:
      description: Requests permitted in the window.
      schema:
        type: integer
        example: 5000
  responses:
    Unauthorized:
      description: Missing, invalid or expired credential.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            detail: Authentication token is invalid or expired. Please log in again.
    Unprocessable:
      description: The body parsed but failed validation.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            detail: Invalid feedback format. Please check your data and try again.
    BadRequest:
      description: The request body was malformed or a required field was absent.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            detail: Either 'text' or 'items' field required
    RateLimited:
      description: Rate limit exceeded.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
        RateLimit:
          $ref: '#/components/headers/RateLimit'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ServerError:
      description: Unexpected server error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    IdempotencyConflict:
      description: A request with this Idempotency-Key is still running. Retry after the number of seconds in `Retry-After`.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
            example: 1
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            detail: A request with this Idempotency-Key is still running. Retry shortly.
  parameters:
    AnnsaVersion:
      name: Annsa-Version
      in: header
      required: false
      description: Pin the dated API contract. Omit it to use the current version. An unrecognised value is refused with 400 rather than downgraded. The version that served the request is echoed back in the same header. See https://annsa.ai/api-policy.
      schema:
        type: string
        enum:
        - '2026-08-22'
        example: '2026-08-22'
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: 'Any unique string, up to 255 visible ASCII characters, to make a retry safe. The first request with a key runs, and its answer is kept for 24 hours for the credential that sent it. A retry with the same key and body gets that answer back, marked `Idempotent-Replayed: true`, without writing again. The same key with a different body is refused with 422, and a retry while the first is still running gets 409 with `Retry-After`. Leave it out and every request runs.'
      schema:
        type: string
        minLength: 1
        maxLength: 255
      example: 7f3c2a9e-5b1d-4e8a-9c61-2d0f8b4a7e15
  schemas:
    TranscriptUpload:
      type: object
      title: TranscriptUpload
      required:
      - title
      - text
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 500
        text:
          type: string
          minLength: 50
          maxLength: 500000
          description: The transcript body. Speaker labels are preserved if present.
        transcript_type:
          type: string
          default: discovery
          description: What kind of conversation this was.
        transcript_date:
          type: string
          description: When the conversation happened. Preserved on export.
        company_name:
          type: string
        customer_name:
          type: string
        customer_email:
          type: string
          format: email
        revenue_band:
          type: string
        sku:
          type: string
        project_id:
          type: string
        customer_speaker:
          type: string
          maxLength: 64
          description: The customer's speaker label. Supply it and only their words become signals, so the interviewer's questions never reach the roadmap. Omit it and segmentation treats the whole transcript as one voice.
    Error:
      type: object
      title: Error
      description: The single error shape every endpoint returns. `detail` is human-readable and may change; branch on the HTTP status code instead.
      required:
      - detail
      properties:
        detail:
          type: string
          description: What went wrong, in plain language.
          examples:
          - Invalid API key
    TranscriptAccepted:
      type: object
      title: TranscriptAccepted
      description: Accepted; segmentation continues in the background.
      properties:
        id:
          type: string
        status:
          type: string
      additionalProperties: true
    FeedbackBatch:
      type: object
      title: FeedbackBatch
      description: Up to 1000 items in one request.
      required:
      - items
      properties:
        items:
          type: array
          minItems: 1
          maxItems: 1000
          items:
            $ref: '#/components/schemas/FeedbackItem'
        project_id:
          type: string
          description: Applies to every item in the batch.
    FeedbackDuplicate:
      type: object
      title: FeedbackDuplicate
      description: The text or external_id already existed, so nothing new was stored. The id points at the item already held.
      required:
      - id
      - duplicate
      properties:
        id:
          type: string
        duplicate:
          type: boolean
          const: true
        original_source:
          type: string
          description: Where the item already held came from.
          examples:
          - slack
    FeedbackAccepted:
      type: object
      title: FeedbackAccepted
      description: A single item was stored.
      required:
      - id
      properties:
        id:
          type: string
          description: The stored feedback's identifier.
    FeedbackItem:
      type: object
      title: FeedbackItem
      description: One piece of customer feedback.
      required:
      - text
      properties:
        text:
          type: string
          minLength: 1
          description: The feedback content. The only required field.
        customer_name:
          type: string
          description: Appears in Customer Voice quotes.
        customer_email:
          type: string
          format: email
          description: Enables close-the-loop notifications when the request ships.
        revenue_band:
          type:
          - number
          - string
          description: What this customer pays you, e.g. 199. An amount is kept as an amount and never collapsed into a band. Legacy band strings (enterprise / paid / free) are still accepted.
          examples:
          - 199
        sku:
          type: string
          description: Product tier or plan identifier.
        external_id:
          type: string
          description: Your own identifier. A second submission carrying the same external_id is skipped, whatever the text says.
        source:
          type: string
          description: Where this came from. Defaults to `api`. Setting it is what makes Activity Log filtering useful.
          default: api
          examples:
          - api
          - widget
          - csv
          - slack
        project_id:
          type: string
          description: Home this feedback to a specific project.
      additionalProperties: true
    FeedbackBatchResult:
      type: object
      title: FeedbackBatchResult
      description: The outcome of a batch submission, item by item.
      required:
      - batch
      - total_items
      - successful
      - failed
      properties:
        batch:
          type: boolean
          const: true
        total_items:
          type: integer
        successful:
          type: integer
        queued:
          type: integer
          description: Held against a plan limit rather than dropped, and released when capacity returns.
        duplicates:
          type: integer
        failed:
          type: integer
        results:
          type: array
          items:
            type: object
        duplicates_list:
          type: array
          items:
            type: object
        errors:
          type: array
          items:
            type: object
        performance:
          type: object
          properties:
            total_time_seconds:
              type: number
            items_per_second:
              type: number
    WidgetSubmission:
      type: object
      title: WidgetSubmission
      description: A submission from the in-product survey. Note that the credential is the `api_key` field below, not a header.
      required:
      - api_key
      properties:
        api_key:
          type: string
          description: The survey key from Settings → Integrations → Survey. This is the credential for this endpoint.
        text:
          type: string
          description: The feedback content.
        email:
          type: string
          maxLength: 255
        feedback_type:
          type: string
          enum:
          - bug
          - feature
          - question
          - other
        page_url:
          type: string
          maxLength: 2000
          description: Where the feedback was given.
        screenshot:
          type: string
          description: Base64-encoded image.
        surface:
          type: string
          enum:
          - float
          - inline
          - bar
          - thumbs
          - page
          - widget
          description: Which collection surface captured this.
        rating:
          type: integer
        user_metadata:
          type: object
          description: Who the submitter is. Recognised keys include `email`, `name`, `full_name`, `displayName`, `id`, `plan`, `tier`, and any of `amount` / `mrr` / `arr` / `revenue` for what they pay.
          additionalProperties: true
  securitySchemes:
    sessionBearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: A Supabase session token for a signed-in user. Grants that user's own workspace scope and nothing wider. This is not the survey key — sending a survey key here returns 401.
    oauth2:
      type: oauth2
      description: OAuth 2.0 for the MCP resource `https://app.annsa.ai/mcp`. PKCE with S256 is required. Clients may register themselves dynamically (RFC 7591); server metadata is published per RFC 8414 at `/.well-known/oauth-authorization-server` and protected-resource metadata per RFC 9728 at `/.well-known/oauth-protected-resource/mcp`.
      flows:
        authorizationCode:
          authorizationUrl: https://app.annsa.ai/mcp/auth
          tokenUrl: https://app.annsa.ai/mcp/token
          refreshUrl: https://app.annsa.ai/mcp/token
          scopes:
            read: Read priorities, specs, survey results and search.
            write: Change state — start building, mark shipped, share back with customers.
externalDocs:
  description: 'Versioning, deprecation and rate limits: how the version header works, what counts as a breaking change, the 90 days of notice before anything is removed, and the ceilings each endpoint carries.'
  url: https://annsa.ai/api-policy